diff --git a/build.gradle b/build.gradle index c1d0496b3..35bbec3cc 100644 --- a/build.gradle +++ b/build.gradle @@ -241,6 +241,12 @@ dependencies { jarJar.ranged(it, "[0.3.6,)") } annotationProcessor 'org.spongepowered:mixin:0.8.5:processor' + implementation 'it.unimi.dsi:fastutil:8.5.18' + /* + implementation(jarJar('it.unimi.dsi:fastutil:8.5.18')) { + jarJar.ranged(it, '[8.5,9.0)') + }*/ + annotationProcessor 'org.spongepowered:mixin:0.8.5:processor' } jar { @@ -262,4 +268,4 @@ java { tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' -} +} \ No newline at end of file diff --git a/src/main/java/com/tacz/guns/GunMod.java b/src/main/java/com/tacz/guns/GunMod.java index 2bb890c84..5c9b1353f 100644 --- a/src/main/java/com/tacz/guns/GunMod.java +++ b/src/main/java/com/tacz/guns/GunMod.java @@ -8,6 +8,7 @@ import com.tacz.guns.init.*; import com.tacz.guns.resource.GunPackLoader; import com.tacz.guns.resource.modifier.AttachmentPropertyManager; +import com.tacz.guns.util.ShootBus; import net.minecraft.server.packs.PackType; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.eventbus.api.IEventBus; @@ -28,7 +29,6 @@ public class GunMod { * 默认模型包文件夹 */ public static final String DEFAULT_GUN_PACK_NAME = "tacz_default_gun"; - public GunMod() { ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, CommonConfig.init()); ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, ServerConfig.init()); diff --git a/src/main/java/com/tacz/guns/api/client/gameplay/IClientPlayerGunOperator.java b/src/main/java/com/tacz/guns/api/client/gameplay/IClientPlayerGunOperator.java index 201deea05..191e57ea9 100644 --- a/src/main/java/com/tacz/guns/api/client/gameplay/IClientPlayerGunOperator.java +++ b/src/main/java/com/tacz/guns/api/client/gameplay/IClientPlayerGunOperator.java @@ -27,6 +27,11 @@ static IClientPlayerGunOperator fromLocalPlayer(LocalPlayer player) { */ ShootResult shoot(); + /** + * 停止全自动射击 + */ + void stopFullAuto(); + /** * 执行客户端切枪逻辑。 */ diff --git a/src/main/java/com/tacz/guns/api/entity/IGunOperator.java b/src/main/java/com/tacz/guns/api/entity/IGunOperator.java index 4ce730410..948f44a4c 100644 --- a/src/main/java/com/tacz/guns/api/entity/IGunOperator.java +++ b/src/main/java/com/tacz/guns/api/entity/IGunOperator.java @@ -118,6 +118,29 @@ static IGunOperator fromLivingEntity(LivingEntity entity) { */ ShootResult shoot(Supplier pitch, Supplier yaw, long timestamp); + /** + * 从实体的位置,向指定的方向开枪。计算冷却的时候使用指定的 timestamp。指定包含的弹药数和射击发起的来源 + * + * @param pitch 开火方向的俯仰角(即 xRot ) + * @param yaw 开火方向的偏航角(即 yRot ) + * @param timestamp 指定的时间戳,为偏移时间戳(相对于 base timestamp 的时间戳) + * @param count 包含的弹药数 + * @param fromServer true为来自服务器,false为来自客户端 + * @return 本次射击的结果 + */ + ShootResult shoot(Supplier pitch, Supplier yaw, long timestamp, int count, boolean fromServer); + + /** + * 开始全自动射击 + * @param timestamp 开始的时间戳 + */ + void startFullAuto(long timestamp); + + /** + * 停止全自动射击 + */ + void stopFullAuto(); + /** * 服务端,该操作者是否受弹药数影响 * diff --git a/src/main/java/com/tacz/guns/api/item/gun/AbstractGunItem.java b/src/main/java/com/tacz/guns/api/item/gun/AbstractGunItem.java index 9a99c0f3e..35d0db28a 100644 --- a/src/main/java/com/tacz/guns/api/item/gun/AbstractGunItem.java +++ b/src/main/java/com/tacz/guns/api/item/gun/AbstractGunItem.java @@ -62,7 +62,7 @@ private static Comparator> idNameSor /** * 射击时触发 */ - public abstract void shoot(ShooterDataHolder dataHolder, ItemStack gunItem, Supplier pitch, Supplier yaw, LivingEntity shooter); + public abstract void shoot(ShooterDataHolder dataHolder, ItemStack gunItem, Supplier pitch, Supplier yaw, LivingEntity shooter, int count); /** * 开始换弹时调用 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 302a3c262..8610a4f2b 100644 --- a/src/main/java/com/tacz/guns/client/gameplay/LocalPlayerShoot.java +++ b/src/main/java/com/tacz/guns/client/gameplay/LocalPlayerShoot.java @@ -1,5 +1,6 @@ package com.tacz.guns.client.gameplay; +import com.tacz.guns.GunMod; import com.tacz.guns.api.TimelessAPI; import com.tacz.guns.api.client.animation.statemachine.AnimationStateMachine; import com.tacz.guns.api.client.gameplay.IClientPlayerGunOperator; @@ -15,12 +16,15 @@ import com.tacz.guns.client.sound.SoundPlayManager; import com.tacz.guns.network.NetworkHandler; import com.tacz.guns.network.message.ClientMessagePlayerShoot; +import com.tacz.guns.network.message.ClientMessagePlayerShootBegin; +import com.tacz.guns.network.message.ClientMessagePlayerShootEnd; import com.tacz.guns.resource.index.CommonGunIndex; import com.tacz.guns.resource.modifier.AttachmentCacheProperty; import com.tacz.guns.resource.modifier.custom.SilenceModifier; import com.tacz.guns.resource.pojo.data.gun.Bolt; import com.tacz.guns.resource.pojo.data.gun.GunData; import com.tacz.guns.sound.SoundManager; +import com.tacz.guns.util.ShootBus; import it.unimi.dsi.fastutil.Pair; import net.minecraft.client.Minecraft; import net.minecraft.client.player.LocalPlayer; @@ -30,6 +34,8 @@ import net.minecraftforge.fml.LogicalSide; import java.util.Optional; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -39,6 +45,13 @@ public class LocalPlayerShoot { private static final Predicate SHOOT_LOCKED_CONDITION = operator -> operator.getSynShootCoolDown() > 0; private final LocalPlayerDataHolder data; private final LocalPlayer player; + private static final ScheduledExecutorService SHOOT_SCHEDULER = + Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "Gun-AutoShoot-Scheduler"); + t.setDaemon(true); + return t; + }); + private ScheduledFuture shootTask; public LocalPlayerShoot(LocalPlayerDataHolder data, LocalPlayer player) { this.data = data; @@ -144,6 +157,41 @@ public ShootResult shoot() { private void doShoot(GunDisplayInstance display, IGun iGun, ItemStack mainHandItem, GunData gunData, long delay) { FireMode fireMode = iGun.getFireMode(mainHandItem); + //如果是全自动则按照射速应用后坐力,直到松开射击键或弹药耗尽由服务器调用stopFullAuto() + if(fireMode == FireMode.AUTO) { + data.isShootRecorded = true; + NetworkHandler.CHANNEL.sendToServer(new ClientMessagePlayerShootBegin(data.clientShootTimestamp - data.clientBaseTimestamp)); + int rpm = iGun.getRPM(this.player.getMainHandItem()); + double roundsPerSecond = rpm / 60.0; + long intervalNanos = (long) (1_000_000_000.0 / roundsPerSecond); + ScheduledFuture task = SHOOT_SCHEDULER.scheduleAtFixedRate( + () -> { + Minecraft.getInstance().submitAsync(() -> { + // 触发击发事件 + boolean fire = !MinecraftForge.EVENT_BUS.post(new GunFireEvent(player, mainHandItem, LogicalSide.CLIENT)); + if (fire) { + // 动画和声音循环播放 + AnimationStateMachine animationStateMachine = display.getAnimationStateMachine(); + if (animationStateMachine != null) { + animationStateMachine.trigger(GunAnimationConstant.INPUT_SHOOT); + } + // 获取消音 + final boolean useSilenceSound = this.useSilenceSound(); + // 开火需要打断检视 + SoundPlayManager.stopPlayGunSound(display, SoundManager.INSPECT_SOUND); + if (useSilenceSound) { + SoundPlayManager.playSilenceSound(player, display, gunData); + } else { + SoundPlayManager.playShootSound(player, display, gunData); + } + } + }); + }, + 0, intervalNanos, TimeUnit.NANOSECONDS + ); + shootTask = task; + return; + } Bolt boltType = gunData.getBolt(); // 获取余弹数 boolean consumeAmmo = IGunOperator.fromLivingEntity(player).consumesAmmoOrNot(); @@ -216,7 +264,14 @@ private void doShoot(GunDisplayInstance display, IGun iGun, ItemStack mainHandIt count.getAndIncrement(); }, delay, period, TimeUnit.MILLISECONDS); } - + public boolean stopFullAuto() { + data.isShootRecorded = true; + if(shootTask != null) { + shootTask.cancel(true); + } + NetworkHandler.CHANNEL.sendToServer(new ClientMessagePlayerShootEnd(data.clientShootTimestamp - data.clientBaseTimestamp)); + return true; + } private boolean useSilenceSound() { AttachmentCacheProperty cacheProperty = IGunOperator.fromLivingEntity(player).getCacheProperty(); if (cacheProperty != null) { 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 c1c1f2b30..442608034 100644 --- a/src/main/java/com/tacz/guns/client/input/ShootKey.java +++ b/src/main/java/com/tacz/guns/client/input/ShootKey.java @@ -28,6 +28,7 @@ @OnlyIn(Dist.CLIENT) @Mod.EventBusSubscriber(value = Dist.CLIENT) public class ShootKey { + public static boolean pushDown = false; public static final KeyMapping SHOOT_KEY = new KeyMapping("key.tacz.shoot.desc", KeyConflictContext.IN_GAME, KeyModifier.NONE, @@ -63,10 +64,16 @@ public static void autoShoot(TickEvent.ClientTickEvent event) { // 非全自动情况,禁止连续开火 return; } - if (operator.shoot() == ShootResult.SUCCESS) { - lastTimeShootSuccess = true; + //开始全自动射击 + if(fireMode == FireMode.AUTO && !pushDown) { + if (operator.shoot() == ShootResult.SUCCESS) { + lastTimeShootSuccess = true; + } + pushDown = true; } } else { + operator.stopFullAuto(); + pushDown = false; lastTimeShootSuccess = false; } } @@ -154,4 +161,4 @@ public static boolean semiShootController(boolean isPress) { } return false; } -} +} \ No newline at end of file diff --git a/src/main/java/com/tacz/guns/entity/EntityKineticBullet.java b/src/main/java/com/tacz/guns/entity/EntityKineticBullet.java index b1f03ae3c..634ef9b7a 100644 --- a/src/main/java/com/tacz/guns/entity/EntityKineticBullet.java +++ b/src/main/java/com/tacz/guns/entity/EntityKineticBullet.java @@ -128,6 +128,8 @@ public class EntityKineticBullet extends Projectile implements IEntityAdditional private boolean explosionKnockback = false; private boolean explosionDestroyBlock = false; private float damageModifier = 1; + //子弹数量(将多发子弹压入同一个实体内处理,实现超过1200的射速) + private int bulletCount = 1; // 穿透数 private int pierce = 1; // 初始位置 @@ -155,17 +157,21 @@ public EntityKineticBullet(EntityType type, double x, doub } public EntityKineticBullet(Level worldIn, LivingEntity throwerIn, ItemStack gunItem, ResourceLocation ammoId, ResourceLocation gunId, - ResourceLocation gunDisplayId, boolean isTracerAmmo, GunData gunData, BulletData bulletData) { - this(TYPE, worldIn, throwerIn, gunItem, ammoId, gunId, gunDisplayId, isTracerAmmo, gunData, bulletData); + ResourceLocation gunDisplayId, boolean isTracerAmmo, GunData gunData, BulletData bulletData) { + this(TYPE, worldIn, throwerIn, gunItem, ammoId, gunId, gunDisplayId, isTracerAmmo, gunData, bulletData, 1); + } + public EntityKineticBullet(Level worldIn, LivingEntity throwerIn, ItemStack gunItem, ResourceLocation ammoId, ResourceLocation gunId, + ResourceLocation gunDisplayId, boolean isTracerAmmo, GunData gunData, BulletData bulletData, int bulletCount) { + this(TYPE, worldIn, throwerIn, gunItem, ammoId, gunId, gunDisplayId, isTracerAmmo, gunData, bulletData, bulletCount); } public EntityKineticBullet(Level worldIn, LivingEntity throwerIn, ItemStack gunItem, ResourceLocation ammoId, ResourceLocation gunId, boolean isTracerAmmo, GunData gunData, BulletData bulletData) { - this(TYPE, worldIn, throwerIn, gunItem, ammoId, gunId, DefaultAssets.DEFAULT_GUN_DISPLAY_ID, isTracerAmmo, gunData, bulletData); + this(TYPE, worldIn, throwerIn, gunItem, ammoId, gunId, DefaultAssets.DEFAULT_GUN_DISPLAY_ID, isTracerAmmo, gunData, bulletData, 1); } protected EntityKineticBullet(EntityType type, Level worldIn, LivingEntity throwerIn, ItemStack gunItem, ResourceLocation ammoId, ResourceLocation gunId, ResourceLocation gunDisplayId, - boolean isTracerAmmo, GunData gunData, BulletData bulletData) { + boolean isTracerAmmo, GunData gunData, BulletData bulletData, int bulletCount) { this(type, throwerIn.getX(), throwerIn.getEyeY() - (double) 0.1F, throwerIn.getZ(), worldIn); this.setOwner(throwerIn); // gunId 提前赋值,以让 modifyProperty 可以在构造函数中运行 @@ -190,6 +196,7 @@ protected EntityKineticBullet(EntityType type, Level world this.igniteBlock = modifyProperty(IGNITE_BLOCK, Boolean.class, bulletData.getIgnite().isIgniteBlock() || ignite.isIgniteBlock()); this.damageAmount = cacheProperty.getCache(DamageModifier.ID); this.distanceAmount = modifyProperty(GunProperties.EFFECTIVE_RANGE, Float.class, cacheProperty.getCache(GunProperties.EFFECTIVE_RANGE)); + this.bulletCount = bulletCount; int pierce = modifyProperty(GunProperties.PIERCE, Integer.class, cacheProperty.getCache(GunProperties.PIERCE)); this.pierce = Mth.clamp(pierce, 1, Integer.MAX_VALUE); ExplosionData explosionData = Objects.requireNonNullElse(cacheProperty.getCache(ExplosionModifier.ID), DEFAULT_EXPLOSION_DATA); @@ -321,27 +328,55 @@ protected void onBulletTick() { } // 当子弹击中实体时,进行被命中的实体读取 if (hitEntities != null && !hitEntities.isEmpty()) { - EntityResult[] hitEntityResult = hitEntities.toArray(new EntityResult[0]); - // 对被命中的实体进行排序,按照距离子弹发射位置的距离进行升序排序 - for (int i = 0; (i < this.pierce || i < 1) && i < (hitEntityResult.length - 1); i++) { - int k = i; - for (int j = i + 1; j < hitEntityResult.length; j++) { - if (hitEntityResult[j].hitVec.distanceTo(startVec) < hitEntityResult[k].hitVec.distanceTo(startVec)) { - k = j; + //判断是否需要更新实体列表 + boolean entityDestroyed = false; + //对于本实体包含的每一发子弹进行一次判定 + int count = bulletCount; + for(int i = 0; i < count; i++) { + if(entityDestroyed) { + // 子弹的击中检测,穿透为 1 或者爆炸类弹药限制为一个实体穿透判定 + if (this.pierce <= 1 || this.explosion) { + EntityResult entityResult = EntityUtil.findEntityOnPath(this, startVec, endVec); + // 将单个命中是实体创建为单个内容的 list + if (entityResult != null) { + hitEntities = Collections.singletonList(entityResult); + } + } else { + hitEntities = EntityUtil.findEntitiesOnPath(this, startVec, endVec); + } + //判断是否还有实体 + if (hitEntities.isEmpty()) { + break; } } - EntityResult t = hitEntityResult[i]; - hitEntityResult[i] = hitEntityResult[k]; - hitEntityResult[k] = t; - } - for (EntityResult entityResult : hitEntityResult) { - result = new TacHitResult(entityResult); - this.onHitEntity((TacHitResult) result, startVec, endVec); - this.pierce--; - if (this.pierce < 1 || this.explosion) { - // 子弹已经穿透所有实体,结束子弹的飞行 - this.discard(); - return; + EntityResult[] hitEntityResult = hitEntities.toArray(new EntityResult[0]); + // 对被命中的实体进行排序,按照距离子弹发射位置的距离进行升序排序 + for (int j = 0; (i < this.pierce || j < 1) && j < (hitEntityResult.length - 1); j++) { + int l = j; + for (int k = j + 1; k < hitEntityResult.length; k++) { + if (hitEntityResult[k].hitVec.distanceTo(startVec) < hitEntityResult[l].hitVec.distanceTo(startVec)) { + l = k; + } + } + EntityResult t = hitEntityResult[j]; + hitEntityResult[j] = hitEntityResult[l]; + hitEntityResult[l] = t; + } + for (EntityResult entityResult : hitEntityResult) { + result = new TacHitResult(entityResult); + String hitResult = this.onHitEntity((TacHitResult) result, startVec, endVec); + if(Objects.equals(hitResult, "DEAD")) { + entityDestroyed = true; + } + + this.pierce--; + if (this.pierce < 1 || this.explosion) { + // 子弹已经穿透所有实体,结束子弹的飞行 + this.bulletCount--; + //减少子弹数量,如果已全部消耗则清除自身 + if(this.bulletCount == 0) + this.discard(); + } } } } @@ -383,12 +418,12 @@ public static MaybeMultipartEntity of(Entity hitPart) { } } - protected void onHitEntity(TacHitResult result, Vec3 startVec, Vec3 endVec) { + protected String onHitEntity(TacHitResult result, Vec3 startVec, Vec3 endVec) { if (result.getEntity() instanceof ITargetEntity targetEntity) { DamageSource source = this.damageSources().thrown(this, this.getOwner()); targetEntity.onProjectileHit(this, result, source, this.getDamage(result.getLocation())); // 打靶直接返回 - return; + return "SHOT_TARGET"; } // 获取Pre事件必要的信息 Entity entity = result.getEntity(); @@ -403,7 +438,7 @@ protected void onHitEntity(TacHitResult result, Vec3 startVec, Vec3 endVec) { var preEvent = new EntityHurtByGunEvent.Pre(this, entity, attacker, this.gunId, this.gunDisplayId, damage, sources, headshot, headShotMultiplier, LogicalSide.SERVER); var cancelled = MinecraftForge.EVENT_BUS.post(preEvent); if (cancelled) { - return; + return "CANCELLED"; } // 刷新由Pre事件修改后的参数 entity = preEvent.getHurtEntity(); @@ -416,7 +451,7 @@ protected void onHitEntity(TacHitResult result, Vec3 startVec, Vec3 endVec) { headshot = preEvent.isHeadShot(); headShotMultiplier = preEvent.getHeadshotMultiplier(); if (entity == null) { - return; + return "NOT_HIT"; } // 点燃 if (this.igniteEntity && AmmoConfig.IGNITE_ENTITY.get()) { @@ -459,12 +494,15 @@ protected void onHitEntity(TacHitResult result, Vec3 startVec, Vec3 endVec) { if (livingCore.isDeadOrDying()) { MinecraftForge.EVENT_BUS.post(new EntityKillByGunEvent(this, livingCore, attacker, newGunId, gunDisplayId, damage, sources, headshot, headShotMultiplier, LogicalSide.SERVER)); NetworkHandler.sendToDimension(new ServerMessageGunKill(getId(), livingCore.getId(), attackerId, newGunId, gunDisplayId, damage, headshot, headShotMultiplier), livingCore); + return "DEAD"; } else { MinecraftForge.EVENT_BUS.post(new EntityHurtByGunEvent.Post(this, livingCore, attacker, newGunId, gunDisplayId, damage, sources, headshot, headShotMultiplier, LogicalSide.SERVER)); NetworkHandler.sendToDimension(new ServerMessageGunHurt(getId(), livingCore.getId(), attackerId, newGunId, gunDisplayId, damage, headshot, headShotMultiplier), livingCore); + return "ALIVE"; } } } + return "UNKNOWN"; } protected void onHitBlock(BlockHitResult result, Vec3 startVec, Vec3 endVec) { 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 9b2b31798..00591cbf0 100644 --- a/src/main/java/com/tacz/guns/entity/shooter/LivingEntityShoot.java +++ b/src/main/java/com/tacz/guns/entity/shooter/LivingEntityShoot.java @@ -1,5 +1,6 @@ package com.tacz.guns.entity.shooter; +import com.tacz.guns.GunMod; import com.tacz.guns.api.TimelessAPI; import com.tacz.guns.api.entity.IGunOperator; import com.tacz.guns.api.entity.ShootResult; @@ -9,14 +10,17 @@ import com.tacz.guns.api.item.gun.FireMode; import com.tacz.guns.config.sync.SyncConfig; import com.tacz.guns.network.NetworkHandler; +import com.tacz.guns.network.message.ServerMessageGunStop; import com.tacz.guns.network.message.ServerMessageSyncBaseTimestamp; 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.util.ShootBus; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.capabilities.ForgeCapabilities; @@ -25,20 +29,31 @@ import java.util.Objects; import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; import java.util.function.Supplier; public class LivingEntityShoot { private final LivingEntity shooter; private final ShooterDataHolder data; private final LivingEntityDrawGun draw; - + private static final ScheduledExecutorService SHOOT_SCHEDULER = + Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "Gun-AutoShoot-Scheduler"); + t.setDaemon(true); + return t; + }); + private ScheduledFuture shootTask; public LivingEntityShoot(LivingEntity shooter, ShooterDataHolder data, LivingEntityDrawGun draw) { this.shooter = shooter; this.data = data; this.draw = draw; } - public ShootResult shoot(Supplier pitch, Supplier yaw, long timestamp) { + public ShootResult shoot(Supplier pitch, Supplier yaw, long timestamp, int count, boolean fromServer) { if (data.currentGunItem == null) { return ShootResult.NOT_DRAW; } @@ -63,8 +78,8 @@ public ShootResult shoot(Supplier pitch, Supplier yaw, long timest return ShootResult.COOL_DOWN; } } - if (SyncConfig.SERVER_SHOOT_NETWORK_V.get()) { - // 根据 tick time 和 允许的网络延迟波动 计算 时间戳的接受窗口 + if (SyncConfig.SERVER_SHOOT_NETWORK_V.get() && !fromServer) { + // 根据 tick time 和 允许的网络延迟波动 计算 时间戳的接受窗口 如果来自服务器则无需计算窗口可直接使用 MinecraftServer server = Objects.requireNonNull(shooter.getServer()); double tickTime = Math.max(server.tickTimes[server.getTickCount() % 100] * 1.0E-6D, 50); long alpha = System.currentTimeMillis() - data.baseTimestamp - timestamp; @@ -138,11 +153,33 @@ public ShootResult shoot(Supplier pitch, Supplier yaw, long timest data.shootTimestamp = timestamp; // 执行枪械射击逻辑 if (iGun instanceof AbstractGunItem logicGun) { - logicGun.shoot(data, currentGunItem, pitch, yaw, shooter); + logicGun.shoot(data, currentGunItem, pitch, yaw, shooter, count); + } + if(((IGun)shooter.getMainHandItem().getItem()).getFireMode(shooter.getMainHandItem()) == FireMode.AUTO && + ((IGun)shooter.getMainHandItem().getItem()).getCurrentAmmoCount(shooter.getMainHandItem()) <= 0 && + !((IGun)shooter.getMainHandItem().getItem()).hasBulletInBarrel(shooter.getMainHandItem())) { + NetworkHandler.sendToClientPlayer(new ServerMessageGunStop(shooter.getId()), (Player) shooter); } return ShootResult.SUCCESS; } - + public boolean startFullAuto(long timestamp) { + UUID playerUuid = this.shooter.getUUID(); + if(this.shooter.getMainHandItem().getItem() instanceof IGun iGun) { + int rpm = iGun.getRPM(this.shooter.getMainHandItem()); + double roundsPerSecond = rpm / 60.0; + long intervalNanos = (long) (1_000_000_000.0 / roundsPerSecond); + ScheduledFuture task = SHOOT_SCHEDULER.scheduleAtFixedRate( + () -> ShootBus.addShot(playerUuid), + 0, intervalNanos, TimeUnit.NANOSECONDS + ); + shootTask = task; + return true; + } + return false; + } + public boolean stopFullAuto() { + return shootTask.cancel(true); + } /** * 以当前时间戳查询射击冷却。返回值一般不会超过枪械的射击间隔 * @return 射击冷却 diff --git a/src/main/java/com/tacz/guns/item/ModernKineticGunItem.java b/src/main/java/com/tacz/guns/item/ModernKineticGunItem.java index 87a859851..96172994e 100644 --- a/src/main/java/com/tacz/guns/item/ModernKineticGunItem.java +++ b/src/main/java/com/tacz/guns/item/ModernKineticGunItem.java @@ -97,7 +97,7 @@ public boolean tickBolt(ShooterDataHolder dataHolder, ItemStack gunItem, LivingE } @Override - public void shoot(ShooterDataHolder dataHolder, ItemStack gunItem, Supplier pitch, Supplier yaw, LivingEntity shooter) { + public void shoot(ShooterDataHolder dataHolder, ItemStack gunItem, Supplier pitch, Supplier yaw, LivingEntity shooter, int count) { ModernKineticGunScriptAPI api = new ModernKineticGunScriptAPI(); api.setItemStack(gunItem); api.setShooter(shooter); @@ -114,7 +114,7 @@ public void shoot(ShooterDataHolder dataHolder, ItemStack gunItem, Supplier checkFunction(script.get("shoot"))) .ifPresentOrElse( func -> func.call(CoerceJavaToLua.coerce(api)), - () -> api.shootOnce(api.isShootingNeedConsumeAmmo())); + () -> api.shootOnce(api.isShootingNeedConsumeAmmo(), count)); } @Override diff --git a/src/main/java/com/tacz/guns/item/ModernKineticGunScriptAPI.java b/src/main/java/com/tacz/guns/item/ModernKineticGunScriptAPI.java index 6718147c2..22aebd8cc 100644 --- a/src/main/java/com/tacz/guns/item/ModernKineticGunScriptAPI.java +++ b/src/main/java/com/tacz/guns/item/ModernKineticGunScriptAPI.java @@ -1,5 +1,6 @@ package com.tacz.guns.item; +import com.tacz.guns.GunMod; import com.tacz.guns.api.DefaultAssets; import com.tacz.guns.api.GunProperties; import com.tacz.guns.api.GunProperty; @@ -8,6 +9,7 @@ import com.tacz.guns.api.event.common.GunFireEvent; import com.tacz.guns.api.item.IAmmo; import com.tacz.guns.api.item.IAmmoBox; +import com.tacz.guns.api.item.IGun; import com.tacz.guns.api.item.attachment.AttachmentType; import com.tacz.guns.api.item.gun.AbstractGunItem; import com.tacz.guns.api.item.gun.FireMode; @@ -19,6 +21,7 @@ import com.tacz.guns.entity.shooter.ShooterDataHolder; import com.tacz.guns.network.NetworkHandler; import com.tacz.guns.network.message.event.ServerMessageGunFire; +import com.tacz.guns.network.message.ServerMessageGunStop; import com.tacz.guns.resource.index.CommonGunIndex; import com.tacz.guns.resource.modifier.AttachmentCacheProperty; import com.tacz.guns.resource.modifier.custom.SilenceModifier; @@ -30,11 +33,13 @@ import net.minecraft.resources.ResourceLocation; import net.minecraft.util.Mth; import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.capabilities.ForgeCapabilities; import net.minecraftforge.fml.LogicalSide; +import org.antlr.v4.parse.ANTLRParser; import org.luaj.vm2.LuaError; import org.luaj.vm2.LuaFunction; import org.luaj.vm2.LuaTable; @@ -98,7 +103,7 @@ public LuaValue getCachedProperty(String id) { * 执行一次完整的射击逻辑,会考虑玩家的状态(是否在瞄准、是否在移动、是否在匍匐等)、配件数值影响、多弹丸散射、连发,播放开火音效、 * @param consumeAmmo 本次射击是否消耗弹药 */ - public void shootOnce(boolean consumeAmmo){ + public void shootOnce(boolean consumeAmmo, int count) { GunData gunData = gunIndex.getGunData(); BulletData bulletData = gunIndex.getBulletData(); IGunOperator gunOperator = IGunOperator.fromLivingEntity(shooter); @@ -143,6 +148,7 @@ public void shootOnce(boolean consumeAmmo){ long period = modifyProperty(GunProperties.RuntimeOnly.BURST_SHOOT_INTERVAL, Long.class, fireMode == FireMode.BURST ? gunData.getBurstShootInterval() : 1); CycleTaskHelper.addCycleTask(() -> { + int bulletCount = count; // 如果射击者死亡,取消射击 if (shooter.isDeadOrDying()) { return false; @@ -157,7 +163,15 @@ public void shootOnce(boolean consumeAmmo){ NetworkHandler.sendToTrackingEntity(new ServerMessageGunFire(shooter.getId(), itemStack), shooter); // 削减弹药 if (consumeAmmo) { - if (!this.reduceAmmoOnce()) { + int i = bulletCount; + while (i > 0) { + if (!this.reduceAmmoOnce()) { + bulletCount -= i; + break; + } + i-=1; + } + if(bulletCount <= 0) { return false; } } @@ -179,7 +193,7 @@ public void shootOnce(boolean consumeAmmo){ for (int i = 0; i < bulletAmount; i++) { boolean isTracer = bulletData.hasTracerAmmo() && gunOperator.nextBulletIsTracer(bulletData.getTracerCountInterval()); EntityKineticBullet bullet = new EntityKineticBullet(world, shooter, itemStack, ammoId, gunId, - gunDisplayId, isTracer, gunData, bulletData); + gunDisplayId, isTracer, gunData, bulletData, bulletCount); bullet.applyShotgunDamageSpread(bulletAmount); abstractGunItem.doBulletSpread(dataHolder, itemStack, shooter, bullet, i, processedSpeed, inaccuracy, pitch, yaw); diff --git a/src/main/java/com/tacz/guns/mixin/client/LocalPlayerMixin.java b/src/main/java/com/tacz/guns/mixin/client/LocalPlayerMixin.java index 6326be452..e5b90e5df 100644 --- a/src/main/java/com/tacz/guns/mixin/client/LocalPlayerMixin.java +++ b/src/main/java/com/tacz/guns/mixin/client/LocalPlayerMixin.java @@ -36,6 +36,12 @@ public ShootResult shoot() { return tac$shoot.shoot(); } + @Unique + @Override + public void stopFullAuto() { + tac$shoot.stopFullAuto(); + } + @Unique @Override public void draw(ItemStack lastItem) { 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 5e2f18769..427106f8b 100644 --- a/src/main/java/com/tacz/guns/mixin/common/LivingEntityMixin.java +++ b/src/main/java/com/tacz/guns/mixin/common/LivingEntityMixin.java @@ -140,7 +140,24 @@ public ShootResult shoot(Supplier pitch, Supplier yaw) { @Unique @Override public ShootResult shoot(Supplier pitch, Supplier yaw, long timestamp) { - return tacz$shoot.shoot(pitch, yaw, timestamp); + return this.shoot(pitch, yaw, timestamp, 1, false); + } + @Unique + @Override + public ShootResult shoot(Supplier pitch, Supplier yaw, long timestamp, int count, boolean source) { + return tacz$shoot.shoot(pitch, yaw, timestamp, count, source); + } + + @Unique + @Override + public void startFullAuto(long timestamp) { + tacz$shoot.startFullAuto(timestamp); + } + + @Unique + @Override + public void stopFullAuto() { + tacz$shoot.stopFullAuto(); } @Unique diff --git a/src/main/java/com/tacz/guns/network/NetworkHandler.java b/src/main/java/com/tacz/guns/network/NetworkHandler.java index 1f0ce3308..33e211d42 100644 --- a/src/main/java/com/tacz/guns/network/NetworkHandler.java +++ b/src/main/java/com/tacz/guns/network/NetworkHandler.java @@ -39,6 +39,12 @@ public class NetworkHandler { public static void init() { CHANNEL.registerMessage(ID_COUNT.getAndIncrement(), ClientMessagePlayerShoot.class, ClientMessagePlayerShoot::encode, ClientMessagePlayerShoot::decode, ClientMessagePlayerShoot::handle, Optional.of(NetworkDirection.PLAY_TO_SERVER)); + CHANNEL.registerMessage(ID_COUNT.getAndIncrement(), ClientMessagePlayerShootBegin.class, ClientMessagePlayerShootBegin::encode, ClientMessagePlayerShootBegin::decode, ClientMessagePlayerShootBegin::handle, + Optional.of(NetworkDirection.PLAY_TO_SERVER)); + CHANNEL.registerMessage(ID_COUNT.getAndIncrement(), ClientMessagePlayerShootEnd.class, ClientMessagePlayerShootEnd::encode, ClientMessagePlayerShootEnd::decode, ClientMessagePlayerShootEnd::handle, + Optional.of(NetworkDirection.PLAY_TO_SERVER)); + CHANNEL.registerMessage(ID_COUNT.getAndIncrement(), ServerMessageGunStop.class, ServerMessageGunStop::encode, ServerMessageGunStop::decode, ServerMessageGunStop::handle, + Optional.of(NetworkDirection.PLAY_TO_CLIENT)); CHANNEL.registerMessage(ID_COUNT.getAndIncrement(), ClientMessagePlayerReloadGun.class, ClientMessagePlayerReloadGun::encode, ClientMessagePlayerReloadGun::decode, ClientMessagePlayerReloadGun::handle, Optional.of(NetworkDirection.PLAY_TO_SERVER)); CHANNEL.registerMessage(ID_COUNT.getAndIncrement(), ClientMessagePlayerCancelReload.class, ClientMessagePlayerCancelReload::encode, ClientMessagePlayerCancelReload::decode, ClientMessagePlayerCancelReload::handle, diff --git a/src/main/java/com/tacz/guns/network/message/ClientMessagePlayerShootBegin.java b/src/main/java/com/tacz/guns/network/message/ClientMessagePlayerShootBegin.java new file mode 100644 index 000000000..a129c971a --- /dev/null +++ b/src/main/java/com/tacz/guns/network/message/ClientMessagePlayerShootBegin.java @@ -0,0 +1,42 @@ +package com.tacz.guns.network.message; + +import com.tacz.guns.api.entity.IGunOperator; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.network.NetworkEvent; + +import java.util.function.Supplier; + +public class ClientMessagePlayerShootBegin { + + private long timestamp; + + public ClientMessagePlayerShootBegin() { + } + + public ClientMessagePlayerShootBegin(long timestamp) { + this.timestamp = timestamp; + } + + public static void encode(ClientMessagePlayerShootBegin message, FriendlyByteBuf buf) { + buf.writeLong(message.timestamp); + } + + public static ClientMessagePlayerShootBegin decode(FriendlyByteBuf buf) { + return new ClientMessagePlayerShootBegin(buf.readLong()); + } + + public static void handle(ClientMessagePlayerShootBegin message, Supplier contextSupplier) { + NetworkEvent.Context context = contextSupplier.get(); + if (context.getDirection().getReceptionSide().isServer()) { + context.enqueueWork(() -> { + ServerPlayer entity = context.getSender(); + if (entity == null) { + return; + } + IGunOperator.fromLivingEntity(entity).startFullAuto(message.timestamp); + }); + } + context.setPacketHandled(true); + } +} diff --git a/src/main/java/com/tacz/guns/network/message/ClientMessagePlayerShootEnd.java b/src/main/java/com/tacz/guns/network/message/ClientMessagePlayerShootEnd.java new file mode 100644 index 000000000..c275a6d1d --- /dev/null +++ b/src/main/java/com/tacz/guns/network/message/ClientMessagePlayerShootEnd.java @@ -0,0 +1,42 @@ +package com.tacz.guns.network.message; + +import com.tacz.guns.api.entity.IGunOperator; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.network.NetworkEvent; + +import java.util.function.Supplier; + +public class ClientMessagePlayerShootEnd { + + private long timestamp; + + public ClientMessagePlayerShootEnd() { + } + + public ClientMessagePlayerShootEnd(long timestamp) { + this.timestamp = timestamp; + } + + public static void encode(ClientMessagePlayerShootEnd message, FriendlyByteBuf buf) { + buf.writeLong(message.timestamp); + } + + public static ClientMessagePlayerShootEnd decode(FriendlyByteBuf buf) { + return new ClientMessagePlayerShootEnd(buf.readLong()); + } + + public static void handle(ClientMessagePlayerShootEnd message, Supplier contextSupplier) { + NetworkEvent.Context context = contextSupplier.get(); + if (context.getDirection().getReceptionSide().isServer()) { + context.enqueueWork(() -> { + ServerPlayer entity = context.getSender(); + if (entity == null) { + return; + } + IGunOperator.fromLivingEntity(entity).stopFullAuto(); + }); + } + context.setPacketHandled(true); + } +} diff --git a/src/main/java/com/tacz/guns/network/message/ServerMessageGunStop.java b/src/main/java/com/tacz/guns/network/message/ServerMessageGunStop.java new file mode 100644 index 000000000..9433f709b --- /dev/null +++ b/src/main/java/com/tacz/guns/network/message/ServerMessageGunStop.java @@ -0,0 +1,49 @@ +package com.tacz.guns.network.message; + +import com.tacz.guns.api.entity.IGunOperator; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.entity.LivingEntity; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.network.NetworkEvent; + +import java.util.function.Supplier; + +public class ServerMessageGunStop { + + private final int shooterId; + + public ServerMessageGunStop(int shooterId) { + this.shooterId = shooterId; + } + + public static void encode(ServerMessageGunStop message, FriendlyByteBuf buf) { + buf.writeVarInt(message.shooterId); + } + + public static ServerMessageGunStop decode(FriendlyByteBuf buf) { + int shooterId = buf.readVarInt(); + return new ServerMessageGunStop(shooterId); + } + + public static void handle(ServerMessageGunStop message, Supplier contextSupplier) { + NetworkEvent.Context context = contextSupplier.get(); + if (context.getDirection().getReceptionSide().isClient()) { + context.enqueueWork(() -> doClientEvent(message, context)); + } + context.setPacketHandled(true); + } + + @OnlyIn(Dist.CLIENT) + private static void doClientEvent(ServerMessageGunStop message, NetworkEvent.Context context) { + ClientLevel level = Minecraft.getInstance().level; + if (level == null) { + return; + } + if (level.getEntity(message.shooterId) instanceof LivingEntity shooter) { + IGunOperator.fromLivingEntity(shooter).stopFullAuto(); + } + } +} diff --git a/src/main/java/com/tacz/guns/util/ShootBus.java b/src/main/java/com/tacz/guns/util/ShootBus.java new file mode 100644 index 000000000..d58344f6f --- /dev/null +++ b/src/main/java/com/tacz/guns/util/ShootBus.java @@ -0,0 +1,69 @@ +package com.tacz.guns.util; + +import com.tacz.guns.GunMod; +import com.tacz.guns.api.entity.IGunOperator; +import com.tacz.guns.entity.shooter.ShooterDataHolder; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.event.entity.player.PlayerEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +import java.util.Iterator; +import java.util.UUID; + +import static com.tacz.guns.util.InputExtraCheck.isInGame; + +/** + * 增加了一个专门用于处理全自动射击的类 + */ +@Mod.EventBusSubscriber(modid = GunMod.MOD_ID) +public class ShootBus { + private static final Object2IntOpenHashMap SHOT_COUNTER = new Object2IntOpenHashMap<>(); + static { + SHOT_COUNTER.defaultReturnValue(0); + } + private ShootBus() {} + + /** + * 射击一发子弹(只记录) + * @param playerUUID 射击者的UUID + */ + public static void addShot(UUID playerUUID) { + ShootBus.SHOT_COUNTER.addTo(playerUUID, 1); + } + @SubscribeEvent + /** + * 每刻统一处理一次,把积攒的所有子弹作为一个弹射物发射出去 + */ + public static void processAndClear(TickEvent.ServerTickEvent event) { + if (event.phase != TickEvent.Phase.END && !isInGame()) { + return; + } + Iterator> it = ShootBus.SHOT_COUNTER.object2IntEntrySet().iterator(); + while (it.hasNext()) { + Object2IntMap.Entry entry = it.next(); + UUID playerUUID = entry.getKey(); + int bulletCount = entry.getIntValue(); + ServerPlayer player = event.getServer().getPlayerList().getPlayer(playerUUID); + if (player == null) { + it.remove(); + continue; + } + IGunOperator shooter = IGunOperator.fromLivingEntity(player); + ShooterDataHolder data = shooter.getDataHolder(); + // 射击 + shooter.shoot(player::getXRot, player::getYRot, System.currentTimeMillis() - data.baseTimestamp, bulletCount, true); + //从中清除 + it.remove(); + } + } + @SubscribeEvent + public static void onPlayerLogout(PlayerEvent.PlayerLoggedOutEvent event) { + if (event.getEntity() instanceof ServerPlayer serverPlayer) { + SHOT_COUNTER.removeInt(serverPlayer.getUUID()); + } + } +}