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:
NotEvil
2026-04-12 00:51:22 +02:00
parent 2e7a1d403b
commit f6466360b6
1947 changed files with 238025 additions and 1 deletions

View File

@@ -0,0 +1,234 @@
package com.tiedup.remake.client.events;
import com.mojang.blaze3d.systems.RenderSystem;
import com.tiedup.remake.core.ModConfig;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.state.PlayerBindState;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.entity.HumanoidArm;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.RenderGuiOverlayEvent;
import net.minecraftforge.client.gui.overlay.VanillaGuiOverlay;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
/**
* Phase 5: Blindfold Rendering
*
* Based on the original TiedUp! mod (1.12.2) by Yuti & Marl Velius.
*
* The original approach:
* 1. Render blindfold texture over the entire screen (covers everything)
* 2. Manually redraw the hotbar on top of the blindfold
*
* This ensures the hotbar remains visible while the game world is obscured.
*/
@Mod.EventBusSubscriber(
modid = TiedUpMod.MOD_ID,
bus = Mod.EventBusSubscriber.Bus.FORGE,
value = Dist.CLIENT
)
public class BlindfoldRenderEventHandler {
private static final ResourceLocation BLINDFOLD_TEXTURE =
ResourceLocation.fromNamespaceAndPath(
TiedUpMod.MOD_ID,
"textures/misc/blindfolded.png"
);
// Vanilla widgets texture (contains hotbar graphics)
private static final ResourceLocation WIDGETS_TEXTURE =
ResourceLocation.fromNamespaceAndPath(
"minecraft",
"textures/gui/widgets.png"
);
private static boolean wasBlindfolded = false;
/**
* Render the blindfold overlay AFTER the hotbar is rendered.
* Then redraw the hotbar on top of the blindfold (original mod approach).
*/
@SubscribeEvent
public static void onRenderGuiPost(RenderGuiOverlayEvent.Post event) {
// Render after HOTBAR (same as original mod)
if (event.getOverlay() != VanillaGuiOverlay.HOTBAR.type()) {
return;
}
Minecraft mc = Minecraft.getInstance();
LocalPlayer player = mc.player;
// Safety checks
if (player == null || mc.options.hideGui) {
return;
}
// Non-hardcore mode: hide blindfold when a GUI screen is open
boolean hardcore = ModConfig.CLIENT.hardcoreBlindfold.get();
if (!hardcore && mc.screen != null) {
return;
}
// Get player state
PlayerBindState state = PlayerBindState.getInstance(player);
if (state == null) {
return;
}
boolean isBlindfolded = state.isBlindfolded();
// Log state changes only
if (isBlindfolded != wasBlindfolded) {
if (isBlindfolded) {
TiedUpMod.LOGGER.info(
"[BLINDFOLD] Player is now blindfolded - rendering overlay"
);
} else {
TiedUpMod.LOGGER.info(
"[BLINDFOLD] Player is no longer blindfolded - stopping overlay"
);
}
wasBlindfolded = isBlindfolded;
}
// Only render if blindfolded
if (!isBlindfolded) {
return;
}
try {
int screenWidth = mc.getWindow().getGuiScaledWidth();
int screenHeight = mc.getWindow().getGuiScaledHeight();
// Set opacity: hardcore forces full opacity, otherwise use config
float opacity = hardcore
? 1.0F
: ModConfig.CLIENT.blindfoldOverlayOpacity
.get()
.floatValue();
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, opacity);
RenderSystem.enableBlend();
RenderSystem.defaultBlendFunc();
// Step 1: Render the blindfold texture over the entire screen
event
.getGuiGraphics()
.blit(
BLINDFOLD_TEXTURE,
0,
0,
0.0F,
0.0F,
screenWidth,
screenHeight,
screenWidth,
screenHeight
);
// Reset shader color for hotbar
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
// Step 2: Redraw the hotbar on top of the blindfold (original mod approach)
redrawHotbar(
mc,
event.getGuiGraphics(),
screenWidth,
screenHeight,
player
);
} catch (RuntimeException e) {
TiedUpMod.LOGGER.error("[BLINDFOLD] Error rendering overlay", e);
}
}
/**
* Manually redraw the hotbar on top of the blindfold texture.
* Based on the original mod's redrawHotBar() function.
*
* This draws:
* - Hotbar background (182x22 pixels)
* - Selected slot highlight
* - Offhand slot (if item present)
*/
private static void redrawHotbar(
Minecraft mc,
GuiGraphics guiGraphics,
int screenWidth,
int screenHeight,
LocalPlayer player
) {
// Reset render state
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
RenderSystem.enableBlend();
RenderSystem.defaultBlendFunc();
// Center of screen (hotbar is centered)
int centerX = screenWidth / 2;
int hotbarY = screenHeight - 22; // Hotbar is 22 pixels from bottom
// Draw hotbar background (182 pixels wide, 22 pixels tall)
// Original: this.drawTexturedModalRect(i - 91, sr.getScaledHeight() - 22, 0, 0, 182, 22);
guiGraphics.blit(
WIDGETS_TEXTURE,
centerX - 91,
hotbarY, // Position
0,
0, // Texture UV start
182,
22 // Size
);
// Draw selected slot highlight (24x22 pixels)
// Original: this.drawTexturedModalRect(i - 91 - 1 + entityplayer.inventory.currentItem * 20, ...);
int selectedSlot = player.getInventory().selected;
guiGraphics.blit(
WIDGETS_TEXTURE,
centerX - 91 - 1 + selectedSlot * 20,
hotbarY - 1, // Position (offset by selected slot)
0,
22, // Texture UV (highlight texture)
24,
22 // Size
);
// Draw offhand slot if player has an item in offhand
ItemStack offhandItem = player.getItemInHand(InteractionHand.OFF_HAND);
if (!offhandItem.isEmpty()) {
HumanoidArm offhandSide = player.getMainArm().getOpposite();
if (offhandSide == HumanoidArm.LEFT) {
// Offhand on left side
// Original: this.drawTexturedModalRect(i - 91 - 29, sr.getScaledHeight() - 23, 24, 22, 29, 24);
guiGraphics.blit(
WIDGETS_TEXTURE,
centerX - 91 - 29,
hotbarY - 1, // Position
24,
22, // Texture UV
29,
24 // Size
);
} else {
// Offhand on right side
// Original: this.drawTexturedModalRect(i + 91, sr.getScaledHeight() - 23, 53, 22, 29, 24);
guiGraphics.blit(
WIDGETS_TEXTURE,
centerX + 91,
hotbarY - 1, // Position
53,
22, // Texture UV
29,
24 // Size
);
}
}
RenderSystem.disableBlend();
}
}

View File

@@ -0,0 +1,323 @@
package com.tiedup.remake.client.events;
import com.mojang.blaze3d.vertex.PoseStack;
import com.tiedup.remake.blocks.BlockMarker;
import com.tiedup.remake.blocks.entity.MarkerBlockEntity;
import com.tiedup.remake.cells.CellDataV2;
import com.tiedup.remake.cells.CellRegistryV2;
import com.tiedup.remake.cells.MarkerType;
import com.tiedup.remake.client.renderer.CellOutlineRenderer;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.ItemAdminWand;
import java.util.UUID;
import net.minecraft.client.Camera;
import net.minecraft.client.Minecraft;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.RenderLevelStageEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
/**
* Event handler for rendering cell outlines when holding an admin wand.
*
* Phase: Kidnapper Revamp - Cell System
*
* Renders colored outlines around cell positions when:
* - Player is holding an Admin Wand
* - A cell is currently selected in the wand
*
* The outlines help builders visualize which blocks are part of the cell.
*
* Network sync: On dedicated servers, cell data is synced via PacketSyncCellData.
* On integrated servers, direct access to server data is used as a fallback.
*/
@Mod.EventBusSubscriber(
modid = TiedUpMod.MOD_ID,
bus = Mod.EventBusSubscriber.Bus.FORGE,
value = Dist.CLIENT
)
public class CellHighlightHandler {
// Client-side cache of cell data (synced from server via PacketSyncCellData)
private static final java.util.Map<UUID, CellDataV2> syncedCells =
new java.util.concurrent.ConcurrentHashMap<>();
// Legacy single-cell cache for backward compatibility
private static CellDataV2 cachedCellData = null;
private static UUID cachedCellId = null;
/**
* Render cell outlines after translucent blocks.
*/
@SubscribeEvent
public static void onRenderLevelStage(RenderLevelStageEvent event) {
// Only render after translucent stage (so outlines appear on top)
if (
event.getStage() !=
RenderLevelStageEvent.Stage.AFTER_TRANSLUCENT_BLOCKS
) {
return;
}
Minecraft mc = Minecraft.getInstance();
LocalPlayer player = mc.player;
if (player == null) return;
// Check if player is holding an Admin Wand
ItemStack mainHand = player.getMainHandItem();
ItemStack offHand = player.getOffhandItem();
boolean holdingAdminWand =
mainHand.getItem() instanceof ItemAdminWand ||
offHand.getItem() instanceof ItemAdminWand;
if (!holdingAdminWand) {
cachedCellData = null;
cachedCellId = null;
return;
}
PoseStack poseStack = event.getPoseStack();
Camera camera = event.getCamera();
// If holding Admin Wand, render nearby structure markers and preview
renderNearbyStructureMarkers(poseStack, camera, player);
renderAdminWandPreview(poseStack, camera, player, mainHand, offHand);
}
/**
* Render a preview outline showing where the Admin Wand will place a marker.
*/
private static void renderAdminWandPreview(
PoseStack poseStack,
Camera camera,
LocalPlayer player,
ItemStack mainHand,
ItemStack offHand
) {
// Get the block the player is looking at
net.minecraft.world.phys.HitResult hitResult =
Minecraft.getInstance().hitResult;
if (
hitResult == null ||
hitResult.getType() != net.minecraft.world.phys.HitResult.Type.BLOCK
) {
return;
}
net.minecraft.world.phys.BlockHitResult blockHit =
(net.minecraft.world.phys.BlockHitResult) hitResult;
BlockPos targetPos = blockHit.getBlockPos().above(); // Marker goes above the clicked block
// Get the current marker type from the wand
MarkerType type;
if (mainHand.getItem() instanceof ItemAdminWand) {
type = ItemAdminWand.getCurrentType(mainHand);
} else {
type = ItemAdminWand.getCurrentType(offHand);
}
Vec3 cameraPos = camera.getPosition();
float[] color = CellOutlineRenderer.getColorForType(type);
// Make preview semi-transparent and pulsing
float alpha =
0.5f + 0.3f * (float) Math.sin(System.currentTimeMillis() / 200.0);
float[] previewColor = { color[0], color[1], color[2], alpha };
// Setup rendering (depth test off so preview shows through blocks)
com.mojang.blaze3d.systems.RenderSystem.enableBlend();
com.mojang.blaze3d.systems.RenderSystem.defaultBlendFunc();
com.mojang.blaze3d.systems.RenderSystem.disableDepthTest();
com.mojang.blaze3d.systems.RenderSystem.depthMask(false);
com.mojang.blaze3d.systems.RenderSystem.setShader(
net.minecraft.client.renderer.GameRenderer::getPositionColorShader
);
CellOutlineRenderer.renderFilledBlock(
poseStack,
targetPos,
cameraPos,
previewColor
);
com.mojang.blaze3d.systems.RenderSystem.depthMask(true);
com.mojang.blaze3d.systems.RenderSystem.enableDepthTest();
com.mojang.blaze3d.systems.RenderSystem.disableBlend();
}
/**
* Render outlines for nearby structure markers (markers without cell IDs).
*/
private static void renderNearbyStructureMarkers(
PoseStack poseStack,
Camera camera,
LocalPlayer player
) {
Level level = player.level();
BlockPos playerPos = player.blockPosition();
Vec3 cameraPos = camera.getPosition();
// Collect markers first to check if we need to render anything
java.util.List<
java.util.Map.Entry<BlockPos, MarkerType>
> markersToRender = new java.util.ArrayList<>();
// Scan in a 32-block radius for structure markers
int radius = 32;
for (int x = -radius; x <= radius; x++) {
for (int y = -radius / 2; y <= radius / 2; y++) {
for (int z = -radius; z <= radius; z++) {
BlockPos pos = playerPos.offset(x, y, z);
if (
level.getBlockState(pos).getBlock() instanceof
BlockMarker
) {
BlockEntity be = level.getBlockEntity(pos);
if (be instanceof MarkerBlockEntity marker) {
// Only render structure markers (no cell ID)
if (marker.getCellId() == null) {
markersToRender.add(
java.util.Map.entry(
pos,
marker.getMarkerType()
)
);
}
}
}
}
}
}
// Only setup rendering if we have markers to render
if (!markersToRender.isEmpty()) {
// Setup rendering state (depth test off so markers show through blocks)
com.mojang.blaze3d.systems.RenderSystem.enableBlend();
com.mojang.blaze3d.systems.RenderSystem.defaultBlendFunc();
com.mojang.blaze3d.systems.RenderSystem.disableDepthTest();
com.mojang.blaze3d.systems.RenderSystem.depthMask(false);
com.mojang.blaze3d.systems.RenderSystem.setShader(
net.minecraft.client.renderer
.GameRenderer::getPositionColorShader
);
for (var entry : markersToRender) {
BlockPos pos = entry.getKey();
MarkerType type = entry.getValue();
float[] baseColor = CellOutlineRenderer.getColorForType(type);
// Semi-transparent fill
float[] fillColor = {
baseColor[0],
baseColor[1],
baseColor[2],
0.4f,
};
CellOutlineRenderer.renderFilledBlock(
poseStack,
pos,
cameraPos,
fillColor
);
}
// Restore rendering state
com.mojang.blaze3d.systems.RenderSystem.depthMask(true);
com.mojang.blaze3d.systems.RenderSystem.enableDepthTest();
com.mojang.blaze3d.systems.RenderSystem.disableBlend();
}
}
/**
* Get cell data on client side.
* First checks the network-synced cache, then falls back to integrated server access.
*
* @param cellId The cell UUID to look up
* @return CellDataV2 if found, null otherwise
*/
private static CellDataV2 getCellDataClient(UUID cellId) {
if (cellId == null) return null;
// Priority 1: Check network-synced cache (works on dedicated servers)
CellDataV2 synced = syncedCells.get(cellId);
if (synced != null) {
return synced;
}
// Priority 2: Check legacy single-cell cache
if (cellId.equals(cachedCellId) && cachedCellData != null) {
return cachedCellData;
}
// Priority 3: On integrated server, access server level directly (fallback)
Minecraft mc = Minecraft.getInstance();
if (mc.getSingleplayerServer() != null) {
ServerLevel serverLevel = mc.getSingleplayerServer().overworld();
if (serverLevel != null) {
CellRegistryV2 registry = CellRegistryV2.get(serverLevel);
CellDataV2 cell = registry.getCell(cellId);
if (cell != null) {
// Cache for future use
cachedCellId = cellId;
cachedCellData = cell;
return cell;
}
}
}
// Not found - on dedicated server, packet hasn't arrived yet
return null;
}
/**
* Update cached cell data (called from network sync - PacketSyncCellData).
* Stores in both the synced map and legacy cache for compatibility.
*
* @param cell The cell data received from server
*/
public static void updateCachedCell(CellDataV2 cell) {
if (cell != null) {
// Store in synced map
syncedCells.put(cell.getId(), cell);
// Also update legacy cache
cachedCellId = cell.getId();
cachedCellData = cell;
}
}
/**
* Remove a cell from the cache (e.g., when cell is deleted).
*
* @param cellId The cell UUID to remove
*/
public static void removeCachedCell(UUID cellId) {
if (cellId != null) {
syncedCells.remove(cellId);
if (cellId.equals(cachedCellId)) {
cachedCellId = null;
cachedCellData = null;
}
}
}
/**
* Clear all cached cell data.
* Called when disconnecting from server or on dimension change.
*/
public static void clearCache() {
syncedCells.clear();
cachedCellId = null;
cachedCellData = null;
}
}

View File

@@ -0,0 +1,91 @@
package com.tiedup.remake.client.events;
import com.tiedup.remake.client.MuffledSoundInstance;
import com.tiedup.remake.core.ModConfig;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.state.PlayerBindState;
import net.minecraft.client.Minecraft;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.client.resources.sounds.SoundInstance;
import net.minecraft.sounds.SoundSource;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.client.event.sound.PlaySoundEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
/**
* Client-side sound handler for earplugs effect.
*
* When the player has earplugs equipped, all sounds are muffled
* (volume reduced to simulate hearing impairment).
*
* Based on Forge's PlaySoundEvent to intercept and modify sounds.
*/
@OnlyIn(Dist.CLIENT)
@Mod.EventBusSubscriber(
modid = TiedUpMod.MOD_ID,
bus = Mod.EventBusSubscriber.Bus.FORGE,
value = Dist.CLIENT
)
public class EarplugSoundHandler {
/** Pitch modifier to make sounds more muffled (much lower for "underwater" effect) */
private static final float MUFFLED_PITCH_MODIFIER = 0.6f;
/** Categories to always let through at normal volume (important UI feedback) */
private static final SoundSource[] UNAFFECTED_CATEGORIES = {
SoundSource.MASTER, // Master volume controls
SoundSource.MUSIC, // Music is internal, not muffled by earplugs
};
/**
* Intercept sound events and muffle them if player has earplugs.
*/
@SubscribeEvent
public static void onPlaySound(PlaySoundEvent event) {
// Get the sound being played
SoundInstance sound = event.getSound();
if (sound == null) {
return;
}
// Check if player has earplugs
Minecraft mc = Minecraft.getInstance();
if (mc == null) {
return;
}
LocalPlayer player = mc.player;
if (player == null) {
return;
}
PlayerBindState state = PlayerBindState.getInstance(player);
if (state == null || !state.hasEarplugs()) {
return;
}
// Check if this sound category should be affected
SoundSource source = sound.getSource();
for (SoundSource unaffected : UNAFFECTED_CATEGORIES) {
if (source == unaffected) {
return; // Don't muffle this category
}
}
// Don't wrap already-wrapped sounds (prevent infinite recursion)
if (sound instanceof MuffledSoundInstance) {
return;
}
// Wrap the sound with our muffling wrapper
// The wrapper delegates to the original but modifies getVolume()/getPitch()
SoundInstance muffledSound = new MuffledSoundInstance(
sound,
ModConfig.CLIENT.earplugVolumeMultiplier.get().floatValue(),
MUFFLED_PITCH_MODIFIER
);
event.setSound(muffledSound);
}
}

View File

@@ -0,0 +1,71 @@
package com.tiedup.remake.client.events;
import com.mojang.logging.LogUtils;
import com.tiedup.remake.client.animation.BondageAnimationManager;
import com.tiedup.remake.client.animation.PendingAnimationManager;
import com.tiedup.remake.core.TiedUpMod;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.event.entity.EntityLeaveLevelEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import org.slf4j.Logger;
/**
* Automatic cleanup handler for entity-related resources.
*
* <p>This handler automatically cleans up animation layers and pending animations
* when entities leave the world, preventing memory leaks from stale cache entries.
*
* <p>Phase: Performance & Memory Management
*
* <p>Previously, cleanup had to be called manually via {@link BondageAnimationManager#cleanup(java.util.UUID)},
* which was error-prone and could lead to memory leaks if forgotten.
* This handler ensures cleanup happens automatically on entity removal.
*/
@Mod.EventBusSubscriber(
modid = TiedUpMod.MOD_ID,
bus = Mod.EventBusSubscriber.Bus.FORGE,
value = Dist.CLIENT
)
public class EntityCleanupHandler {
private static final Logger LOGGER = LogUtils.getLogger();
/**
* Automatically clean up animation resources when an entity leaves the world.
*
* <p>This event fires when:
* <ul>
* <li>An entity is removed from the world (killed, despawned, unloaded)</li>
* <li>A player logs out</li>
* <li>A chunk is unloaded and its entities are removed</li>
* </ul>
*
* <p>Cleanup includes:
* <ul>
* <li>Removing animation layers from {@link BondageAnimationManager}</li>
* <li>Removing pending animations from {@link PendingAnimationManager}</li>
* </ul>
*
* @param event The entity leave level event
*/
@SubscribeEvent
public static void onEntityLeaveLevel(EntityLeaveLevelEvent event) {
// Only process on client side
if (!event.getLevel().isClientSide()) {
return;
}
// Clean up animation layers
BondageAnimationManager.cleanup(event.getEntity().getUUID());
// Clean up pending animation queue
PendingAnimationManager.remove(event.getEntity().getUUID());
LOGGER.debug(
"Auto-cleaned animation resources for entity: {} (type: {})",
event.getEntity().getUUID(),
event.getEntity().getClass().getSimpleName()
);
}
}

View File

@@ -0,0 +1,156 @@
package com.tiedup.remake.client.events;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.items.base.BindVariant;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.KidnappedHelper;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import net.minecraft.client.Minecraft;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
/**
* Client-side handler for smooth leash proxy positioning.
*
* FIX: Changed from RenderLevelStageEvent.AFTER_ENTITIES to ClientTickEvent.
* AFTER_ENTITIES positioned the proxy AFTER rendering, causing 1-frame lag.
* ClientTickEvent positions BEFORE rendering for smooth leash display.
*
* Instead of waiting for server position updates (which causes lag),
* this handler repositions the proxy entity locally each tick based
* on the player's current position.
*/
@OnlyIn(Dist.CLIENT)
@Mod.EventBusSubscriber(
modid = TiedUpMod.MOD_ID,
value = Dist.CLIENT,
bus = Mod.EventBusSubscriber.Bus.FORGE
)
public class LeashProxyClientHandler {
/**
* Map of player UUID -> proxy entity ID.
* Uses UUID for player (persistent) and entity ID for proxy (runtime).
*/
private static final Map<UUID, Integer> playerToProxy =
new ConcurrentHashMap<>();
/** Default Y offset for normal standing pose (neck height) */
private static final double DEFAULT_Y_OFFSET = 1.3;
/** Y offset for dogwalk pose (back/hip level) */
private static final double DOGWALK_Y_OFFSET = 0.35;
/**
* Handle sync packet from server.
* Called when a player gets leashed or unleashed.
*/
public static void handleSyncPacket(
UUID targetPlayerUUID,
int proxyId,
boolean attach
) {
if (attach) {
playerToProxy.put(targetPlayerUUID, proxyId);
TiedUpMod.LOGGER.debug(
"[LeashProxyClient] Registered proxy {} for player {}",
proxyId,
targetPlayerUUID
);
} else {
playerToProxy.remove(targetPlayerUUID);
TiedUpMod.LOGGER.debug(
"[LeashProxyClient] Removed proxy for player {}",
targetPlayerUUID
);
}
}
/**
* FIX: Use ClientTickEvent instead of RenderLevelStageEvent.AFTER_ENTITIES.
* This positions proxies BEFORE rendering, eliminating the 1-frame lag
* that caused jittery leash rendering.
*/
@SubscribeEvent
public static void onClientTick(TickEvent.ClientTickEvent event) {
// Only run at end of tick (after player position is updated)
if (event.phase != TickEvent.Phase.END) {
return;
}
if (playerToProxy.isEmpty()) {
return;
}
Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.level == null || mc.isPaused()) {
return;
}
Level level = mc.level;
// Reposition each tracked proxy
for (Map.Entry<UUID, Integer> entry : playerToProxy.entrySet()) {
UUID playerUUID = entry.getKey();
int proxyId = entry.getValue();
Player playerEntity = level.getPlayerByUUID(playerUUID);
Entity proxyEntity = level.getEntity(proxyId);
if (playerEntity != null && proxyEntity != null) {
// FIX: Calculate Y offset based on bind type (dogwalk vs normal)
double yOffset = calculateYOffset(playerEntity);
// Use current position (interpolation will be handled by renderer)
double x = playerEntity.getX();
double y = playerEntity.getY() + yOffset;
double z = playerEntity.getZ() - 0.15;
// Set proxy position
proxyEntity.setPos(x, y, z);
// Update old positions for smooth interpolation
proxyEntity.xOld = proxyEntity.xo = x;
proxyEntity.yOld = proxyEntity.yo = y;
proxyEntity.zOld = proxyEntity.zo = z;
}
}
}
/**
* Calculate Y offset based on player's bind type.
* Dogwalk (DOGBINDER) uses lower offset for 4-legged pose.
*/
private static double calculateYOffset(Player player) {
IBondageState state = KidnappedHelper.getKidnappedState(player);
if (state != null && state.isTiedUp()) {
ItemStack bind = state.getEquipment(BodyRegionV2.ARMS);
if (
!bind.isEmpty() &&
bind.getItem() == ModItems.getBind(BindVariant.DOGBINDER)
) {
return DOGWALK_Y_OFFSET;
}
}
return DEFAULT_Y_OFFSET;
}
/**
* Clear all tracked proxies.
* Called when disconnecting from server.
*/
public static void clearAll() {
playerToProxy.clear();
}
}

View File

@@ -0,0 +1,179 @@
package com.tiedup.remake.client.events;
import com.tiedup.remake.items.base.*;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.v2.bondage.IV2BondageItem;
import com.tiedup.remake.network.selfbondage.PacketSelfBondage;
import net.minecraft.client.Minecraft;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
/**
* Client-side event handler for self-bondage input.
*
* Intercepts left-click when holding bondage items and
* sends packets continuously to the server to perform self-bondage.
*
* Self-bondage items:
* - Binds (rope, chain, etc.) - Self-tie (requires holding left-click)
* - Gags - Self-gag (if already tied, instant)
* - Blindfolds - Self-blindfold (if already tied, instant)
* - Mittens - Self-mitten (if already tied, instant)
* - Earplugs - Self-earplug (if already tied, instant)
* - Collar - NOT ALLOWED (cannot self-collar)
*/
@Mod.EventBusSubscriber(
modid = "tiedup",
value = Dist.CLIENT,
bus = Mod.EventBusSubscriber.Bus.FORGE
)
@OnlyIn(Dist.CLIENT)
public class SelfBondageInputHandler {
/** Track if we're currently in self-bondage mode */
private static boolean isSelfBondageActive = false;
/** The hand we're using for self-bondage */
private static InteractionHand activeHand = null;
/** Tick counter for packet sending interval */
private static int tickCounter = 0;
/** Send packet every 4 ticks (5 times per second) for smooth progress */
private static final int PACKET_INTERVAL = 4;
/**
* Handle left-click in empty air - START self-bondage.
*/
@SubscribeEvent
public static void onLeftClickEmpty(
PlayerInteractEvent.LeftClickEmpty event
) {
startSelfBondage();
}
/**
* Handle left-click on block - START self-bondage (cancel block breaking).
*/
@SubscribeEvent
public static void onLeftClickBlock(
PlayerInteractEvent.LeftClickBlock event
) {
if (!event.getLevel().isClientSide()) return;
ItemStack stack = event.getItemStack();
if (isSelfBondageItem(stack.getItem())) {
event.setCanceled(true);
startSelfBondage();
}
}
/**
* Start self-bondage mode if holding a bondage item.
*/
private static void startSelfBondage() {
LocalPlayer player = Minecraft.getInstance().player;
if (player == null) return;
// Check main hand first, then off hand
InteractionHand hand = InteractionHand.MAIN_HAND;
ItemStack stack = player.getMainHandItem();
if (!isSelfBondageItem(stack.getItem())) {
stack = player.getOffhandItem();
hand = InteractionHand.OFF_HAND;
if (!isSelfBondageItem(stack.getItem())) {
return; // No bondage item in either hand
}
}
// Start self-bondage mode
isSelfBondageActive = true;
activeHand = hand;
tickCounter = 0;
// Send initial packet immediately
ModNetwork.sendToServer(new PacketSelfBondage(hand));
}
/**
* Client tick - continuously send packets while attack button is held.
*/
@SubscribeEvent
public static void onClientTick(TickEvent.ClientTickEvent event) {
if (event.phase != TickEvent.Phase.END) return;
if (!isSelfBondageActive) return;
Minecraft mc = Minecraft.getInstance();
LocalPlayer player = mc.player;
// Stop if conditions are no longer valid
if (player == null || mc.screen != null) {
stopSelfBondage();
return;
}
// Check if attack button is still held
if (!mc.options.keyAttack.isDown()) {
stopSelfBondage();
return;
}
// Check if still holding bondage item in the active hand
ItemStack stack = player.getItemInHand(activeHand);
if (!isSelfBondageItem(stack.getItem())) {
stopSelfBondage();
return;
}
// Send packet at interval for continuous progress
tickCounter++;
if (tickCounter >= PACKET_INTERVAL) {
tickCounter = 0;
ModNetwork.sendToServer(new PacketSelfBondage(activeHand));
}
}
/**
* Stop self-bondage mode.
*/
private static void stopSelfBondage() {
isSelfBondageActive = false;
activeHand = null;
tickCounter = 0;
}
/**
* Check if an item supports self-bondage.
* Collar is explicitly excluded.
*/
private static boolean isSelfBondageItem(Item item) {
// Collar cannot be self-equipped (V1 collar guard)
if (item instanceof ItemCollar) {
return false;
}
// V2 bondage items support self-bondage (left-click hold with tying duration)
if (item instanceof IV2BondageItem) {
return true;
}
// V1 bondage items (legacy)
return (
item instanceof ItemBind ||
item instanceof ItemGag ||
item instanceof ItemBlindfold ||
item instanceof ItemMittens ||
item instanceof ItemEarplugs
);
}
}