fix elytra rush collison
This commit is contained in:
6 files changed
+220
-12
No files matched your search
@@ -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} 调用来决定
|
||||
* 「还要在门里待多久」,命中条件时覆写为 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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),当场跨维度。
|
||||
* <p>
|
||||
* 本模块不注册任何注册表内容;行为由 {@code NetherPortalBlockMixin} 在
|
||||
* {@code NetherPortalBlock#getPortalTransitionTime} 头部委托 {@link #shouldTeleportNow}
|
||||
* 实现:命中条件时把等待时间覆写为 1 tick,否则放行原版(游戏规则)逻辑。
|
||||
* 本模块不注册任何注册表内容;行为由两个 mixin 委托驱动:
|
||||
* <ul>
|
||||
* <li>{@code NetherPortalBlockMixin} 在 {@code NetherPortalBlock#getPortalTransitionTime} 头部委托
|
||||
* {@link #shouldTeleportNow},命中条件时把等待时间覆写掉,否则放行原版(游戏规则)逻辑;</li>
|
||||
* <li>{@code ElytraRushMixin} 负责记录撞墙前的速度({@link #mixin$recordRushSpeed})、免掉门后撞墙的
|
||||
* 动能伤害({@link #shouldIgnoreWallImpact})。</li>
|
||||
* </ul>
|
||||
* 配置经 {@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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
"common.fixes.EndermanEntityMixin",
|
||||
"common.fixes.LivingEntityMixin",
|
||||
"gameplay.AnvilMenuMixin",
|
||||
"gameplay.ElytraRushMixin",
|
||||
"gameplay.ItemFrameMixin",
|
||||
"gameplay.MobCommanderMixin",
|
||||
"gameplay.NetherPortalBlockMixin",
|
||||
|
||||
Reference in new issue
Block a user