Files
TiedUp-/src/main/java/com/tiedup/remake/mixin/MixinServerPlayer.java
NotEvil f6466360b6 Clean repo for open source release
Remove build artifacts, dev tool configs, unused dependencies,
and third-party source dumps. Add proper README, update .gitignore,
clean up Makefile.
2026-04-12 00:51:22 +02:00

474 lines
16 KiB
Java

package com.tiedup.remake.mixin;
import com.tiedup.remake.entities.LeashProxyEntity;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.sync.PacketSyncLeashProxy;
import com.tiedup.remake.state.IPlayerLeashAccess;
import net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.decoration.LeashFenceKnotEntity;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
/**
* Mixin for ServerPlayer to add leash proxy functionality.
*
* This replaces the old EntityInvisibleSlaveTransporter mount-based system
* with a proxy-based system where:
* - The player does NOT ride an entity
* - A LeashProxyEntity follows the player and holds the leash
* - Traction is applied via push() when the player is too far from the holder
*/
@Mixin(ServerPlayer.class)
public abstract class MixinServerPlayer implements IPlayerLeashAccess {
@Unique
private final ServerPlayer tiedup$self = (ServerPlayer) (Object) this;
/** The proxy entity that follows this player and renders the leash */
@Unique
private LeashProxyEntity tiedup$leashProxy;
/** The entity holding this player's leash (master or fence knot) */
@Unique
private Entity tiedup$leashHolder;
/** Tick counter since last leash attachment (prevents immediate detach) */
@Unique
private int tiedup$leashAge;
/** Previous X position for stuck detection */
@Unique
private double tiedup$prevX;
/** Previous Z position for stuck detection */
@Unique
private double tiedup$prevZ;
/** Ticks spent stuck (not moving towards holder) */
@Unique
private int tiedup$leashStuckCounter;
/** Extra slack on leash - increases pull/max distances (for "pet leads" dogwalk) */
@Unique
private double tiedup$leashSlack = 0.0;
/** Tick counter for periodic leash proxy resync (for late-joining clients) */
@Unique
private int tiedup$leashResyncTimer = 0;
// ==================== Leash Constants ====================
/** Distance at which pull force starts (4 free blocks before any pull) */
@Unique
private static final double LEASH_PULL_START_DISTANCE = 4.0;
/** Maximum distance before instant teleport (6-block elastic zone) */
@Unique
private static final double LEASH_MAX_DISTANCE = 10.0;
/** Distance at which stuck detection activates (middle of elastic zone) */
@Unique
private static final double LEASH_TELEPORT_DISTANCE = 7.0;
/** Ticks of being stuck before safety teleport (2 seconds) */
@Unique
private static final int LEASH_STUCK_THRESHOLD = 40;
/** Maximum pull force cap */
@Unique
private static final double LEASH_MAX_FORCE = 0.14;
/** Force ramp per block beyond pull start */
@Unique
private static final double LEASH_FORCE_RAMP = 0.04;
/** Blend factor for pull vs momentum (0.6 = 60% pull, 40% momentum) */
@Unique
private static final double LEASH_BLEND_FACTOR = 0.6;
// ==================== IPlayerLeashAccess Implementation ====================
@Override
public void tiedup$attachLeash(Entity holder) {
if (holder == null) return;
tiedup$leashHolder = holder;
// Create proxy if not exists
if (tiedup$leashProxy == null) {
tiedup$leashProxy = new LeashProxyEntity(tiedup$self);
tiedup$leashProxy.setPos(
tiedup$self.getX(),
tiedup$self.getY(),
tiedup$self.getZ()
);
tiedup$self.level().addFreshEntity(tiedup$leashProxy);
}
// Attach leash from proxy to holder
tiedup$leashProxy.setLeashedTo(tiedup$leashHolder, true);
tiedup$leashAge = tiedup$self.tickCount;
tiedup$leashStuckCounter = 0;
tiedup$leashResyncTimer = 0;
// Send sync packet to all tracking clients for smooth rendering
PacketSyncLeashProxy packet = PacketSyncLeashProxy.attach(
tiedup$self.getUUID(),
tiedup$leashProxy.getId()
);
ModNetwork.sendToAllTrackingAndSelf(packet, tiedup$self);
}
@Override
public void tiedup$detachLeash() {
tiedup$leashHolder = null;
if (tiedup$leashProxy != null) {
if (
tiedup$leashProxy.isAlive() &&
!tiedup$leashProxy.proxyIsRemoved()
) {
tiedup$leashProxy.proxyRemove();
}
tiedup$leashProxy = null;
// Send detach packet to all tracking clients
PacketSyncLeashProxy packet = PacketSyncLeashProxy.detach(
tiedup$self.getUUID()
);
ModNetwork.sendToAllTrackingAndSelf(packet, tiedup$self);
}
}
@Override
public void tiedup$dropLeash() {
// Don't drop if player is disconnected or dead (position may be invalid)
if (tiedup$self.hasDisconnected() || !tiedup$self.isAlive()) {
return;
}
tiedup$self.drop(new ItemStack(Items.LEAD), false, true);
}
@Override
public boolean tiedup$isLeashed() {
return (
tiedup$leashHolder != null &&
tiedup$leashProxy != null &&
!tiedup$leashProxy.proxyIsRemoved()
);
}
@Override
public Entity tiedup$getLeashHolder() {
return tiedup$leashHolder;
}
@Override
public LeashProxyEntity tiedup$getLeashProxy() {
return tiedup$leashProxy;
}
@Override
public void tiedup$setLeashSlack(double slack) {
this.tiedup$leashSlack = slack;
}
@Override
public double tiedup$getLeashSlack() {
return this.tiedup$leashSlack;
}
// ==================== Tick Update (called from Forge event) ====================
/**
* Update leash state and apply traction if needed.
* Called from LeashTickHandler via Forge TickEvent.
*/
@Override
public void tiedup$tickLeash() {
// Check if this player is still valid
if (!tiedup$self.isAlive() || tiedup$self.hasDisconnected()) {
tiedup$detachLeash();
// Don't drop leash if we're disconnected (we won't be there to pick it up)
return;
}
// Check if holder is still valid
if (tiedup$leashHolder != null) {
boolean holderInvalid =
!tiedup$leashHolder.isAlive() || tiedup$leashHolder.isRemoved();
// If holder is a player, also check if they disconnected
if (
!holderInvalid &&
tiedup$leashHolder instanceof ServerPlayer holderPlayer
) {
holderInvalid = holderPlayer.hasDisconnected();
}
// If player is being used as vehicle, break leash
if (!holderInvalid && tiedup$self.isVehicle()) {
holderInvalid = true;
}
if (holderInvalid) {
tiedup$detachLeash();
tiedup$dropLeash();
return;
}
}
// Sync proxy state with actual leash holder
if (tiedup$leashProxy != null) {
if (tiedup$leashProxy.proxyIsRemoved()) {
tiedup$leashProxy = null;
} else {
Entity holderActual = tiedup$leashHolder;
Entity holderFromProxy = tiedup$leashProxy.getLeashHolder();
// Leash was broken externally (by another player)
if (holderFromProxy == null && holderActual != null) {
tiedup$detachLeash();
tiedup$dropLeash();
return;
}
// Holder changed (shouldn't happen normally)
else if (holderFromProxy != holderActual) {
tiedup$leashHolder = holderFromProxy;
}
}
}
// Periodic resync for late-joining clients (every 5 seconds)
if (tiedup$leashProxy != null && ++tiedup$leashResyncTimer >= 100) {
tiedup$leashResyncTimer = 0;
PacketSyncLeashProxy packet = PacketSyncLeashProxy.attach(
tiedup$self.getUUID(),
tiedup$leashProxy.getId()
);
ModNetwork.sendToAllTrackingAndSelf(packet, tiedup$self);
}
// Apply traction force
tiedup$applyLeashPull();
}
/**
* Apply pull force towards the leash holder if player is too far.
*
* Uses normalized direction, progressive capped force, velocity blending,
* and conditional sync to prevent jitter, oscillation, and runaway velocity.
* Modeled after DamselAIController.tickLeashTraction().
*/
@Unique
private void tiedup$applyLeashPull() {
if (tiedup$leashHolder == null) return;
// Cross-dimension: detach leash cleanly instead of silently ignoring
if (tiedup$leashHolder.level() != tiedup$self.level()) {
tiedup$detachLeash();
tiedup$dropLeash();
return;
}
float distance = tiedup$self.distanceTo(tiedup$leashHolder);
// Apply slack to effective distances (for "pet leads" dogwalk)
double effectivePullStart =
LEASH_PULL_START_DISTANCE + tiedup$leashSlack;
double effectiveMaxDistance = LEASH_MAX_DISTANCE + tiedup$leashSlack;
double effectiveTeleportDist =
LEASH_TELEPORT_DISTANCE + tiedup$leashSlack;
// Close enough: no pull needed, reset stuck counter
if (distance < effectivePullStart) {
tiedup$leashStuckCounter = 0;
return;
}
// Too far: teleport to holder instead of breaking
if (distance > effectiveMaxDistance) {
tiedup$teleportToSafePositionNearHolder();
tiedup$leashStuckCounter = 0;
return;
}
// Direction to holder
double dx = tiedup$leashHolder.getX() - tiedup$self.getX();
double dy = tiedup$leashHolder.getY() - tiedup$self.getY();
double dz = tiedup$leashHolder.getZ() - tiedup$self.getZ();
// Normalized horizontal direction (replaces Math.signum bang-bang)
double horizontalDist = Math.sqrt(dx * dx + dz * dz);
double dirX = horizontalDist > 0.01 ? dx / horizontalDist : 0.0;
double dirZ = horizontalDist > 0.01 ? dz / horizontalDist : 0.0;
// Calculate how much the player moved since last check
double movedX = tiedup$self.getX() - tiedup$prevX;
double movedZ = tiedup$self.getZ() - tiedup$prevZ;
double movedHorizontal = Math.sqrt(movedX * movedX + movedZ * movedZ);
// Store current position for next stuck check
tiedup$prevX = tiedup$self.getX();
tiedup$prevZ = tiedup$self.getZ();
// Stuck detection - slack-aware threshold
boolean isStuck =
distance > effectiveTeleportDist &&
tiedup$self.getDeltaMovement().lengthSqr() < 0.001 &&
movedHorizontal < 0.05;
if (isStuck) {
tiedup$leashStuckCounter++;
if (tiedup$leashStuckCounter >= LEASH_STUCK_THRESHOLD) {
tiedup$teleportToSafePositionNearHolder();
tiedup$leashStuckCounter = 0;
return;
}
} else {
tiedup$leashStuckCounter = 0;
}
// Progressive capped force: 0.04 per block beyond pull start, max 0.14
double distanceBeyond = distance - effectivePullStart;
double forceFactor = Math.min(
LEASH_MAX_FORCE,
distanceBeyond * LEASH_FORCE_RAMP
);
// Fence knots are static, need 1.3x pull
if (tiedup$leashHolder instanceof LeashFenceKnotEntity) {
forceFactor *= 1.3;
}
// Velocity blending: 60% pull direction + 40% existing momentum
net.minecraft.world.phys.Vec3 currentMotion =
tiedup$self.getDeltaMovement();
double pullVelX = dirX * forceFactor * 3.0;
double pullVelZ = dirZ * forceFactor * 3.0;
double newVelX =
currentMotion.x * (1.0 - LEASH_BLEND_FACTOR) +
pullVelX * LEASH_BLEND_FACTOR;
double newVelZ =
currentMotion.z * (1.0 - LEASH_BLEND_FACTOR) +
pullVelZ * LEASH_BLEND_FACTOR;
// Soft auto-step (replaces 0.42 vanilla jump velocity)
double newVelY = currentMotion.y;
if (
tiedup$self.onGround() &&
movedHorizontal < 0.1 &&
distanceBeyond > 0.5
) {
if (dy > 0.3) {
newVelY += 0.08; // Holder is above: gentle upward boost
} else {
newVelY += 0.05; // Normal step-up
}
} else if (dy > 0.5 && !tiedup$self.onGround()) {
newVelY += 0.02; // Gentle aerial drift
}
tiedup$self.setDeltaMovement(newVelX, newVelY, newVelZ);
// Conditional velocity sync: only send packet when force is meaningful
if (forceFactor > 0.02 && tiedup$self.connection != null) {
tiedup$self.connection.send(
new ClientboundSetEntityMotionPacket(tiedup$self)
);
// Only suppress impulse flag after we've actually synced to the client
tiedup$self.hasImpulse = false;
}
}
/**
* Teleport player to a safe position near the leash holder.
* Used when player is stuck and can't path to holder.
*/
@Unique
private void tiedup$teleportToSafePositionNearHolder() {
if (tiedup$leashHolder == null) return;
// Target: 2 blocks away from holder in player's direction
double dx = tiedup$self.getX() - tiedup$leashHolder.getX();
double dz = tiedup$self.getZ() - tiedup$leashHolder.getZ();
double dist = Math.sqrt(dx * dx + dz * dz);
double offsetX = 0;
double offsetZ = 0;
if (dist > 0.1) {
offsetX = (dx / dist) * 2.0;
offsetZ = (dz / dist) * 2.0;
}
double targetX = tiedup$leashHolder.getX() + offsetX;
double targetZ = tiedup$leashHolder.getZ() + offsetZ;
// Find safe Y (ground level)
double targetY = tiedup$findSafeY(
targetX,
tiedup$leashHolder.getY(),
targetZ
);
tiedup$self.teleportTo(targetX, targetY, targetZ);
// Sync position to client (null check for fake players)
if (tiedup$self.connection != null) {
tiedup$self.connection.send(
new ClientboundSetEntityMotionPacket(tiedup$self)
);
}
}
/**
* Find a safe Y coordinate for teleporting.
*
* @param x Target X coordinate
* @param startY Starting Y to search from
* @param z Target Z coordinate
* @return Safe Y coordinate on solid ground
*/
@Unique
private double tiedup$findSafeY(double x, double startY, double z) {
net.minecraft.core.BlockPos.MutableBlockPos mutable =
new net.minecraft.core.BlockPos.MutableBlockPos();
// Search down first (max 5 blocks)
for (int y = 0; y > -5; y--) {
mutable.set((int) x, (int) startY + y, (int) z);
if (
tiedup$self
.level()
.getBlockState(mutable)
.isSolidRender(tiedup$self.level(), mutable) &&
tiedup$self.level().getBlockState(mutable.above()).isAir()
) {
return mutable.getY() + 1;
}
}
// Search up (max 5 blocks)
for (int y = 1; y < 5; y++) {
mutable.set((int) x, (int) startY + y, (int) z);
if (
tiedup$self
.level()
.getBlockState(mutable.below())
.isSolidRender(tiedup$self.level(), mutable.below()) &&
tiedup$self.level().getBlockState(mutable).isAir()
) {
return mutable.getY();
}
}
// Fallback: use holder's Y
return startY;
}
}