fix elytra rush collison

This commit is contained in:
iceBear67 committed 2026-08-24 20:12:59 +08:00
1 parent 63250d48ae
commit bf38f8b2b2
6 files changed
+220 -12

No files matched your search

@@ -0,0 +1,8 @@
§d§lSFCraft 已更新到 2.2.1。
§a主要改动:
- 修复了非被斩首怪物掉落头颅的问题
- 修复了 4K 图片无法下载到服务器中的问题。现在高分辨率图片会被降采样
- 修复了鞘翅撞进地狱门需要有空隙的问题
具体信息请查阅我们的更新日志:
§bhttps://sfclub.cc/~sfcraft/changelog/2026-08-08/
@@ -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;
/**
* 鞘翅冲门在「传送门后面有遮挡」这一情况下的两处补偿:
* <ol>
* <li>{@code travelFallFlying} 头部——此处还在 {@code move()} 之前,deltaMovement 尚未被墙裁剪,
* 把速度存进 {@link ElytraRushHolder} 快照,供下一 tick 的传送门判定使用;</li>
* <li>{@code handleFallFlyingCollisions} 头部——原版在 {@code move()} 之后按碰撞前后的速度差扣动能伤害,
* 玩家高速冲进传送门方块又撞上门后墙体的这一下不该算他的,直接取消。</li>
* </ol>
* 判定与配置读取都在 {@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;
}
}
@@ -14,8 +14,8 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
/** /**
* 鞘翅冲出下界传送门时跳过等待:本方法每 tick 被 {@code PortalProcessor} 调用来决定 * 鞘翅冲出下界传送门时跳过等待:本方法每 tick 被 {@code PortalProcessor} 调用来决定
* 「还要在门里待多久」,命中条件时覆写为 1 tick(与原版创造模式一致),下一 tick 即跨维度; * 「还要在门里待多久」,命中条件时覆写成 0,本次 {@code handlePortal} 就跨维度;否则不动 cir。
* 否则不动 cir。判定逻辑见 {@link ElytraPortalModule#shouldTeleportNow}。 * 判定逻辑见 {@link ElytraPortalModule#shouldTeleportNow}。
*/ */
@Mixin(NetherPortalBlock.class) @Mixin(NetherPortalBlock.class)
public abstract class NetherPortalBlockMixin { public abstract class NetherPortalBlockMixin {
@@ -27,6 +27,8 @@ public abstract class NetherPortalBlockMixin {
} }
var module = SFCraft.getInjector().getInstance(ElytraPortalModule.class); var module = SFCraft.getInjector().getInstance(ElytraPortalModule.class);
if (module.shouldTeleportNow(entity)) { if (module.shouldTeleportNow(entity)) {
// 门后有遮挡时玩家早被墙撞停了,趁传送前把速度补回去,出门那侧才能继续滑翔
module.restoreRushVelocity(entity);
cir.setReturnValue(module.getInstantTransitionTicks()); cir.setReturnValue(module.getInstantTransitionTicks());
} }
} }
@@ -3,21 +3,35 @@ package io.ib67.sfcraft.module.game.portal;
import com.google.inject.Inject; import com.google.inject.Inject;
import io.ib67.sfcraft.ServerModule; import io.ib67.sfcraft.ServerModule;
import io.ib67.sfcraft.config.GameConfigService; import io.ib67.sfcraft.config.GameConfigService;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity; 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),当场跨维度。
* <p> * <p>
* 本模块不注册任何注册表内容;行为由 {@code NetherPortalBlockMixin} 在 * 本模块不注册任何注册表内容;行为由两个 mixin 委托驱动:
* {@code NetherPortalBlock#getPortalTransitionTime} 头部委托 {@link #shouldTeleportNow} * <ul>
* 实现:命中条件时把等待时间覆写为 1 tick,否则放行原版(游戏规则)逻辑。 * <li>{@code NetherPortalBlockMixin} 在 {@code NetherPortalBlock#getPortalTransitionTime} 头部委托
* {@link #shouldTeleportNow},命中条件时把等待时间覆写掉,否则放行原版(游戏规则)逻辑;</li>
* <li>{@code ElytraRushMixin} 负责记录撞墙前的速度({@link #mixin$recordRushSpeed})、免掉门后撞墙的
* 动能伤害({@link #shouldIgnoreWallImpact})。</li>
* </ul>
* 配置经 {@link GameConfigService} 每次现读,/sfcraft reload 即时生效。 * 配置经 {@link GameConfigService} 每次现读,/sfcraft reload 即时生效。
*/ */
public class ElytraPortalModule extends ServerModule { public class ElytraPortalModule extends ServerModule {
/** 冲门判定时覆写的等待 tick 数:与原版的创造模式进一致。 */ /** 冲门判定时覆写的等待 tick 数:0 = 本次 handlePortal 就传送,不再多贴一 tick 在墙上。 */
private static final int INSTANT_TRANSITION_TICKS = 1; 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 @Inject
private GameConfigService config; private GameConfigService config;
@@ -27,21 +41,109 @@ public class ElytraPortalModule extends ServerModule {
// 无注册表内容、无事件订阅:特性完全由 mixin 委托驱动 // 无注册表内容、无事件订阅:特性完全由 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}。 */ /** mixin 在下界传送门询问等待时间时调用;命中即把本次等待覆写为 {@link #INSTANT_TRANSITION_TICKS}。 */
public boolean shouldTeleportNow(Entity entity) { public boolean shouldTeleportNow(Entity entity) {
if (!(entity instanceof ServerPlayer player) || !player.isFallFlying()) { if (!(entity instanceof ServerPlayer player)) {
return false; return false;
} }
double threshold = config.get().elytraPortal.minSpeedBlocksPerSecond; double threshold = threshold();
// 速度阈值 <= 0 视为关闭本特性 // 速度阈值 <= 0 视为关闭本特性
if (threshold <= 0) { if (threshold <= 0) {
return false; 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() { public int getInstantTransitionTicks() {
return INSTANT_TRANSITION_TICKS; 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());
}
} }
@@ -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 移动「被碰撞裁剪之前」的滑翔速度。
* <p>
* 传送门后面有遮挡时,玩家进门那一 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();
}
+1
View File
@@ -19,6 +19,7 @@
"common.fixes.EndermanEntityMixin", "common.fixes.EndermanEntityMixin",
"common.fixes.LivingEntityMixin", "common.fixes.LivingEntityMixin",
"gameplay.AnvilMenuMixin", "gameplay.AnvilMenuMixin",
"gameplay.ElytraRushMixin",
"gameplay.ItemFrameMixin", "gameplay.ItemFrameMixin",
"gameplay.MobCommanderMixin", "gameplay.MobCommanderMixin",
"gameplay.NetherPortalBlockMixin", "gameplay.NetherPortalBlockMixin",