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;
+
+/**
+ * 鞘翅冲门在「传送门后面有遮挡」这一情况下的两处补偿:
+ *
+ * - {@code travelFallFlying} 头部——此处还在 {@code move()} 之前,deltaMovement 尚未被墙裁剪,
+ * 把速度存进 {@link ElytraRushHolder} 快照,供下一 tick 的传送门判定使用;
+ * - {@code handleFallFlyingCollisions} 头部——原版在 {@code move()} 之后按碰撞前后的速度差扣动能伤害,
+ * 玩家高速冲进传送门方块又撞上门后墙体的这一下不该算他的,直接取消。
+ *
+ * 判定与配置读取都在 {@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 委托驱动:
+ *
+ * - {@code NetherPortalBlockMixin} 在 {@code NetherPortalBlock#getPortalTransitionTime} 头部委托
+ * {@link #shouldTeleportNow},命中条件时把等待时间覆写掉,否则放行原版(游戏规则)逻辑;
+ * - {@code ElytraRushMixin} 负责记录撞墙前的速度({@link #mixin$recordRushSpeed})、免掉门后撞墙的
+ * 动能伤害({@link #shouldIgnoreWallImpact})。
+ *
* 配置经 {@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",