From f0e74ca479c9b0bfaaf23a78b032d24aa9e3a258 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ZX=E5=A4=8F=E5=A4=9C=E4=B9=8B=E9=A3=8E?= Date: Sat, 23 May 2026 16:21:28 +0800 Subject: [PATCH 1/2] refactor: AUTO fire mode uses start/stop packet model instead of per-bullet High-RPM weapons (e.g. minigun at 1200 RPM) suffered from intermittent shooting on remote servers due to network jitter causing timestamp validation failures on every per-bullet packet. Now AUTO mode (and continuous BURST) sends only start/stop packets. The server drives shooting in its tick loop, eliminating sensitivity to network timing. Client still locally drives animation/sound. --- .../client/gameplay/LocalPlayerShoot.java | 7 +- .../com/tacz/guns/client/input/ShootKey.java | 64 +++++++++++++++---- .../entity/shooter/LivingEntityShoot.java | 57 +++++++++++++++++ .../entity/shooter/ShooterDataHolder.java | 5 ++ .../guns/mixin/common/LivingEntityMixin.java | 2 + .../com/tacz/guns/network/NetworkHandler.java | 3 + .../message/ClientMessagePlayerAutoShoot.java | 56 ++++++++++++++++ 7 files changed, 179 insertions(+), 15 deletions(-) create mode 100644 src/main/java/com/tacz/guns/network/message/ClientMessagePlayerAutoShoot.java 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 066eb929d..fb2f2c32d 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,24 +58,58 @@ 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; - if (operator.chargeShoot(isShootDown)) { - LocalPlayerSprint.stopSprint = true; - if (fireMode != FireMode.AUTO && !isBurstAuto && 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; + } + if (operator.chargeShoot(isShootDown)) { + LocalPlayerSprint.stopSprint = true; + if (fireMode != FireMode.AUTO && !isBurstAuto && 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); + } +} From f39dc2f2d9dd8631bec6db68d91cab9dc05dfe7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ZX=E5=A4=8F=E5=A4=9C=E4=B9=8B=E9=A3=8E?= Date: Wed, 27 May 2026 14:01:01 +0800 Subject: [PATCH 2/2] =?UTF-8?q?style:=20ShootKey=20=E9=9D=9E=E5=85=A8?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=88=86=E6=94=AF=E6=B7=BB=E5=8A=A0=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E8=AF=B4=E6=98=8E=E8=93=84=E5=8A=9B=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/tacz/guns/client/input/ShootKey.java | 3 +++ 1 file changed, 3 insertions(+) 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 3d0eed280..5729597a6 100644 --- a/src/main/java/com/tacz/guns/client/input/ShootKey.java +++ b/src/main/java/com/tacz/guns/client/input/ShootKey.java @@ -89,9 +89,12 @@ public static void autoShoot(TickEvent.ClientTickEvent event) { 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; }