diff --git a/server-deploy/overrides/sfcraft/announcements/2.3_update.txt b/server-deploy/overrides/sfcraft/announcements/2.3_update.txt new file mode 100644 index 0000000..8efcddf --- /dev/null +++ b/server-deploy/overrides/sfcraft/announcements/2.3_update.txt @@ -0,0 +1,8 @@ +§d§lSFCraft 已更新到 2.2.1。 +§a主要改动: + - 修复了非被斩首怪物掉落头颅的问题 + - 修复了 4K 图片无法下载到服务器中的问题。现在高分辨率图片会被降采样 + - 修复了鞘翅撞进地狱门需要有空隙的问题 + +具体信息请查阅我们的更新日志: +§bhttps://sfclub.cc/~sfcraft/changelog/2026-08-08/ diff --git a/src/main/java/io/ib67/sfcraft/mixin/gameplay/ElytraRushMixin.java b/src/main/java/io/ib67/sfcraft/mixin/gameplay/ElytraRushMixin.java new file mode 100644 index 0000000..ef9dbe4 --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/mixin/gameplay/ElytraRushMixin.java @@ -0,0 +1,72 @@ +package io.ib67.sfcraft.mixin.gameplay; + +import io.ib67.sfcraft.SFCraft; +import io.ib67.sfcraft.module.game.portal.ElytraPortalModule; +import io.ib67.sfcraft.module.game.portal.ElytraRushHolder; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.phys.Vec3; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * 鞘翅冲门在「传送门后面有遮挡」这一情况下的两处补偿: + *
    + *
  1. {@code travelFallFlying} 头部——此处还在 {@code move()} 之前,deltaMovement 尚未被墙裁剪, + * 把速度存进 {@link ElytraRushHolder} 快照,供下一 tick 的传送门判定使用;
  2. + *
  3. {@code handleFallFlyingCollisions} 头部——原版在 {@code move()} 之后按碰撞前后的速度差扣动能伤害, + * 玩家高速冲进传送门方块又撞上门后墙体的这一下不该算他的,直接取消。
  4. + *
+ * 判定与配置读取都在 {@link ElytraPortalModule}。 + */ +@Mixin(LivingEntity.class) +public abstract class ElytraRushMixin implements ElytraRushHolder { + @Unique + private Vec3 sfcraft$rushDelta; + + @Unique + private int sfcraft$rushTick; + + @Inject(method = "travelFallFlying", at = @At("HEAD")) + private void sfcraft$recordRushSpeed(Vec3 input, CallbackInfo ci) { + if ($this() instanceof ServerPlayer player) { + sfcraft$module().mixin$recordRushSpeed(player); + } + } + + @Inject(method = "handleFallFlyingCollisions", at = @At("HEAD"), cancellable = true) + private void sfcraft$skipPortalWallImpact(double moveHorLength, double newMoveHorLength, CallbackInfo ci) { + if ($this() instanceof ServerPlayer player && sfcraft$module().shouldIgnoreWallImpact(player, moveHorLength)) { + ci.cancel(); + } + } + + @Override + public void sfcraft$recordElytraRush(Vec3 delta, int tick) { + sfcraft$rushDelta = delta; + sfcraft$rushTick = tick; + } + + @Override + public Vec3 sfcraft$elytraRushDelta() { + return sfcraft$rushDelta; + } + + @Override + public int sfcraft$elytraRushTick() { + return sfcraft$rushTick; + } + + @Unique + private ElytraPortalModule sfcraft$module() { + return SFCraft.getInjector().getInstance(ElytraPortalModule.class); + } + + @Unique + private LivingEntity $this() { + return (LivingEntity) (Object) this; + } +} diff --git a/src/main/java/io/ib67/sfcraft/mixin/gameplay/NetherPortalBlockMixin.java b/src/main/java/io/ib67/sfcraft/mixin/gameplay/NetherPortalBlockMixin.java index 91435f6..9dadf5f 100644 --- a/src/main/java/io/ib67/sfcraft/mixin/gameplay/NetherPortalBlockMixin.java +++ b/src/main/java/io/ib67/sfcraft/mixin/gameplay/NetherPortalBlockMixin.java @@ -14,8 +14,8 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; /** * 鞘翅冲出下界传送门时跳过等待:本方法每 tick 被 {@code PortalProcessor} 调用来决定 - * 「还要在门里待多久」,命中条件时覆写为 1 tick(与原版创造模式一致),下一 tick 即跨维度; - * 否则不动 cir。判定逻辑见 {@link ElytraPortalModule#shouldTeleportNow}。 + * 「还要在门里待多久」,命中条件时覆写成 0,本次 {@code handlePortal} 就跨维度;否则不动 cir。 + * 判定逻辑见 {@link ElytraPortalModule#shouldTeleportNow}。 */ @Mixin(NetherPortalBlock.class) public abstract class NetherPortalBlockMixin { @@ -27,6 +27,8 @@ public abstract class NetherPortalBlockMixin { } var module = SFCraft.getInjector().getInstance(ElytraPortalModule.class); if (module.shouldTeleportNow(entity)) { + // 门后有遮挡时玩家早被墙撞停了,趁传送前把速度补回去,出门那侧才能继续滑翔 + module.restoreRushVelocity(entity); cir.setReturnValue(module.getInstantTransitionTicks()); } } diff --git a/src/main/java/io/ib67/sfcraft/module/game/portal/ElytraPortalModule.java b/src/main/java/io/ib67/sfcraft/module/game/portal/ElytraPortalModule.java index 81662e7..d9935b7 100644 --- a/src/main/java/io/ib67/sfcraft/module/game/portal/ElytraPortalModule.java +++ b/src/main/java/io/ib67/sfcraft/module/game/portal/ElytraPortalModule.java @@ -3,21 +3,35 @@ package io.ib67.sfcraft.module.game.portal; import com.google.inject.Inject; import io.ib67.sfcraft.ServerModule; import io.ib67.sfcraft.config.GameConfigService; +import net.minecraft.core.BlockPos; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; /** * 鞘翅冲门:玩家使用鞘翅滑翔、以不低于配置阈值(米/秒)的速度冲进下界传送门时, - * 跳过原本的传送门等待(生存默认 80 tick),下一 tick 直接跨维度。 + * 跳过原本的传送门等待(生存默认 80 tick),当场跨维度。 *

- * 本模块不注册任何注册表内容;行为由 {@code NetherPortalBlockMixin} 在 - * {@code NetherPortalBlock#getPortalTransitionTime} 头部委托 {@link #shouldTeleportNow} - * 实现:命中条件时把等待时间覆写为 1 tick,否则放行原版(游戏规则)逻辑。 + * 本模块不注册任何注册表内容;行为由两个 mixin 委托驱动: + *

* 配置经 {@link GameConfigService} 每次现读,/sfcraft reload 即时生效。 */ public class ElytraPortalModule extends ServerModule { - /** 冲门判定时覆写的等待 tick 数:与原版的创造模式进一致。 */ - private static final int INSTANT_TRANSITION_TICKS = 1; + /** 冲门判定时覆写的等待 tick 数:0 = 本次 handlePortal 就传送,不再多贴一 tick 在墙上。 */ + private static final int INSTANT_TRANSITION_TICKS = 0; + + /** + * 撞墙前速度快照的有效期(tick)。进门发生在第 N tick 的 aiStep 末尾(applyEffectsFromBlocks), + * 传送门判定在第 N+1 tick 开头的 handlePortal,理论上只差 1 tick,留点余量应付被墙磨蹭几 tick 的情况。 + */ + private static final int RUSH_MEMORY_TICKS = 3; @Inject private GameConfigService config; @@ -27,21 +41,109 @@ public class ElytraPortalModule extends ServerModule { // 无注册表内容、无事件订阅:特性完全由 mixin 委托驱动 } + /** + * {@code LivingEntity#travelFallFlying} 头部调用:此刻还在 {@code move()} 之前、deltaMovement 尚未被 + * 墙裁剪,速度够快就记下来。只记够快的——这样玩家撞墙后那些近零速的 tick 不会把快照冲掉。 + */ + public void mixin$recordRushSpeed(ServerPlayer player) { + double threshold = threshold(); + if (threshold <= 0) { + return; + } + Vec3 delta = player.getDeltaMovement(); + if (metersPerSecond(delta) < threshold) { + return; + } + ((ElytraRushHolder) player).sfcraft$recordElytraRush(delta, player.tickCount); + } + /** mixin 在下界传送门询问等待时间时调用;命中即把本次等待覆写为 {@link #INSTANT_TRANSITION_TICKS}。 */ public boolean shouldTeleportNow(Entity entity) { - if (!(entity instanceof ServerPlayer player) || !player.isFallFlying()) { + if (!(entity instanceof ServerPlayer player)) { return false; } - double threshold = config.get().elytraPortal.minSpeedBlocksPerSecond; + double threshold = threshold(); // 速度阈值 <= 0 视为关闭本特性 if (threshold <= 0) { return false; } - // deltaMovement 单位为 方块/tick,乘 20 换算成米/秒与配置同单位 - return player.getDeltaMovement().length() * 20.0 >= threshold; + // 门后没遮挡的常规情况:当前速度就够快 + if (player.isFallFlying() && metersPerSecond(player.getDeltaMovement()) >= threshold) { + return true; + } + // 门后有遮挡:进门那一 tick 已经被墙撞停(连 isFallFlying 都可能因随后落地而取消),只能看快照 + return recentRushDelta(player) != null; + } + + /** + * {@code LivingEntity#handleFallFlyingCollisions} 头部调用:玩家高速冲进传送门方块、又撞上门后墙体时, + * 这一下动能伤害不该算在他头上——他本来就是要穿过去的。 + * + * @param preCollisionSpeedPerTick 原版传进来的碰撞前水平速度,单位 方块/tick + */ + public boolean shouldIgnoreWallImpact(ServerPlayer player, double preCollisionSpeedPerTick) { + double threshold = threshold(); + return threshold > 0 + && metersPerSecond(preCollisionSpeedPerTick) >= threshold + && isTouchingNetherPortal(player); + } + + /** + * 传送前把被墙撞掉的速度补回来。原版的 {@code TeleportTransition} 带 {@code Relative.DELTA} + * (含 {@code ROTATE_DELTA}),会保留并按出口朝向旋转玩家速度,所以这里只要把快照写回去, + * 出门那一侧就能继续原速滑翔,而不是零速掉下去。 + */ + public void restoreRushVelocity(Entity entity) { + if (!(entity instanceof ServerPlayer player)) { + return; + } + double threshold = threshold(); + // 只在速度确实掉到阈值以下(被墙吃了)时才补;门后没遮挡的正常冲门不去动它 + if (threshold <= 0 || metersPerSecond(player.getDeltaMovement()) >= threshold) { + return; + } + Vec3 recorded = recentRushDelta(player); + if (recorded == null) { + return; + } + player.setDeltaMovement(recorded); + // 玩家移动是客户端权威的,而客户端同样预测到了这次撞墙。不补一个速度包的话, + // 服务端这边写回去的速度会立刻被客户端的位置汇报盖掉。 + player.hurtMarked = true; } public int getInstantTransitionTicks() { return INSTANT_TRANSITION_TICKS; } + + /** 仍在有效期内的撞墙前速度快照;过期或从未记录过返回 null。 */ + private @Nullable Vec3 recentRushDelta(ServerPlayer player) { + var holder = (ElytraRushHolder) player; + Vec3 delta = holder.sfcraft$elytraRushDelta(); + if (delta == null || player.tickCount - holder.sfcraft$elytraRushTick() > RUSH_MEMORY_TICKS) { + return null; + } + return delta; + } + + /** 玩家碰撞箱是否正压在传送门方块上——撞墙那一 tick 传送门还没被登记,只能直接看方块。 */ + private boolean isTouchingNetherPortal(ServerPlayer player) { + var level = player.level(); + // 稍微收一点碰撞箱,避免只是紧贴着门的隔壁方块也被算进来 + return BlockPos.betweenClosedStream(player.getBoundingBox().deflate(1.0E-6)) + .anyMatch(pos -> level.getBlockState(pos).is(Blocks.NETHER_PORTAL)); + } + + private double threshold() { + return config.get().elytraPortal.minSpeedBlocksPerSecond; + } + + /** deltaMovement 单位为 方块/tick,乘 20 换算成米/秒与配置同单位。 */ + private static double metersPerSecond(double blocksPerTick) { + return blocksPerTick * 20.0; + } + + private static double metersPerSecond(Vec3 delta) { + return metersPerSecond(delta.length()); + } } diff --git a/src/main/java/io/ib67/sfcraft/module/game/portal/ElytraRushHolder.java b/src/main/java/io/ib67/sfcraft/module/game/portal/ElytraRushHolder.java new file mode 100644 index 0000000..c472c0f --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/module/game/portal/ElytraRushHolder.java @@ -0,0 +1,23 @@ +package io.ib67.sfcraft.module.game.portal; + +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; + +/** + * 由 {@code ElytraRushMixin} 混入 LivingEntity:记住玩家本 tick 移动「被碰撞裁剪之前」的滑翔速度。 + *

+ * 传送门后面有遮挡时,玩家进门那一 tick 就被墙撞停(deltaMovement 归零),而传送门是在下一 tick + * 开头的 {@code Entity#handlePortal} 里才问「还要等多久」,那时候速度早就没了、判不出冲门。 + * 这份快照就是给那一刻补判用的,见 {@link ElytraPortalModule#shouldTeleportNow}。 + */ +public interface ElytraRushHolder { + /** 记下够快的滑翔速度及其发生的 tickCount;低速 tick 不会来覆盖,快照只靠 tick 窗口过期。 */ + void sfcraft$recordElytraRush(Vec3 delta, int tick); + + /** 最近一次记录的速度;从未记录过则为 null。 */ + @Nullable + Vec3 sfcraft$elytraRushDelta(); + + /** 上一次记录发生在哪个 {@code Entity#tickCount}。 */ + int sfcraft$elytraRushTick(); +} diff --git a/src/main/resources/sfcraft.mixins.json b/src/main/resources/sfcraft.mixins.json index 2d0356b..6a70365 100644 --- a/src/main/resources/sfcraft.mixins.json +++ b/src/main/resources/sfcraft.mixins.json @@ -19,6 +19,7 @@ "common.fixes.EndermanEntityMixin", "common.fixes.LivingEntityMixin", "gameplay.AnvilMenuMixin", + "gameplay.ElytraRushMixin", "gameplay.ItemFrameMixin", "gameplay.MobCommanderMixin", "gameplay.NetherPortalBlockMixin",