diff --git a/src/main/java/com/tacz/guns/client/gameplay/LocalPlayerShoot.java b/src/main/java/com/tacz/guns/client/gameplay/LocalPlayerShoot.java index d41a39ef4..42a9adbd5 100644 --- a/src/main/java/com/tacz/guns/client/gameplay/LocalPlayerShoot.java +++ b/src/main/java/com/tacz/guns/client/gameplay/LocalPlayerShoot.java @@ -13,6 +13,7 @@ import com.tacz.guns.client.resource.GunDisplayInstance; import com.tacz.guns.client.resource.index.ClientGunIndex; import com.tacz.guns.client.sound.SoundPlayManager; +import com.tacz.guns.entity.shooter.LivingEntityShoot; import com.tacz.guns.network.NetworkHandler; import com.tacz.guns.network.message.ClientMessagePlayerShoot; import com.tacz.guns.resource.index.CommonGunIndex; @@ -281,8 +282,10 @@ private void doShoot(GunDisplayInstance display, IGun iGun, ItemStack mainHandIt // 记录新的开火时间戳 data.clientLastShootTimestamp = data.clientShootTimestamp; data.clientShootTimestamp = System.currentTimeMillis(); - // 发送开火的数据包,通知服务器 - NetworkHandler.CHANNEL.sendToServer(new ClientMessagePlayerShoot(data.clientShootTimestamp - data.clientBaseTimestamp, chargeProgress)); + // AUTO 模式由服务端 tick 驱动射击,不发送 per-bullet 包 + if (!LivingEntityShoot.isAutoShootMode(fireMode, iGun, mainHandItem)) { + NetworkHandler.CHANNEL.sendToServer(new ClientMessagePlayerShoot(data.clientShootTimestamp - data.clientBaseTimestamp, chargeProgress)); + } } // todo 需要检查 diff --git a/src/main/java/com/tacz/guns/client/input/ShootKey.java b/src/main/java/com/tacz/guns/client/input/ShootKey.java index 440f0d5e9..5729597a6 100644 --- a/src/main/java/com/tacz/guns/client/input/ShootKey.java +++ b/src/main/java/com/tacz/guns/client/input/ShootKey.java @@ -9,6 +9,9 @@ import com.tacz.guns.client.gameplay.LocalPlayerSprint; import com.tacz.guns.client.sound.SoundPlayManager; import com.tacz.guns.compat.controllable.ControllableCompat; +import com.tacz.guns.entity.shooter.LivingEntityShoot; +import com.tacz.guns.network.NetworkHandler; +import com.tacz.guns.network.message.ClientMessagePlayerAutoShoot; import net.minecraft.client.KeyMapping; import net.minecraft.client.Minecraft; import net.minecraft.client.player.LocalPlayer; @@ -35,6 +38,7 @@ public class ShootKey { "key.category.tacz"); private static boolean lastTimeShootSuccess = false; private static boolean controllerShootDown = false; + private static boolean autoShootSent = false; @SubscribeEvent public static void autoShoot(TickEvent.ClientTickEvent event) { @@ -54,26 +58,62 @@ public static void autoShoot(TickEvent.ClientTickEvent event) { boolean isBurstAuto = fireMode == FireMode.BURST && TimelessAPI.getCommonGunIndex(iGun.getGunId(mainHandItem)) .map(index -> index.getGunData().getBurstData().isContinuousShoot()) .orElse(false); + boolean isAutoMode = LivingEntityShoot.isAutoShootMode(fireMode, iGun, mainHandItem); IClientPlayerGunOperator operator = IClientPlayerGunOperator.fromLocalPlayer(player); boolean isShootDown = SHOOT_KEY.isDown() || controllerShootDown; - boolean canContinuouslyShoot = fireMode == FireMode.AUTO || isBurstAuto; - boolean shouldCharge = isShootDown && (canContinuouslyShoot || !lastTimeShootSuccess); - if (operator.chargeShoot(shouldCharge)) { - LocalPlayerSprint.stopSprint = true; - if (!canContinuouslyShoot && lastTimeShootSuccess) { - // 非全自动情况,禁止连续开火,也不应在按住上一枪扳机时继续蓄力 - return; + + if (isAutoMode) { + if (operator.chargeShoot(isShootDown)) { + LocalPlayerSprint.stopSprint = true; + if (operator.shoot() == ShootResult.SUCCESS) { + if (!autoShootSent) { + NetworkHandler.CHANNEL.sendToServer(new ClientMessagePlayerAutoShoot(true)); + autoShootSent = true; + } + lastTimeShootSuccess = true; + ControllableCompat.onGunShoot(mainHandItem, fireMode); + } } - if (operator.shoot() == ShootResult.SUCCESS) { - lastTimeShootSuccess = true; - ControllableCompat.onGunShoot(mainHandItem, fireMode); + if (isShootDown) { + LocalPlayerSprint.stopSprint = true; + } else { + if (autoShootSent) { + NetworkHandler.CHANNEL.sendToServer(new ClientMessagePlayerAutoShoot(false)); + autoShootSent = false; + } + lastTimeShootSuccess = false; + SoundPlayManager.resetDryFireSound(); } - } - if (isShootDown) { - LocalPlayerSprint.stopSprint = true; } else { - lastTimeShootSuccess = false; - SoundPlayManager.resetDryFireSound(); + if (autoShootSent) { + NetworkHandler.CHANNEL.sendToServer(new ClientMessagePlayerAutoShoot(false)); + autoShootSent = false; + } + // 非全自动:上一发成功后不再主动蓄力,但仍需调用 chargeShoot 驱动蓄力衰减 + boolean shouldCharge = isShootDown && !lastTimeShootSuccess; + if (operator.chargeShoot(shouldCharge)) { + LocalPlayerSprint.stopSprint = true; + // HOLD 蓄力武器松开扳机时,若剩余蓄力仍超过阈值,chargeShoot(false) 也会返回 true + // 此处阻止已成功开火后的重复射击 + if (lastTimeShootSuccess) { + return; + } + if (operator.shoot() == ShootResult.SUCCESS) { + lastTimeShootSuccess = true; + ControllableCompat.onGunShoot(mainHandItem, fireMode); + } + } + if (isShootDown) { + LocalPlayerSprint.stopSprint = true; + } else { + lastTimeShootSuccess = false; + SoundPlayManager.resetDryFireSound(); + } + } + } else { + if (autoShootSent) { + NetworkHandler.CHANNEL.sendToServer(new ClientMessagePlayerAutoShoot(false)); + autoShootSent = false; } } } diff --git a/src/main/java/com/tacz/guns/entity/shooter/LivingEntityShoot.java b/src/main/java/com/tacz/guns/entity/shooter/LivingEntityShoot.java index 59dad6488..5bdac63e2 100644 --- a/src/main/java/com/tacz/guns/entity/shooter/LivingEntityShoot.java +++ b/src/main/java/com/tacz/guns/entity/shooter/LivingEntityShoot.java @@ -13,6 +13,7 @@ import com.tacz.guns.network.message.event.ServerMessageGunShoot; import com.tacz.guns.resource.index.CommonGunIndex; import com.tacz.guns.resource.pojo.data.gun.Bolt; +import com.tacz.guns.resource.pojo.data.gun.BurstData; import com.tacz.guns.resource.pojo.data.gun.ChargeData; import com.tacz.guns.resource.pojo.data.gun.ChargeType; import net.minecraft.resources.ResourceLocation; @@ -280,4 +281,60 @@ public void consumeAmmoFromPlayer(int neededAmount, ItemStack itemStack, boolean .map(cap -> abstractGunItem.findAndExtractInventoryAmmo(cap, itemStack, neededAmount)); } } + + public void tickAutoShoot(Supplier pitch, Supplier yaw) { + if (!data.isAutoShooting) { + return; + } + if (data.currentGunItem == null) { + data.isAutoShooting = false; + return; + } + ItemStack currentGunItem = data.currentGunItem.get(); + if (!(currentGunItem.getItem() instanceof IGun iGun)) { + data.isAutoShooting = false; + return; + } + FireMode fireMode = iGun.getFireMode(currentGunItem); + if (!isAutoShootMode(fireMode, iGun, currentGunItem)) { + data.isAutoShooting = false; + return; + } + // 额外 5ms 容差补偿 tick 抖动,避免高射速武器丢失射击 + // 此检查仅在 auto-shoot 路径中执行,不影响非 auto 武器 + if (getShootCoolDown() > 5) { + return; + } + long timestamp = System.currentTimeMillis() - data.baseTimestamp; + ShootResult result = shoot(pitch, yaw, timestamp); + switch (result) { + case SUCCESS: + case COOL_DOWN: + case IS_SPRINTING: + case IS_DRAWING: + case IS_BOLTING: + case IS_MELEE: + case NETWORK_FAIL: + break; + default: + data.isAutoShooting = false; + break; + } + } + + public static boolean isAutoShootMode(FireMode fireMode, IGun iGun, ItemStack gunItem) { + if (fireMode == FireMode.AUTO) { + return true; + } + if (fireMode == FireMode.BURST) { + ResourceLocation gunId = iGun.getGunId(gunItem); + return TimelessAPI.getCommonGunIndex(gunId) + .map(index -> { + BurstData burstData = index.getGunData().getBurstData(); + return burstData != null && burstData.isContinuousShoot(); + }) + .orElse(false); + } + return false; + } } diff --git a/src/main/java/com/tacz/guns/entity/shooter/ShooterDataHolder.java b/src/main/java/com/tacz/guns/entity/shooter/ShooterDataHolder.java index 9ffd356ad..0d24b95ab 100644 --- a/src/main/java/com/tacz/guns/entity/shooter/ShooterDataHolder.java +++ b/src/main/java/com/tacz/guns/entity/shooter/ShooterDataHolder.java @@ -101,6 +101,10 @@ public class ShooterDataHolder { public LuaValue scriptData = null; public long heatTimestamp = -1; + /** + * 是否正在自动射击(服务端 tick 驱动) + */ + public volatile boolean isAutoShooting = false; /** * 配件修改过的各种属性缓存 */ @@ -124,5 +128,6 @@ public void initialData() { chargeProgress = 0f; scriptData = null; heatTimestamp = -1; + isAutoShooting = false; } } diff --git a/src/main/java/com/tacz/guns/mixin/common/LivingEntityMixin.java b/src/main/java/com/tacz/guns/mixin/common/LivingEntityMixin.java index f189d6d1a..9f22a51d4 100644 --- a/src/main/java/com/tacz/guns/mixin/common/LivingEntityMixin.java +++ b/src/main/java/com/tacz/guns/mixin/common/LivingEntityMixin.java @@ -219,6 +219,8 @@ public void zoom() { private void onTickServerSide(CallbackInfo ci) { // 仅在服务端调用 if (!level().isClientSide()) { + // 自动射击 tick + this.tacz$shoot.tickAutoShoot(tacz$shooter::getXRot, tacz$shooter::getYRot); // 完成各种 tick 任务 ReloadState reloadState = this.tacz$reload.tickReloadState(); this.tacz$aim.tickAimingProgress(); diff --git a/src/main/java/com/tacz/guns/network/NetworkHandler.java b/src/main/java/com/tacz/guns/network/NetworkHandler.java index 46411828d..37c2f092f 100644 --- a/src/main/java/com/tacz/guns/network/NetworkHandler.java +++ b/src/main/java/com/tacz/guns/network/NetworkHandler.java @@ -102,6 +102,9 @@ public static void init() { CHANNEL.registerMessage(ID_COUNT.getAndIncrement(), ClientMessageLaserColor.class, ClientMessageLaserColor::encode, ClientMessageLaserColor::decode, ClientMessageLaserColor::handle, Optional.of(NetworkDirection.PLAY_TO_SERVER)); + CHANNEL.registerMessage(ID_COUNT.getAndIncrement(), ClientMessagePlayerAutoShoot.class, ClientMessagePlayerAutoShoot::encode, ClientMessagePlayerAutoShoot::decode, ClientMessagePlayerAutoShoot::handle, + Optional.of(NetworkDirection.PLAY_TO_SERVER)); + registerAcknowledge(); registerHandshakeMessage(ServerMessageSyncedEntityDataMapping.class, null); } diff --git a/src/main/java/com/tacz/guns/network/message/ClientMessagePlayerAutoShoot.java b/src/main/java/com/tacz/guns/network/message/ClientMessagePlayerAutoShoot.java new file mode 100644 index 000000000..f7cbbd0e6 --- /dev/null +++ b/src/main/java/com/tacz/guns/network/message/ClientMessagePlayerAutoShoot.java @@ -0,0 +1,56 @@ +package com.tacz.guns.network.message; + +import com.tacz.guns.api.entity.IGunOperator; +import com.tacz.guns.api.item.IGun; +import com.tacz.guns.api.item.gun.FireMode; +import com.tacz.guns.entity.shooter.LivingEntityShoot; +import com.tacz.guns.entity.shooter.ShooterDataHolder; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.network.NetworkEvent; + +import java.util.function.Supplier; + +public class ClientMessagePlayerAutoShoot { + private final boolean shooting; + + public ClientMessagePlayerAutoShoot(boolean shooting) { + this.shooting = shooting; + } + + public static void encode(ClientMessagePlayerAutoShoot message, FriendlyByteBuf buf) { + buf.writeBoolean(message.shooting); + } + + public static ClientMessagePlayerAutoShoot decode(FriendlyByteBuf buf) { + return new ClientMessagePlayerAutoShoot(buf.readBoolean()); + } + + public static void handle(ClientMessagePlayerAutoShoot message, Supplier contextSupplier) { + NetworkEvent.Context context = contextSupplier.get(); + if (context.getDirection().getReceptionSide().isServer()) { + context.enqueueWork(() -> { + ServerPlayer entity = context.getSender(); + if (entity == null) { + return; + } + ShooterDataHolder dataHolder = IGunOperator.fromLivingEntity(entity).getDataHolder(); + if (message.shooting) { + ItemStack mainHandItem = entity.getMainHandItem(); + if (!(mainHandItem.getItem() instanceof IGun iGun)) { + return; + } + FireMode fireMode = iGun.getFireMode(mainHandItem); + if (!LivingEntityShoot.isAutoShootMode(fireMode, iGun, mainHandItem)) { + return; + } + dataHolder.isAutoShooting = true; + } else { + dataHolder.isAutoShooting = false; + } + }); + } + context.setPacketHandled(true); + } +}