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..ace255ddb 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); /** * 开始换弹时调用 @@ -227,21 +227,25 @@ public void dropAllAmmo(Player player, ItemStack gunItem) { public int findAndExtractInventoryAmmos(IItemHandler itemHandler, ItemStack gunItem, int needAmmoCount) { return findAndExtractInventoryAmmo(itemHandler, gunItem, needAmmoCount); } - + public int findAndExtractInventoryAmmo(IItemHandler itemHandler, ItemStack gunItem, int needAmmoCount) { + return findAndExtractInventoryAmmo(itemHandler, gunItem, needAmmoCount, false); + } /** * 枪械寻弹和扣除背包弹药逻辑 * @param itemHandler 目标实体的背包 * @param gunItem 枪械物品 * @param needAmmoCount 需要的弹药 (物品) 数量 + * @param simulate 如果为 {@code true},则仅测试是否可以消耗子弹,不实际修改数量; + * 如果为 {@code false},则真实消耗子弹。 * @return 寻找到的弹药 (物品) 数量 */ - public int findAndExtractInventoryAmmo(IItemHandler itemHandler, ItemStack gunItem, int needAmmoCount) { + public int findAndExtractInventoryAmmo(IItemHandler itemHandler, ItemStack gunItem, int needAmmoCount, boolean simulate) { int cnt = needAmmoCount; // 背包检查 for (int i = 0; i < itemHandler.getSlots(); i++) { ItemStack checkAmmoStack = itemHandler.getStackInSlot(i); if (checkAmmoStack.getItem() instanceof IAmmo iAmmo && iAmmo.isAmmoOfGun(gunItem, checkAmmoStack)) { - ItemStack extractItem = itemHandler.extractItem(i, cnt, false); + ItemStack extractItem = itemHandler.extractItem(i, cnt, simulate); cnt = cnt - extractItem.getCount(); if (cnt <= 0) { break; @@ -251,9 +255,11 @@ public int findAndExtractInventoryAmmo(IItemHandler itemHandler, ItemStack gunIt int boxAmmoCount = iAmmoBox.getAmmoCount(checkAmmoStack); int extractCount = Math.min(boxAmmoCount, cnt); int remainCount = boxAmmoCount - extractCount; - iAmmoBox.setAmmoCount(checkAmmoStack, remainCount); - if (remainCount <= 0) { - iAmmoBox.setAmmoId(checkAmmoStack, DefaultAssets.EMPTY_AMMO_ID); + if (!simulate) { + iAmmoBox.setAmmoCount(checkAmmoStack, remainCount); + if (remainCount <= 0) { + iAmmoBox.setAmmoId(checkAmmoStack, DefaultAssets.EMPTY_AMMO_ID); + } } cnt = cnt - extractCount; if (cnt <= 0) { 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..389f21188 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 { + } else if(pushDown) { + 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..99686cc64 100644 --- a/src/main/java/com/tacz/guns/entity/shooter/LivingEntityShoot.java +++ b/src/main/java/com/tacz/guns/entity/shooter/LivingEntityShoot.java @@ -8,15 +8,19 @@ import com.tacz.guns.api.item.gun.AbstractGunItem; import com.tacz.guns.api.item.gun.FireMode; import com.tacz.guns.config.sync.SyncConfig; +import com.tacz.guns.item.ModernKineticGunScriptAPI; 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,30 @@ 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.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 +77,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 +152,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) { + ModernKineticGunScriptAPI api = new ModernKineticGunScriptAPI(); + api.setItemStack(currentGunItem); + api.setShooter(shooter); + api.setDataHolder(data); + api.setPitchSupplier(pitch); + api.setYawSupplier(yaw); + if(!api.reduceAmmoOnce(true)) + 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()); + ShootBus.beginShot(playerUuid, rpm); + return true; + } + return false; + } + public void stopFullAuto() { + UUID playerUuid = this.shooter.getUUID(); + ShootBus.endShot(playerUuid); + } /** * 以当前时间戳查询射击冷却。返回值一般不会超过枪械的射击间隔 * @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..34119faf8 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,8 @@ public LuaValue getCachedProperty(String id) { * 执行一次完整的射击逻辑,会考虑玩家的状态(是否在瞄准、是否在移动、是否在匍匐等)、配件数值影响、多弹丸散射、连发,播放开火音效、 * @param consumeAmmo 本次射击是否消耗弹药 */ - public void shootOnce(boolean consumeAmmo){ + public void shootOnce(boolean consumeAmmo, int count) { + GunMod.LOGGER.info("{}", count); GunData gunData = gunIndex.getGunData(); BulletData bulletData = gunIndex.getBulletData(); IGunOperator gunOperator = IGunOperator.fromLivingEntity(shooter); @@ -143,6 +149,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 +164,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 = 0; + break; + } + i-=1; + } + if(bulletCount <= 0) { return false; } } @@ -179,7 +194,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); @@ -217,13 +232,22 @@ public void handleShootHeat() { abstractGunItem.setOverheatLocked(itemStack, true); } } - /** * 让枪械内的子弹减少一发。会遵从栓动、闭膛待击和开膛待机的规律,消耗枪管内子弹或者弹匣内子弹。 - * 如果没有可以消耗的子弹,这个方法会返回 false。例如栓动步枪,虽然弹匣内有子弹,但是在 bolt 之前枪管内没有子弹,那么就会返回 false, + * * @return 是否成功减少子弹。 */ public boolean reduceAmmoOnce() { + return reduceAmmoOnce(false); + } + /** + * 让枪械内的子弹减少一发。会遵从栓动、闭膛待击和开膛待机的规律,消耗枪管内子弹或者弹匣内子弹。 + * + * @param simulate 如果为 {@code true},则仅测试是否可以消耗子弹,不实际修改数量; + * 如果为 {@code false},则真实消耗子弹。 + * @return 是否成功减少子弹(或是否可以减少)。 + */ + public boolean reduceAmmoOnce(boolean simulate) { Bolt boltType = TimelessAPI.getCommonGunIndex(abstractGunItem.getGunId(itemStack)) .map(index -> index.getGunData().getBolt()) .orElse(null); @@ -253,10 +277,12 @@ public boolean reduceAmmoOnce() { if (!noAmmo) { // 如果背包直读则背包内射击后弹药 - 1 if (useInventoryAmmo()) { - return consumeAmmoFromPlayer(1) == 1; + return consumeAmmoFromPlayer(1, simulate) == 1; } // 如果非背包直读则弹匣内子弹 - 1 - abstractGunItem.reduceCurrentAmmoCount(itemStack); + if(!simulate) { + abstractGunItem.reduceCurrentAmmoCount(itemStack); + } return true; } // 没有膛内子弹无法射击 @@ -264,7 +290,9 @@ public boolean reduceAmmoOnce() { return false; } // 没有弹匣内的子弹则消耗枪膛内的子弹 - abstractGunItem.setBulletInBarrel(itemStack, false); + if(!simulate) { + abstractGunItem.setBulletInBarrel(itemStack, false); + } return true; } // 开膛逻辑 @@ -275,16 +303,19 @@ public boolean reduceAmmoOnce() { } // 如果背包直读则背包内射击后弹药 - 1 if (useInventoryAmmo()) { - return consumeAmmoFromPlayer(1) == 1; + return consumeAmmoFromPlayer(1, simulate) == 1; } // 如果非背包直读则弹匣内子弹 - 1 - abstractGunItem.reduceCurrentAmmoCount(itemStack); + if(!simulate) { + abstractGunItem.reduceCurrentAmmoCount(itemStack); + } return true; } // 非三种已知 Bolt 类型 (目前不会出现),默认返回 false return false; } + /** * 获取从开始换弹到现在经历的时间,单位为 ms * @@ -448,7 +479,6 @@ public int getMaxAmmoCount() { public int getMagExtentLevel() { return AttachmentDataUtils.getMagExtendLevel(itemStack, gunIndex.getGunData()); } - /** * 尽可能多地从玩家身上 (或者虚拟备弹) 消耗掉弹药,返回消耗的数量 * @@ -456,6 +486,17 @@ public int getMagExtentLevel() { * @return 实际消耗的弹药数量 */ public int consumeAmmoFromPlayer(int neededAmount) { + return consumeAmmoFromPlayer(neededAmount, false); + } + /** + * 尽可能多地从玩家身上 (或者虚拟备弹) 消耗掉弹药,返回消耗的数量 + * + * @param neededAmount 需要的弹药数量 + * @param simulate 如果为 {@code true},则仅测试是否可以消耗子弹,不实际修改数量; + * 如果为 {@code false},则真实消耗子弹。 + * @return 实际消耗的弹药数量 + */ + public int consumeAmmoFromPlayer(int neededAmount, boolean simulate) { // 如果处于背包直读并且创造模式不消耗的情况 if (useInventoryAmmo() && !isReloadingNeedConsumeAmmo()) { return neededAmount; @@ -464,7 +505,7 @@ public int consumeAmmoFromPlayer(int neededAmount) { return abstractGunItem.findAndExtractDummyAmmo(itemStack, neededAmount); } else { return shooter.getCapability(ForgeCapabilities.ITEM_HANDLER, null) - .map(cap -> abstractGunItem.findAndExtractInventoryAmmo(cap, itemStack, neededAmount)) + .map(cap -> abstractGunItem.findAndExtractInventoryAmmo(cap, itemStack, 1, true)) .orElse(0); } } 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..51980245c --- /dev/null +++ b/src/main/java/com/tacz/guns/util/ShootBus.java @@ -0,0 +1,88 @@ +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.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.tacz.guns.util.InputExtraCheck.isInGame; + +/** + * 增加了一个专门用于处理全自动射击的类 + */ +@Mod.EventBusSubscriber(modid = GunMod.MOD_ID) +public class ShootBus { + private static class ShootingSession { + final long startNano; // 开始射击时的 System.nanoTime() + final double bulletsPerNano; // 每秒射速转换为每纳秒子弹数 (RPM / 60 / 1e9) + int lastTotalBullets; // 上一次发射时计算的总子弹数(整数部分) + + ShootingSession(int rpm) { + this.startNano = System.nanoTime(); + this.bulletsPerNano = rpm / 60.0 / 1_000_000_000.0; + this.lastTotalBullets = 0; + } + } + private static final ConcurrentHashMap ACTIVE_SESSIONS = new ConcurrentHashMap<>(); + static { + } + private ShootBus() {} + + public static void beginShot(UUID playerUUID, int rpm) { + ACTIVE_SESSIONS.put(playerUUID, new ShootingSession(rpm)); + } + + public static void endShot(UUID playerUUID) { + ACTIVE_SESSIONS.remove(playerUUID); + } + @SubscribeEvent + /** + * 每刻统一处理一次,把积攒的所有子弹作为一个弹射物发射出去 + */ + public static void processAndClear(TickEvent.ServerTickEvent event) { + if (event.phase != TickEvent.Phase.END && !isInGame()) { + return; + } + long nowNano = System.nanoTime(); + Iterator> it = ShootBus.ACTIVE_SESSIONS.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + UUID playerUUID = entry.getKey(); + ShootingSession session= entry.getValue(); + ServerPlayer player = event.getServer().getPlayerList().getPlayer(playerUUID); + if (player == null) { + it.remove(); + continue; + } + //计算射出多少发和最后一次射击的精确时间戳 + + int bulletCount = (int) ((nowNano - session.startNano) * session.bulletsPerNano + 1) - session.lastTotalBullets; //向下取整 + session.lastTotalBullets += bulletCount; + //GunMod.LOGGER.info("{} {} {} {}", System.nanoTime(), bulletCount, session.startNano, session.lastTotalBullets); + if(bulletCount == 0) + return; + // 射击 + IGunOperator shooter = IGunOperator.fromLivingEntity(player); + ShooterDataHolder data = shooter.getDataHolder(); + shooter.shoot(player::getXRot, player::getYRot, System.currentTimeMillis() - data.baseTimestamp, bulletCount, true); + } + } + @SubscribeEvent + public static void onPlayerLogout(PlayerEvent.PlayerLoggedOutEvent event) { + if (event.getEntity() instanceof ServerPlayer serverPlayer) { + ACTIVE_SESSIONS.remove(serverPlayer.getUUID()); + } + } +}