Remove build artifacts, dev tool configs, unused dependencies, and third-party source dumps. Add proper README, update .gitignore, clean up Makefile.
474 lines
16 KiB
Java
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;
|
|
}
|
|
}
|