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.
This commit is contained in:
473
src/main/java/com/tiedup/remake/mixin/MixinServerPlayer.java
Normal file
473
src/main/java/com/tiedup/remake/mixin/MixinServerPlayer.java
Normal file
@@ -0,0 +1,473 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user