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,280 @@
package com.tiedup.remake.network;
import com.tiedup.remake.compat.mca.network.PacketSyncMCABondage;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.network.action.PacketForceFeeding;
import com.tiedup.remake.network.action.PacketForceSeatModifier;
import com.tiedup.remake.network.action.PacketSetKnifeCutTarget;
import com.tiedup.remake.network.action.PacketStruggle;
import com.tiedup.remake.network.action.PacketTighten;
import com.tiedup.remake.network.action.PacketTying;
import com.tiedup.remake.network.action.PacketUntying;
import com.tiedup.remake.network.armorstand.PacketSyncArmorStandBondage;
import com.tiedup.remake.network.bounty.PacketDeleteBounty;
import com.tiedup.remake.network.bounty.PacketRequestBounties;
import com.tiedup.remake.network.bounty.PacketSendBounties;
import com.tiedup.remake.network.cell.PacketAssignCellToCollar;
import com.tiedup.remake.network.cell.PacketCellAction;
import com.tiedup.remake.network.cell.PacketCoreMenuAction;
import com.tiedup.remake.network.cell.PacketOpenCellManager;
import com.tiedup.remake.network.cell.PacketOpenCellSelector;
import com.tiedup.remake.network.cell.PacketOpenCoreMenu;
import com.tiedup.remake.network.cell.PacketRenameCell;
import com.tiedup.remake.network.cell.PacketRequestCellList;
import com.tiedup.remake.network.cell.PacketSyncCellData;
import com.tiedup.remake.network.conversation.PacketEndConversationC2S;
import com.tiedup.remake.network.conversation.PacketEndConversationS2C;
import com.tiedup.remake.network.conversation.PacketOpenConversation;
import com.tiedup.remake.network.conversation.PacketRequestConversation;
import com.tiedup.remake.network.conversation.PacketSelectTopic;
import com.tiedup.remake.network.item.PacketAdjustItem;
import com.tiedup.remake.network.item.PacketAdjustRemote;
import com.tiedup.remake.network.labor.PacketSyncLaborProgress;
import com.tiedup.remake.network.master.PacketMasterStateSync;
import com.tiedup.remake.network.master.PacketOpenPetRequestMenu;
import com.tiedup.remake.network.master.PacketPetRequest;
import com.tiedup.remake.network.merchant.PacketCloseMerchantScreen;
import com.tiedup.remake.network.merchant.PacketOpenMerchantScreen;
import com.tiedup.remake.network.merchant.PacketPurchaseTrade;
import com.tiedup.remake.network.minigame.PacketContinuousStruggleHold;
import com.tiedup.remake.network.minigame.PacketContinuousStruggleState;
import com.tiedup.remake.network.minigame.PacketContinuousStruggleStop;
import com.tiedup.remake.network.minigame.PacketLockpickAttempt;
import com.tiedup.remake.network.minigame.PacketLockpickMiniGameMove;
import com.tiedup.remake.network.minigame.PacketLockpickMiniGameResult;
import com.tiedup.remake.network.minigame.PacketLockpickMiniGameStart;
import com.tiedup.remake.network.minigame.PacketLockpickMiniGameState;
import com.tiedup.remake.network.personality.PacketDisciplineAction;
import com.tiedup.remake.network.personality.PacketNpcCommand;
import com.tiedup.remake.network.personality.PacketOpenCommandWandScreen;
import com.tiedup.remake.network.personality.PacketRequestNpcInventory;
import com.tiedup.remake.network.personality.PacketSlaveBeingFreed;
import com.tiedup.remake.network.selfbondage.PacketSelfBondage;
import com.tiedup.remake.network.slave.PacketMasterEquip;
import com.tiedup.remake.network.slave.PacketSlaveAction;
import com.tiedup.remake.network.slave.PacketSlaveItemManage;
import com.tiedup.remake.network.sync.PacketPlayTestAnimation;
import com.tiedup.remake.network.sync.PacketSyncBindState;
import com.tiedup.remake.network.sync.PacketSyncClothesConfig;
import com.tiedup.remake.network.sync.PacketSyncCollarRegistry;
import com.tiedup.remake.network.sync.PacketSyncEnslavement;
import com.tiedup.remake.network.sync.PacketSyncLeashProxy;
import com.tiedup.remake.network.sync.PacketSyncMovementStyle;
import com.tiedup.remake.network.sync.PacketSyncPetBedState;
import com.tiedup.remake.network.sync.PacketSyncStruggleState;
import com.tiedup.remake.network.trader.PacketBuyCaptive;
import com.tiedup.remake.network.trader.PacketOpenTraderScreen;
import com.tiedup.remake.v2.bondage.network.PacketSyncV2Equipment;
import com.tiedup.remake.v2.bondage.network.PacketV2LockToggle;
import com.tiedup.remake.v2.bondage.network.PacketV2SelfEquip;
import com.tiedup.remake.v2.bondage.network.PacketV2SelfLock;
import com.tiedup.remake.v2.bondage.network.PacketV2SelfRemove;
import com.tiedup.remake.v2.bondage.network.PacketV2SelfUnlock;
import com.tiedup.remake.v2.bondage.network.PacketV2StruggleStart;
import com.tiedup.remake.v2.furniture.network.PacketFurnitureEscape;
import com.tiedup.remake.v2.furniture.network.PacketFurnitureForcemount;
import com.tiedup.remake.v2.furniture.network.PacketFurnitureLock;
import com.tiedup.remake.v2.furniture.network.PacketSyncFurnitureDefinitions;
import com.tiedup.remake.v2.furniture.network.PacketSyncFurnitureState;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.network.NetworkEvent;
import net.minecraftforge.network.NetworkRegistry;
import net.minecraftforge.network.PacketDistributor;
import net.minecraftforge.network.simple.SimpleChannel;
/**
* Network handler for TiedUp mod.
* Manages packet registration and sending.
*/
public class ModNetwork {
private static final String PROTOCOL_VERSION = "1";
public static final SimpleChannel CHANNEL =
NetworkRegistry.newSimpleChannel(
ResourceLocation.fromNamespaceAndPath(TiedUpMod.MOD_ID, "main"),
() -> PROTOCOL_VERSION,
PROTOCOL_VERSION::equals,
PROTOCOL_VERSION::equals
);
private static int packetId = 0;
/**
* Register all packets. Called during mod initialization.
* Order matters — packet IDs are assigned sequentially.
*/
public static void register() {
// Sync (S2C)
reg(PacketSyncBindState.class, PacketSyncBindState::encode, PacketSyncBindState::decode, PacketSyncBindState::handle);
reg(PacketSyncStruggleState.class, PacketSyncStruggleState::encode, PacketSyncStruggleState::decode, PacketSyncStruggleState::handle);
reg(PacketSyncEnslavement.class, PacketSyncEnslavement::encode, PacketSyncEnslavement::decode, PacketSyncEnslavement::handle);
reg(PacketSyncLeashProxy.class, PacketSyncLeashProxy::encode, PacketSyncLeashProxy::decode, PacketSyncLeashProxy::handle);
reg(PacketSyncCollarRegistry.class, PacketSyncCollarRegistry::encode, PacketSyncCollarRegistry::decode, PacketSyncCollarRegistry::handle);
reg(PacketSyncClothesConfig.class, PacketSyncClothesConfig::encode, PacketSyncClothesConfig::decode, PacketSyncClothesConfig::handle);
reg(PacketSyncPetBedState.class, PacketSyncPetBedState::encode, PacketSyncPetBedState::decode, PacketSyncPetBedState::handle);
reg(PacketSyncCellData.class, PacketSyncCellData::encode, PacketSyncCellData::decode, PacketSyncCellData::handle);
reg(PacketSyncLaborProgress.class, PacketSyncLaborProgress::encode, PacketSyncLaborProgress::decode, PacketSyncLaborProgress::handle);
reg(PacketSyncArmorStandBondage.class, PacketSyncArmorStandBondage::encode, PacketSyncArmorStandBondage::decode, PacketSyncArmorStandBondage::handle);
reg(PacketPlayTestAnimation.class, PacketPlayTestAnimation::encode, PacketPlayTestAnimation::decode, PacketPlayTestAnimation::handle);
reg(PacketSyncMCABondage.class, PacketSyncMCABondage::encode, PacketSyncMCABondage::decode, PacketSyncMCABondage::handle);
// Actions (bidirectional)
reg(PacketTying.class, PacketTying::encode, PacketTying::decode, PacketTying::handle);
reg(PacketUntying.class, PacketUntying::encode, PacketUntying::decode, PacketUntying::handle);
reg(PacketForceFeeding.class, PacketForceFeeding::encode, PacketForceFeeding::decode, PacketForceFeeding::handle);
reg(PacketStruggle.class, PacketStruggle::encode, PacketStruggle::decode, PacketStruggle::handle);
reg(PacketTighten.class, PacketTighten::encode, PacketTighten::decode, PacketTighten::handle);
reg(PacketSetKnifeCutTarget.class, PacketSetKnifeCutTarget::encode, PacketSetKnifeCutTarget::decode, PacketSetKnifeCutTarget::handle);
reg(PacketSelfBondage.class, PacketSelfBondage::encode, PacketSelfBondage::decode, PacketSelfBondage::handle);
reg(PacketForceSeatModifier.class, PacketForceSeatModifier::encode, PacketForceSeatModifier::decode, PacketForceSeatModifier::handle);
// Items (C2S)
reg(PacketAdjustItem.class, PacketAdjustItem::encode, PacketAdjustItem::decode, PacketAdjustItem::handle);
reg(PacketAdjustRemote.class, PacketAdjustRemote::encode, PacketAdjustRemote::decode, PacketAdjustRemote::handle);
// Slave management
reg(PacketSlaveAction.class, PacketSlaveAction::encode, PacketSlaveAction::decode, PacketSlaveAction::handle);
reg(PacketSlaveItemManage.class, PacketSlaveItemManage::encode, PacketSlaveItemManage::decode, PacketSlaveItemManage::handle);
reg(PacketSlaveBeingFreed.class, PacketSlaveBeingFreed::encode, PacketSlaveBeingFreed::decode, PacketSlaveBeingFreed::handle);
// NPC commands
reg(PacketNpcCommand.class, PacketNpcCommand::encode, PacketNpcCommand::decode, PacketNpcCommand::handle);
reg(PacketOpenCommandWandScreen.class, PacketOpenCommandWandScreen::encode, PacketOpenCommandWandScreen::decode, PacketOpenCommandWandScreen::handle);
reg(PacketRequestNpcInventory.class, PacketRequestNpcInventory::encode, PacketRequestNpcInventory::decode, PacketRequestNpcInventory::handle);
reg(PacketDisciplineAction.class, PacketDisciplineAction::encode, PacketDisciplineAction::decode, PacketDisciplineAction::handle);
// Bounty
reg(PacketRequestBounties.class, PacketRequestBounties::encode, PacketRequestBounties::decode, PacketRequestBounties::handle);
reg(PacketSendBounties.class, PacketSendBounties::encode, PacketSendBounties::decode, PacketSendBounties::handle);
reg(PacketDeleteBounty.class, PacketDeleteBounty::encode, PacketDeleteBounty::decode, PacketDeleteBounty::handle);
// Struggle mini-game
reg(PacketContinuousStruggleState.class, PacketContinuousStruggleState::encode, PacketContinuousStruggleState::decode, PacketContinuousStruggleState::handle);
reg(PacketContinuousStruggleHold.class, PacketContinuousStruggleHold::encode, PacketContinuousStruggleHold::decode, PacketContinuousStruggleHold::handle);
reg(PacketContinuousStruggleStop.class, PacketContinuousStruggleStop::encode, PacketContinuousStruggleStop::decode, PacketContinuousStruggleStop::handle);
// Lockpick mini-game
reg(PacketLockpickMiniGameStart.class, PacketLockpickMiniGameStart::encode, PacketLockpickMiniGameStart::decode, PacketLockpickMiniGameStart::handle);
reg(PacketLockpickMiniGameState.class, PacketLockpickMiniGameState::encode, PacketLockpickMiniGameState::decode, PacketLockpickMiniGameState::handle);
reg(PacketLockpickMiniGameMove.class, PacketLockpickMiniGameMove::encode, PacketLockpickMiniGameMove::decode, PacketLockpickMiniGameMove::handle);
reg(PacketLockpickAttempt.class, PacketLockpickAttempt::encode, PacketLockpickAttempt::decode, PacketLockpickAttempt::handle);
reg(PacketLockpickMiniGameResult.class, PacketLockpickMiniGameResult::encode, PacketLockpickMiniGameResult::decode, PacketLockpickMiniGameResult::handle);
// Merchant trading
reg(PacketOpenMerchantScreen.class, PacketOpenMerchantScreen::encode, PacketOpenMerchantScreen::decode, PacketOpenMerchantScreen::handle);
reg(PacketPurchaseTrade.class, PacketPurchaseTrade::encode, PacketPurchaseTrade::decode, PacketPurchaseTrade::handle);
reg(PacketCloseMerchantScreen.class, PacketCloseMerchantScreen::encode, PacketCloseMerchantScreen::decode, PacketCloseMerchantScreen::handle);
// Slave trader
reg(PacketOpenTraderScreen.class, PacketOpenTraderScreen::encode, PacketOpenTraderScreen::decode, PacketOpenTraderScreen::handle);
reg(PacketBuyCaptive.class, PacketBuyCaptive::encode, PacketBuyCaptive::decode, PacketBuyCaptive::handle);
// Cell management
reg(PacketOpenCellManager.class, PacketOpenCellManager::encode, PacketOpenCellManager::decode, PacketOpenCellManager::handle);
reg(PacketCellAction.class, PacketCellAction::encode, PacketCellAction::decode, PacketCellAction::handle);
reg(PacketRenameCell.class, PacketRenameCell::encode, PacketRenameCell::decode, PacketRenameCell::handle);
reg(PacketAssignCellToCollar.class, PacketAssignCellToCollar::encode, PacketAssignCellToCollar::decode, PacketAssignCellToCollar::handle);
reg(PacketRequestCellList.class, PacketRequestCellList::encode, PacketRequestCellList::decode, PacketRequestCellList::handle);
reg(PacketOpenCellSelector.class, PacketOpenCellSelector::encode, PacketOpenCellSelector::decode, PacketOpenCellSelector::handle);
reg(PacketOpenCoreMenu.class, PacketOpenCoreMenu::encode, PacketOpenCoreMenu::decode, (msg, ctx) -> msg.handle(ctx));
reg(PacketCoreMenuAction.class, PacketCoreMenuAction::encode, PacketCoreMenuAction::decode, PacketCoreMenuAction::handle);
// Conversation
reg(PacketOpenConversation.class, PacketOpenConversation::encode, PacketOpenConversation::decode, PacketOpenConversation::handle);
reg(PacketSelectTopic.class, PacketSelectTopic::encode, PacketSelectTopic::decode, PacketSelectTopic::handle);
reg(PacketEndConversationC2S.class, PacketEndConversationC2S::encode, PacketEndConversationC2S::decode, PacketEndConversationC2S::handle);
reg(PacketEndConversationS2C.class, PacketEndConversationS2C::encode, PacketEndConversationS2C::decode, PacketEndConversationS2C::handle);
reg(PacketRequestConversation.class, PacketRequestConversation::encode, PacketRequestConversation::decode, PacketRequestConversation::handle);
// Master / pet
reg(PacketMasterStateSync.class, PacketMasterStateSync::encode, PacketMasterStateSync::decode, PacketMasterStateSync::handle);
reg(PacketOpenPetRequestMenu.class, PacketOpenPetRequestMenu::encode, PacketOpenPetRequestMenu::decode, PacketOpenPetRequestMenu::handle);
reg(PacketPetRequest.class, PacketPetRequest::encode, PacketPetRequest::decode, PacketPetRequest::handle);
// V2 bondage equipment
reg(PacketSyncV2Equipment.class, PacketSyncV2Equipment::encode, PacketSyncV2Equipment::decode, PacketSyncV2Equipment::handle);
reg(PacketV2SelfRemove.class, PacketV2SelfRemove::encode, PacketV2SelfRemove::decode, PacketV2SelfRemove::handle);
reg(PacketV2StruggleStart.class, PacketV2StruggleStart::encode, PacketV2StruggleStart::decode, PacketV2StruggleStart::handle);
reg(PacketV2LockToggle.class, PacketV2LockToggle::encode, PacketV2LockToggle::decode, PacketV2LockToggle::handle);
reg(PacketV2SelfEquip.class, PacketV2SelfEquip::encode, PacketV2SelfEquip::decode, PacketV2SelfEquip::handle);
reg(PacketV2SelfLock.class, PacketV2SelfLock::encode, PacketV2SelfLock::decode, PacketV2SelfLock::handle);
reg(PacketV2SelfUnlock.class, PacketV2SelfUnlock::encode, PacketV2SelfUnlock::decode, PacketV2SelfUnlock::handle);
reg(PacketMasterEquip.class, PacketMasterEquip::encode, PacketMasterEquip::decode, PacketMasterEquip::handle);
// Furniture
reg(PacketSyncFurnitureState.class, PacketSyncFurnitureState::encode, PacketSyncFurnitureState::decode, PacketSyncFurnitureState::handle);
reg(PacketSyncFurnitureDefinitions.class, PacketSyncFurnitureDefinitions::encode, PacketSyncFurnitureDefinitions::decode, PacketSyncFurnitureDefinitions::handle);
reg(PacketFurnitureLock.class, PacketFurnitureLock::encode, PacketFurnitureLock::decode, PacketFurnitureLock::handle);
reg(PacketFurnitureForcemount.class, PacketFurnitureForcemount::encode, PacketFurnitureForcemount::decode, PacketFurnitureForcemount::handle);
reg(PacketFurnitureEscape.class, PacketFurnitureEscape::encode, PacketFurnitureEscape::decode, PacketFurnitureEscape::handle);
// Movement style
reg(PacketSyncMovementStyle.class, PacketSyncMovementStyle::encode, PacketSyncMovementStyle::decode, PacketSyncMovementStyle::handle);
TiedUpMod.LOGGER.info("Registered {} network packets", packetId);
}
private static <T> void reg(
Class<T> clazz,
BiConsumer<T, FriendlyByteBuf> encoder,
Function<FriendlyByteBuf, T> decoder,
BiConsumer<T, Supplier<NetworkEvent.Context>> handler
) {
CHANNEL.registerMessage(nextId(), clazz, encoder, decoder, handler);
}
private static int nextId() {
return packetId++;
}
public static void sendToPlayer(Object packet, ServerPlayer player) {
CHANNEL.send(PacketDistributor.PLAYER.with(() -> player), packet);
}
public static void sendToAllTracking(Object packet, ServerPlayer player) {
CHANNEL.send(
PacketDistributor.TRACKING_ENTITY.with(() -> player),
packet
);
}
public static void sendToAllTrackingEntity(
Object packet,
net.minecraft.world.entity.Entity entity
) {
CHANNEL.send(
PacketDistributor.TRACKING_ENTITY.with(() -> entity),
packet
);
}
public static void sendToAllTrackingAndSelf(
Object packet,
ServerPlayer player
) {
CHANNEL.send(
PacketDistributor.TRACKING_ENTITY_AND_SELF.with(() -> player),
packet
);
}
public static void sendToServer(Object packet) {
CHANNEL.sendToServer(packet);
}
public static void sendToTracking(
Object packet,
net.minecraft.world.entity.Entity entity
) {
CHANNEL.send(
PacketDistributor.TRACKING_ENTITY.with(() -> entity),
packet
);
}
}

View File

@@ -0,0 +1,444 @@
package com.tiedup.remake.network;
import com.tiedup.remake.compat.mca.MCABondageManager;
import com.tiedup.remake.compat.mca.MCACompat;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.network.sync.PacketSyncBindState;
import com.tiedup.remake.network.sync.PacketSyncCollarRegistry;
import com.tiedup.remake.network.sync.PacketSyncEnslavement;
import com.tiedup.remake.network.sync.PacketSyncStruggleState;
import com.tiedup.remake.network.sync.SyncManager;
import com.tiedup.remake.v2.furniture.EntityFurniture;
import com.tiedup.remake.v2.furniture.ISeatProvider;
import java.util.UUID;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.protocol.game.ClientboundSetPassengersPacket;
import net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.AABB;
import net.minecraftforge.event.entity.player.PlayerEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import org.jetbrains.annotations.Nullable;
/**
* Handles network synchronization for bondage equipment in multiplayer.
*
* <h2>Sync Events</h2>
* <ul>
* <li><b>onStartTracking:</b> When player A enters player B's view range,
* sync B's bondage inventory to A so they see the correct model layers.</li>
* <li><b>onPlayerLoggedIn:</b> Sync all players' states to the newly joined player,
* and sync the new player's state to everyone else.</li>
* </ul>
*
* <h2>Position Sync (MC-262715 Fix)</h2>
* When a player's state changes (freed from leash, etc.), other clients may have
* stale data. We send delayed position/passenger packets to correct this.
*
* @see SyncManager
* @see PacketSyncBindState
*/
@Mod.EventBusSubscriber(modid = TiedUpMod.MOD_ID)
public class NetworkEventHandler {
/**
* Called when a player starts tracking an entity (another player enters their view range).
* We sync the tracked player's bondage inventory to the tracker.
*
* CRITICAL FIX: Also sync riding state and position to fix MC-262715 desync bug.
* When a tracker reconnects, they may have stale data about the tracked player's
* riding status, causing the player to appear frozen.
*/
@SubscribeEvent
public static void onStartTracking(PlayerEvent.StartTracking event) {
if (!(event.getEntity() instanceof ServerPlayer tracker)) return;
// Handle MCA villagers - sync their bondage state to the tracker
if (
event.getTarget() instanceof LivingEntity target &&
MCACompat.isMCALoaded() &&
MCACompat.isMCAVillager(target)
) {
syncMCAVillagerToTracker(target, tracker);
return;
}
// Handle players
if (!(event.getTarget() instanceof Player trackedPlayer)) return;
// Sync tracked player's V2 equipment to the tracker (so they see the bondage layers)
com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper.syncTo(
trackedPlayer, tracker
);
// Also sync state flags
PacketSyncBindState statePacket = PacketSyncBindState.fromPlayer(
trackedPlayer
);
if (statePacket != null) {
ModNetwork.sendToPlayer(statePacket, tracker);
}
// Sync struggle state (needed for animations)
PacketSyncStruggleState strugglePacket =
PacketSyncStruggleState.fromPlayer(trackedPlayer);
if (strugglePacket != null) {
ModNetwork.sendToPlayer(strugglePacket, tracker);
}
// CRITICAL FIX: Sync enslavement state (needed for leash visibility)
PacketSyncEnslavement enslavementPacket =
PacketSyncEnslavement.fromPlayer(trackedPlayer);
if (enslavementPacket != null) {
ModNetwork.sendToPlayer(enslavementPacket, tracker);
}
// FIX MC-262715: Explicitly sync riding state and position
// This fixes the "frozen player" bug when tracker reconnects after
// the tracked player was freed from a vehicle
if (trackedPlayer instanceof ServerPlayer trackedServerPlayer) {
syncRidingStateAndPosition(trackedServerPlayer, tracker);
}
}
/**
* Sync MCA villager's bondage state to a specific tracker.
* Called when a player starts tracking an MCA villager.
* Delegates to MCABondageManager.
*/
private static void syncMCAVillagerToTracker(
LivingEntity villager,
ServerPlayer tracker
) {
MCABondageManager.getInstance().syncBondageStateTo(villager, tracker);
}
/** Delay before sending position sync (in ticks) - allows entity spawn to complete */
private static final int POSITION_SYNC_DELAY = 5;
/**
* Sync the riding state and position of a player to a specific tracker.
*
* This handles edge cases where the tracker has stale data about the tracked player's
* riding status (e.g., tracker reconnects after tracked player was freed from transport).
*
* The delay ensures the entity spawn packet is processed before we send corrections.
*/
private static void syncRidingStateAndPosition(
ServerPlayer trackedPlayer,
ServerPlayer tracker
) {
var server = tracker.getServer();
if (server == null) return;
server.tell(
new net.minecraft.server.TickTask(
server.getTickCount() + POSITION_SYNC_DELAY,
() -> {
if (!trackedPlayer.isAlive() || !tracker.isAlive()) return;
if (
trackedPlayer.isRemoved() || tracker.isRemoved()
) return;
var vehicle = trackedPlayer.getVehicle();
boolean hasValidVehicle =
vehicle != null &&
vehicle.isAlive() &&
!vehicle.isRemoved();
if (!trackedPlayer.isPassenger() || !hasValidVehicle) {
// Not riding - send position to clear stale riding state on client
tracker.connection.send(
new ClientboundTeleportEntityPacket(trackedPlayer)
);
// Fix orphaned passenger state
if (trackedPlayer.isPassenger() && !hasValidVehicle) {
trackedPlayer.stopRiding();
}
} else {
// Riding valid vehicle - sync passenger relationship
tracker.connection.send(
new ClientboundSetPassengersPacket(vehicle)
);
}
}
)
);
}
/**
* Called when a player logs in.
* We sync:
* 1. The player's own inventory to themselves
* 2. The player's inventory to all other players (so they see the new player)
* 3. All other players' inventories to the new player (so they see everyone)
* 4. The player's collar registry (Phase 17)
*/
@SubscribeEvent
public static void onPlayerLoggedIn(PlayerEvent.PlayerLoggedInEvent event) {
if (!(event.getEntity() instanceof ServerPlayer player)) return;
// Sync this player's state to all others, and all others' states to this player
SyncManager.syncAll(player);
SyncManager.syncAllPlayersTo(player);
// Phase 17: Sync collar registry to this player
syncCollarRegistry(player);
// Sync furniture definitions
com.tiedup.remake.v2.furniture.network.PacketSyncFurnitureDefinitions.sendToPlayer(player);
// Check for furniture reconnection (player was locked in a seat and disconnected)
handleFurnitureReconnection(player);
TiedUpMod.LOGGER.debug(
"[Network] Player {} logged in - sync complete",
player.getName().getString()
);
}
/**
* Sync the collar registry to a player.
* Sends all slaves (collar wearers) owned by this player.
*/
private static void syncCollarRegistry(ServerPlayer player) {
var server = player.getServer();
if (server == null) return;
var registry = com.tiedup.remake.state.CollarRegistry.get(server);
if (registry == null) return;
java.util.Set<java.util.UUID> slaves = registry.getSlaves(
player.getUUID()
);
ModNetwork.sendToPlayer(new PacketSyncCollarRegistry(slaves), player);
TiedUpMod.LOGGER.debug(
"[Network] Synced {} slaves to {}",
slaves.size(),
player.getName().getString()
);
}
/**
* Handle furniture reconnection for a player who was locked in a seat when
* they disconnected. Reads the {@code tiedup_locked_furniture} tag from
* persistent data, finds the furniture entity, and re-mounts the player.
*
* <p>The re-mount is deferred by a few ticks to ensure the player's entity
* is fully spawned and the furniture's chunk is loaded.</p>
*
* @param player the player who just logged in
*/
private static void handleFurnitureReconnection(ServerPlayer player) {
CompoundTag persistentData = player.getPersistentData();
if (!persistentData.contains("tiedup_locked_furniture", 10)) return;
CompoundTag tag = persistentData.getCompound("tiedup_locked_furniture");
// Read stored data
if (!tag.contains("x") || !tag.contains("y") || !tag.contains("z")
|| !tag.contains("dim") || !tag.contains("furniture_uuid")
|| !tag.contains("seat_id")) {
TiedUpMod.LOGGER.warn(
"[Network] Malformed furniture reconnection tag for {}, removing",
player.getName().getString()
);
persistentData.remove("tiedup_locked_furniture");
return;
}
int x = tag.getInt("x");
int y = tag.getInt("y");
int z = tag.getInt("z");
String dimStr = tag.getString("dim");
String furnitureUuidStr = tag.getString("furniture_uuid");
String seatId = tag.getString("seat_id");
// Validate furniture UUID
UUID furnitureUuid;
try {
furnitureUuid = UUID.fromString(furnitureUuidStr);
} catch (IllegalArgumentException e) {
TiedUpMod.LOGGER.warn(
"[Network] Invalid furniture UUID '{}' in reconnection tag for {}, removing",
furnitureUuidStr, player.getName().getString()
);
persistentData.remove("tiedup_locked_furniture");
return;
}
// Resolve the dimension
ResourceKey<Level> dimKey = ResourceKey.create(
net.minecraft.core.registries.Registries.DIMENSION,
new ResourceLocation(dimStr)
);
var server = player.getServer();
if (server == null) {
persistentData.remove("tiedup_locked_furniture");
return;
}
ServerLevel targetLevel = server.getLevel(dimKey);
if (targetLevel == null) {
TiedUpMod.LOGGER.warn(
"[Network] Dimension '{}' not found for furniture reconnection, removing tag",
dimStr
);
persistentData.remove("tiedup_locked_furniture");
return;
}
// Defer the re-mount to ensure the player entity is fully spawned
BlockPos furniturePos = new BlockPos(x, y, z);
server.tell(
new net.minecraft.server.TickTask(
server.getTickCount() + FURNITURE_RECONNECT_DELAY,
() -> {
if (!player.isAlive() || player.isRemoved()) {
persistentData.remove("tiedup_locked_furniture");
return;
}
// Search for the furniture entity near the stored position
Entity furniture = findFurnitureEntity(
targetLevel, furniturePos, furnitureUuid
);
if (furniture == null || !(furniture instanceof ISeatProvider provider)) {
TiedUpMod.LOGGER.info(
"[Network] Furniture entity {} not found at {} for reconnection of {}. "
+ "Teleporting player to last furniture position.",
furnitureUuidStr, furniturePos,
player.getName().getString()
);
// Teleport to furniture position to prevent "disconnect to escape"
teleportPlayerTo(player, targetLevel, x + 0.5, y, z + 0.5);
persistentData.remove("tiedup_locked_furniture");
return;
}
// Verify the seat is still locked
if (!provider.isSeatLocked(seatId)) {
TiedUpMod.LOGGER.info(
"[Network] Seat '{}' is no longer locked on furniture {}. Freeing {}.",
seatId, furnitureUuidStr, player.getName().getString()
);
persistentData.remove("tiedup_locked_furniture");
return;
}
// Teleport to furniture dimension/position if needed
if (player.level() != targetLevel || player.distanceToSqr(furniture) > 25.0) {
teleportPlayerTo(
player, targetLevel,
furniture.getX(), furniture.getY(), furniture.getZ()
);
}
// Re-mount the player
boolean mounted = player.startRiding(furniture, true);
if (mounted) {
provider.assignSeat(player, seatId);
TiedUpMod.LOGGER.info(
"[Network] Re-mounted {} in furniture {} seat '{}'",
player.getName().getString(),
furnitureUuidStr, seatId
);
} else {
TiedUpMod.LOGGER.warn(
"[Network] Failed to re-mount {} in furniture {}. Teleporting to position.",
player.getName().getString(), furnitureUuidStr
);
teleportPlayerTo(
player, (ServerLevel) furniture.level(),
furniture.getX(), furniture.getY(), furniture.getZ()
);
persistentData.remove("tiedup_locked_furniture");
}
}
)
);
}
/** Delay before re-mounting on reconnection (in ticks). Allows entity spawn to complete. */
private static final int FURNITURE_RECONNECT_DELAY = 10;
/**
* Teleport a player to a position, handling cross-dimension transfer if needed.
* Uses the same-dimension {@code teleportTo(x, y, z)} for same-level moves,
* and TeleportHelper for cross-dimension moves.
*/
private static void teleportPlayerTo(
ServerPlayer player,
ServerLevel targetLevel,
double x, double y, double z
) {
if (player.serverLevel() == targetLevel) {
player.teleportTo(x, y, z);
} else {
// Cross-dimension: use the project's TeleportHelper for correct handling
com.tiedup.remake.util.teleport.Position pos =
new com.tiedup.remake.util.teleport.Position(x, y, z, targetLevel.dimension());
com.tiedup.remake.util.teleport.TeleportHelper.teleportEntity(player, pos);
}
}
/**
* Search for a furniture entity near the given position, matching the expected UUID.
* Searches a small area around the stored position to account for entity position drift.
*
* @param level the server level to search in
* @param pos the approximate position of the furniture
* @param expectedUuid the UUID of the furniture entity
* @return the entity if found, or null
*/
@Nullable
private static Entity findFurnitureEntity(
ServerLevel level,
BlockPos pos,
UUID expectedUuid
) {
// Search in a small area around the stored position (furniture shouldn't move,
// but entity positions can drift slightly due to floating point)
AABB searchBox = new AABB(pos).inflate(2.0);
java.util.List<EntityFurniture> entities = level.getEntitiesOfClass(
EntityFurniture.class, searchBox,
e -> e.isAlive() && !e.isRemoved()
);
// First, try to find by UUID (most reliable)
for (EntityFurniture entity : entities) {
if (entity.getUUID().equals(expectedUuid)) {
return entity;
}
}
// Fallback: if UUID doesn't match (entity recreated by chunk reload),
// find the nearest furniture at approximately the same position
for (EntityFurniture entity : entities) {
if (entity.blockPosition().equals(pos)) {
TiedUpMod.LOGGER.debug(
"[Network] Furniture UUID mismatch but position matches at {}. "
+ "Using entity {} instead of expected {}.",
pos, entity.getUUID(), expectedUuid
);
return entity;
}
}
return null;
}
}

View File

@@ -0,0 +1,244 @@
package com.tiedup.remake.network;
import com.mojang.logging.LogUtils;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import net.minecraft.server.level.ServerPlayer;
import org.slf4j.Logger;
/**
* Rate limiter for network packets to prevent server abuse.
*
* <p>Uses the Token Bucket algorithm to limit packet rates per player.
* Each player has a bucket of tokens that refills over time. Each packet
* consumes one token. If the bucket is empty, the packet is rejected.
*
* <p>Phase: Server Protection & Performance
*
* <p>This prevents malicious clients from:
* <ul>
* <li>Spamming packets to overload the server</li>
* <li>Creating lag for other players</li>
* <li>Exploiting game mechanics through rapid packet sending</li>
* </ul>
*
* <p>Example usage in packet handlers:
* <pre>{@code
* public void handle(Supplier<NetworkEvent.Context> ctx) {
* ctx.get().enqueueWork(() -> {
* ServerPlayer player = ctx.get().getSender();
* if (player == null) return;
*
* // Rate limit check
* if (!PacketRateLimiter.allowPacket(player, "struggle")) {
* return; // Packet rejected
* }
*
* handleServer(player);
* });
* ctx.get().setPacketHandled(true);
* }
* }</pre>
*/
public class PacketRateLimiter {
private static final Logger LOGGER = LogUtils.getLogger();
/**
* Map of player UUID -> packet type -> token bucket.
* Concurrent to support multi-threaded packet handling.
*/
private static final Map<UUID, Map<String, TokenBucket>> playerBuckets =
new ConcurrentHashMap<>();
/**
* Rate limit configurations for different packet categories.
*
* <p>Categories:
* <ul>
* <li><b>struggle</b>: Struggle keybind spam (5 tokens, 1/sec refill)</li>
* <li><b>minigame</b>: Minigame inputs like QTE or lockpick (20 tokens, 5/sec refill)</li>
* <li><b>action</b>: Player actions like tying/untying (10 tokens, 2/sec refill)</li>
* <li><b>selfbondage</b>: Self-bondage continuous packets (15 tokens, 6/sec refill)</li>
* <li><b>ui</b>: UI interactions like opening screens (3 tokens, 0.5/sec refill)</li>
* <li><b>default</b>: Fallback for uncategorized packets (10 tokens, 1/sec refill)</li>
* </ul>
*/
private static final Map<String, RateLimitConfig> configs = Map.of(
"struggle",
new RateLimitConfig(5, 1.0),
"minigame",
new RateLimitConfig(20, 5.0),
"action",
new RateLimitConfig(10, 2.0),
"selfbondage",
new RateLimitConfig(15, 6.0),
"ui",
new RateLimitConfig(3, 0.5),
"default",
new RateLimitConfig(10, 1.0)
);
/**
* Check if a packet from a player should be allowed.
*
* <p>This method is thread-safe and can be called from packet handlers
* running on different threads.
*
* @param player The player sending the packet
* @param packetType The packet category (struggle, minigame, action, ui)
* @return true if the packet should be processed, false if rate limited
*/
public static boolean allowPacket(ServerPlayer player, String packetType) {
if (player == null) {
return false;
}
UUID playerId = player.getUUID();
// Get or create the bucket for this player + packet type
TokenBucket bucket = playerBuckets
.computeIfAbsent(playerId, k -> new ConcurrentHashMap<>())
.computeIfAbsent(packetType, k -> {
RateLimitConfig config = configs.getOrDefault(
k,
configs.get("default")
);
return new TokenBucket(config);
});
boolean allowed = bucket.tryConsume();
if (!allowed) {
LOGGER.warn(
"Rate limit exceeded for player '{}' on packet type '{}'. " +
"This may indicate packet spam or a malicious client.",
player.getName().getString(),
packetType
);
// Future enhancement: Track violations and kick/ban repeat offenders
// For now, we just log and reject the packet
}
return allowed;
}
/**
* Clean up rate limiter state for a disconnected player.
*
* <p>This should be called when a player logs out to prevent
* the map from growing unbounded.
*
* @param playerId The UUID of the disconnected player
*/
public static void cleanup(UUID playerId) {
Map<String, TokenBucket> removed = playerBuckets.remove(playerId);
if (removed != null) {
LOGGER.debug(
"Cleaned up rate limiter state for player: {}",
playerId
);
}
}
/**
* Get current statistics for a player (for debugging/monitoring).
*
* @param playerId The player UUID
* @param packetType The packet type
* @return String representation of bucket state, or null if not found
*/
public static String getStats(UUID playerId, String packetType) {
Map<String, TokenBucket> buckets = playerBuckets.get(playerId);
if (buckets == null) {
return null;
}
TokenBucket bucket = buckets.get(packetType);
if (bucket == null) {
return null;
}
return String.format(
"Tokens: %.2f/%.0f (refill: %.1f/s)",
bucket.getCurrentTokens(),
bucket.maxTokens,
bucket.refillRate
);
}
/**
* Token Bucket implementation for rate limiting.
*
* <p>The bucket starts full and refills over time. Each packet consumes
* one token. If the bucket is empty, packets are rejected.
*
* <p>This allows for "bursts" of activity (using accumulated tokens)
* while enforcing a long-term rate limit.
*/
private static class TokenBucket {
private final double maxTokens;
private final double refillRate; // tokens per second
private double tokens;
private long lastRefillNanos;
/**
* Create a new token bucket.
*
* @param config The rate limit configuration
*/
TokenBucket(RateLimitConfig config) {
this.maxTokens = config.capacity;
this.refillRate = config.refillRate;
this.tokens = maxTokens; // Start full
this.lastRefillNanos = System.nanoTime();
}
/**
* Try to consume one token from the bucket.
*
* @return true if a token was consumed, false if bucket is empty
*/
synchronized boolean tryConsume() {
refill();
if (tokens >= 1.0) {
tokens -= 1.0;
return true;
}
return false;
}
/**
* Get current token count (for debugging).
*/
synchronized double getCurrentTokens() {
refill();
return tokens;
}
/**
* Refill the bucket based on elapsed time.
*/
private void refill() {
long now = System.nanoTime();
double elapsedSeconds = (now - lastRefillNanos) / 1_000_000_000.0;
// Add tokens based on elapsed time, capped at max capacity
tokens = Math.min(maxTokens, tokens + elapsedSeconds * refillRate);
lastRefillNanos = now;
}
}
/**
* Configuration for a rate limit category.
*
* @param capacity Maximum number of tokens (burst size)
* @param refillRate Number of tokens added per second
*/
private record RateLimitConfig(double capacity, double refillRate) {}
}

View File

@@ -0,0 +1,47 @@
package com.tiedup.remake.network.action;
import com.tiedup.remake.network.base.AbstractProgressPacketWithRole;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.tasks.PlayerStateTask;
import java.util.function.BiConsumer;
import java.util.function.Function;
import net.minecraft.network.FriendlyByteBuf;
/**
* Packet for synchronizing force feeding progress from server to client.
*
* Sent by server to update the client's feeding task progress.
* Includes role info so the progress bar can show different text
* for feeder vs target.
*/
public class PacketForceFeeding extends AbstractProgressPacketWithRole {
public PacketForceFeeding(
int stateInfo,
int maxState,
boolean isActiveRole,
String otherEntityName
) {
super(stateInfo, maxState, isActiveRole, otherEntityName);
}
public static PacketForceFeeding decode(FriendlyByteBuf buf) {
RolePacketData data = decodeRoleFields(buf);
return new PacketForceFeeding(
data.stateInfo(),
data.maxState(),
data.isActiveRole(),
data.otherEntityName()
);
}
@Override
protected Function<PlayerBindState, PlayerStateTask> getTaskGetter() {
return PlayerBindState::getClientFeedingTask;
}
@Override
protected BiConsumer<PlayerBindState, PlayerStateTask> getTaskSetter() {
return PlayerBindState::setClientFeedingTask;
}
}

View File

@@ -0,0 +1,57 @@
package com.tiedup.remake.network.action;
import com.tiedup.remake.events.captivity.ForcedSeatingHandler;
import com.tiedup.remake.network.PacketRateLimiter;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.network.NetworkEvent;
/**
* Packet to sync Force Seat keybind state from client to server.
*
* When the player presses/releases the Force Seat key (default: ALT),
* this packet is sent to update the server-side state.
* ForcedSeatingHandler uses this state to determine if ALT+click
* should mount/dismount captives on vehicles.
*/
public class PacketForceSeatModifier {
private final boolean pressed;
public PacketForceSeatModifier(boolean pressed) {
this.pressed = pressed;
}
public static void encode(
PacketForceSeatModifier packet,
FriendlyByteBuf buf
) {
buf.writeBoolean(packet.pressed);
}
public static PacketForceSeatModifier decode(FriendlyByteBuf buf) {
return new PacketForceSeatModifier(buf.readBoolean());
}
public static void handle(
PacketForceSeatModifier packet,
Supplier<NetworkEvent.Context> ctx
) {
ctx
.get()
.enqueueWork(() -> {
ServerPlayer player = ctx.get().getSender();
if (player != null) {
if (
!PacketRateLimiter.allowPacket(player, "action")
) return;
ForcedSeatingHandler.setForceSeatPressed(
player.getUUID(),
packet.pressed
);
}
});
ctx.get().setPacketHandled(true);
}
}

View File

@@ -0,0 +1,66 @@
package com.tiedup.remake.network.action;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.network.PacketRateLimiter;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.network.NetworkEvent;
/**
* v2.5: Packet to set the knife cut target accessory slot (Client to Server).
*
* When player clicks "Cut" on a locked accessory in StruggleChoiceScreen,
* this packet stores which slot they want to cut. The player must then
* right-click with a knife to start the cutting process.
*/
public class PacketSetKnifeCutTarget {
private final BodyRegionV2 targetRegion;
public PacketSetKnifeCutTarget(BodyRegionV2 targetRegion) {
this.targetRegion = targetRegion;
}
public void encode(FriendlyByteBuf buf) {
buf.writeEnum(targetRegion);
}
public static PacketSetKnifeCutTarget decode(FriendlyByteBuf buf) {
BodyRegionV2 region = buf.readEnum(BodyRegionV2.class);
return new PacketSetKnifeCutTarget(region);
}
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx
.get()
.enqueueWork(() -> {
ServerPlayer player = ctx.get().getSender();
if (player == null) {
return;
}
handleServer(player);
});
ctx.get().setPacketHandled(true);
}
private void handleServer(ServerPlayer player) {
if (!PacketRateLimiter.allowPacket(player, "action")) return;
PlayerBindState state = PlayerBindState.getInstance(player);
if (state == null) {
return;
}
// Direct V2: PlayerBindState now uses BodyRegionV2 natively
state.setKnifeCutTarget(targetRegion);
TiedUpMod.LOGGER.debug(
"[PacketSetKnifeCutTarget] {} set knife cut target to region {}",
player.getName().getString(),
targetRegion
);
}
}

View File

@@ -0,0 +1,99 @@
package com.tiedup.remake.network.action;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.state.PlayerBindState;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.network.NetworkEvent;
/**
* Phase 7: Packet for struggling (Client to Server).
*
* Based on original PacketStruggleServer from 1.12.2
*
* Sent by client when player presses the struggle keybind.
* No payload needed - just signals the server that the player wants to struggle.
*/
public class PacketStruggle {
/**
* Empty packet - no data needed.
*/
public PacketStruggle() {
// Empty packet
}
/**
* Encode the packet to the network buffer.
* Nothing to encode for this packet.
*
* @param buf The buffer to write to
*/
public void encode(FriendlyByteBuf buf) {
// Empty - no data
}
/**
* Decode the packet from the network buffer.
* Nothing to decode for this packet.
*
* @param buf The buffer to read from
* @return The decoded packet
*/
public static PacketStruggle decode(FriendlyByteBuf buf) {
return new PacketStruggle();
}
/**
* Handle the packet on the receiving side (SERVER SIDE).
*
* @param ctx The network context
*/
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx
.get()
.enqueueWork(() -> {
// This runs on the SERVER side only
ServerPlayer player = ctx.get().getSender();
if (player == null) {
return;
}
// Rate limiting: Prevent struggle spam
if (
!com.tiedup.remake.network.PacketRateLimiter.allowPacket(
player,
"struggle"
)
) {
return;
}
handleServer(player);
});
ctx.get().setPacketHandled(true);
}
/**
* Handle the packet on the server side.
* Calls the player's struggle() method.
*
* Based on original PacketStruggleServer handler
*
* @param player The player who sent the packet
*/
private void handleServer(ServerPlayer player) {
PlayerBindState state = PlayerBindState.getInstance(player);
if (state == null) {
TiedUpMod.LOGGER.warn(
"[PACKET] PacketStruggle received but PlayerBindState is null for {}",
player.getName().getString()
);
return;
}
// Call struggle
state.struggle();
}
}

View File

@@ -0,0 +1,207 @@
package com.tiedup.remake.network.action;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.network.PacketRateLimiter;
import com.tiedup.remake.state.IRestrainable;
import com.tiedup.remake.state.ICaptor;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.util.KidnappedHelper;
import java.util.List;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.network.NetworkEvent;
/**
* Phase 7: Packet for tightening binds (Client to Server).
*
* Based on original PacketTightenBinds from 1.12.2
*
* Sent by client when player wants to tighten a nearby tied player's binds.
* No payload needed - server will find the nearest tied player the sender is looking at.
*/
public class PacketTighten {
/**
* Empty packet - no data needed.
*/
public PacketTighten() {
// Empty packet
}
/**
* Encode the packet to the network buffer.
* Nothing to encode for this packet.
*
* @param buf The buffer to write to
*/
public void encode(FriendlyByteBuf buf) {
// Empty - no data
}
/**
* Decode the packet from the network buffer.
* Nothing to decode for this packet.
*
* @param buf The buffer to read from
* @return The decoded packet
*/
public static PacketTighten decode(FriendlyByteBuf buf) {
return new PacketTighten();
}
/**
* Handle the packet on the receiving side (SERVER SIDE).
*
* @param ctx The network context
*/
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx
.get()
.enqueueWork(() -> {
// This runs on the SERVER side only
ServerPlayer tightener = ctx.get().getSender();
if (tightener == null) {
return;
}
handleServer(tightener);
});
ctx.get().setPacketHandled(true);
}
/**
* Handle the packet on the server side.
* Finds the nearest tied entity (player or NPC) the tightener is looking at
* and tightens their binds.
*
* Based on original PacketTightenBinds handler
*
* @param tightener The player who sent the packet
*/
private void handleServer(ServerPlayer tightener) {
if (!PacketRateLimiter.allowPacket(tightener, "action")) return;
// Find nearby tied entities within 5 blocks
Vec3 eyePos = tightener.getEyePosition();
Vec3 lookVec = tightener.getLookAngle();
double maxDistance = 5.0;
AABB searchBox = new AABB(
eyePos.x - maxDistance,
eyePos.y - maxDistance,
eyePos.z - maxDistance,
eyePos.x + maxDistance,
eyePos.y + maxDistance,
eyePos.z + maxDistance
);
// Search for all LivingEntities (Players + NPCs)
List<LivingEntity> nearbyEntities = tightener
.level()
.getEntitiesOfClass(
LivingEntity.class,
searchBox,
entity -> entity != tightener && entity.isAlive()
);
// Find the closest entity the tightener is looking at
LivingEntity closestTarget = null;
IRestrainable closestState = null;
double closestDistance = maxDistance;
for (LivingEntity candidate : nearbyEntities) {
// Get kidnapped state (works for Players and NPCs)
IRestrainable candidateState = KidnappedHelper.getKidnappedState(
candidate
);
if (candidateState == null || !candidateState.isTiedUp()) {
continue; // Only tighten tied entities
}
// Check if tightener is looking at this entity
Vec3 toCandidate = candidate
.position()
.add(0, candidate.getBbHeight() / 2, 0)
.subtract(eyePos);
double distance = toCandidate.length();
if (distance > maxDistance) {
continue;
}
// Check angle - must be looking roughly at the entity
Vec3 toCandidateNorm = toCandidate.normalize();
double dot = lookVec.dot(toCandidateNorm);
if (dot > 0.9 && distance < closestDistance) {
// ~25 degree cone
closestTarget = candidate;
closestState = candidateState;
closestDistance = distance;
}
}
if (closestTarget != null && closestState != null) {
// Security: Verify sender has permission to tighten
// Must be captor of target, collar owner, or admin
boolean hasPermission = false;
// Check if sender is captor
if (closestState.isCaptive()) {
ICaptor captor = closestState.getCaptor();
if (
captor != null &&
captor.getEntity() != null &&
captor.getEntity().getUUID().equals(tightener.getUUID())
) {
hasPermission = true;
}
}
// Check if sender owns collar
if (!hasPermission && closestState.hasCollar()) {
var collarStack = closestState.getEquipment(BodyRegionV2.NECK);
if (
collarStack.getItem() instanceof
com.tiedup.remake.items.base.ItemCollar collar
) {
if (collar.isOwner(collarStack, tightener)) {
hasPermission = true;
}
}
}
// Check if sender is admin
if (!hasPermission && tightener.hasPermissions(2)) {
hasPermission = true;
}
if (!hasPermission) {
TiedUpMod.LOGGER.debug(
"[PACKET] {} tried to tighten {} but has no permission",
tightener.getName().getString(),
closestTarget.getName().getString()
);
return;
}
closestState.tighten(tightener);
TiedUpMod.LOGGER.info(
"[PACKET] {} tightened {}'s binds",
tightener.getName().getString(),
closestTarget.getName().getString()
);
} else {
TiedUpMod.LOGGER.debug(
"[PACKET] {} tried to tighten but no valid target found",
tightener.getName().getString()
);
}
}
}

View File

@@ -0,0 +1,58 @@
package com.tiedup.remake.network.action;
import com.tiedup.remake.network.base.AbstractProgressPacketWithRole;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.tasks.PlayerStateTask;
import java.util.function.BiConsumer;
import java.util.function.Function;
import net.minecraft.network.FriendlyByteBuf;
/**
* Packet for synchronizing tying progress from server to client.
*
* Sent by server to update the client's tying task progress.
* Includes role info so the progress bar can show different text
* for kidnapper vs victim.
*/
public class PacketTying extends AbstractProgressPacketWithRole {
/**
* Create a new tying progress packet with role info.
*
* @param stateInfo Current elapsed time in seconds (-1 for completion/cancel)
* @param maxState Total duration in seconds
* @param isKidnapper true if the recipient is doing the tying
* @param otherEntityName Name of the other party
*/
public PacketTying(
int stateInfo,
int maxState,
boolean isKidnapper,
String otherEntityName
) {
super(stateInfo, maxState, isKidnapper, otherEntityName);
}
/**
* Decode the packet from the network buffer.
*/
public static PacketTying decode(FriendlyByteBuf buf) {
RolePacketData data = decodeRoleFields(buf);
return new PacketTying(
data.stateInfo(),
data.maxState(),
data.isActiveRole(),
data.otherEntityName()
);
}
@Override
protected Function<PlayerBindState, PlayerStateTask> getTaskGetter() {
return PlayerBindState::getClientTyingTask;
}
@Override
protected BiConsumer<PlayerBindState, PlayerStateTask> getTaskSetter() {
return PlayerBindState::setClientTyingTask;
}
}

View File

@@ -0,0 +1,58 @@
package com.tiedup.remake.network.action;
import com.tiedup.remake.network.base.AbstractProgressPacketWithRole;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.tasks.PlayerStateTask;
import java.util.function.BiConsumer;
import java.util.function.Function;
import net.minecraft.network.FriendlyByteBuf;
/**
* Packet for synchronizing untying progress from server to client.
*
* Sent by server to update the client's untying task progress.
* Includes role info so the progress bar can show different text
* for helper vs victim.
*/
public class PacketUntying extends AbstractProgressPacketWithRole {
/**
* Create a new untying progress packet with role info.
*
* @param stateInfo Current elapsed time (-1 for completion/cancel)
* @param maxState Total duration
* @param isHelper true if the recipient is doing the untying
* @param otherEntityName Name of the other party
*/
public PacketUntying(
int stateInfo,
int maxState,
boolean isHelper,
String otherEntityName
) {
super(stateInfo, maxState, isHelper, otherEntityName);
}
/**
* Decode the packet from the network buffer.
*/
public static PacketUntying decode(FriendlyByteBuf buf) {
RolePacketData data = decodeRoleFields(buf);
return new PacketUntying(
data.stateInfo(),
data.maxState(),
data.isActiveRole(),
data.otherEntityName()
);
}
@Override
protected Function<PlayerBindState, PlayerStateTask> getTaskGetter() {
return PlayerBindState::getClientUntyingTask;
}
@Override
protected BiConsumer<PlayerBindState, PlayerStateTask> getTaskSetter() {
return PlayerBindState::setClientUntyingTask;
}
}

View File

@@ -0,0 +1,99 @@
package com.tiedup.remake.network.armorstand;
import com.tiedup.remake.entities.armorstand.ArmorStandBondageClientCache;
import com.tiedup.remake.network.base.AbstractClientPacket;
import com.tiedup.remake.v2.BodyRegionV2;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
/**
* Packet to synchronize armor stand bondage data to clients.
*
* Direction: Server -> Client (S2C)
*
* Sent when:
* - A bondage item is equipped to an armor stand
* - A bondage item is removed from an armor stand
* - A player joins and armor stands with bondage items are loaded
*
* <p>Wire format (Epic 7D): uses {@link BodyRegionV2} enum for region identification.
*/
public class PacketSyncArmorStandBondage extends AbstractClientPacket {
private final int entityId;
private final BodyRegionV2 region;
private final ItemStack item;
/**
* Create a sync packet for a single slot update.
*
* @param entityId The armor stand entity ID
* @param region The body region
* @param item The item in the slot (empty to clear)
*/
public PacketSyncArmorStandBondage(
int entityId,
BodyRegionV2 region,
ItemStack item
) {
this.entityId = entityId;
this.region = region;
this.item = item != null ? item : ItemStack.EMPTY;
}
/**
* Encode the packet to a byte buffer.
*/
public void encode(FriendlyByteBuf buf) {
buf.writeVarInt(entityId);
buf.writeEnum(region);
if (item.isEmpty()) {
buf.writeBoolean(false);
} else {
buf.writeBoolean(true);
buf.writeNbt(item.save(new CompoundTag()));
}
}
/**
* Decode the packet from a byte buffer.
*/
public static PacketSyncArmorStandBondage decode(FriendlyByteBuf buf) {
int entityId = buf.readVarInt();
BodyRegionV2 region = buf.readEnum(BodyRegionV2.class);
ItemStack item;
if (buf.readBoolean()) {
CompoundTag tag = buf.readNbt();
item = tag != null ? ItemStack.of(tag) : ItemStack.EMPTY;
} else {
item = ItemStack.EMPTY;
}
return new PacketSyncArmorStandBondage(entityId, region, item);
}
@Override
@OnlyIn(Dist.CLIENT)
protected void handleClientImpl() {
// Update the client-side cache
ArmorStandBondageClientCache.updateItem(entityId, region, item);
}
// Getters for debugging
public int getEntityId() {
return entityId;
}
public BodyRegionV2 getRegion() {
return region;
}
public ItemStack getItem() {
return item;
}
}

View File

@@ -0,0 +1,47 @@
package com.tiedup.remake.network.base;
import java.util.function.Supplier;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.fml.loading.FMLEnvironment;
import net.minecraftforge.network.NetworkEvent;
/**
* Abstract base class for client-bound packets.
*
* Provides the standard handle() implementation that:
* 1. Enqueues work to the main thread
* 2. Checks for client distribution
* 3. Calls the abstract handleClientImpl()
*
* Subclasses must implement:
* - encode(FriendlyByteBuf) - serialize packet data
* - decode(FriendlyByteBuf) - static, deserialize packet data
* - handleClientImpl() - client-side logic
*/
public abstract class AbstractClientPacket {
/**
* Handle the packet on the receiving side (client).
* This runs on the network thread, so we enqueue to main thread.
*
* @param ctx The network context
*/
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx
.get()
.enqueueWork(() -> {
if (FMLEnvironment.dist == Dist.CLIENT) {
handleClientImpl();
}
});
ctx.get().setPacketHandled(true);
}
/**
* Client-side packet handling implementation.
* Called on the main client thread after the packet is received.
*/
@OnlyIn(Dist.CLIENT)
protected abstract void handleClientImpl();
}

View File

@@ -0,0 +1,105 @@
package com.tiedup.remake.network.base;
import java.util.UUID;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.entity.player.Player;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
/**
* Abstract base class for player sync packets (Server → Client).
*
* Extends AbstractClientPacket with player UUID lookup logic.
* Used for packets that sync data for a specific player.
*
* Subclasses must implement:
* - encode(FriendlyByteBuf) - serialize packet data (should call super.encodeUUID first)
* - decode(FriendlyByteBuf) - static, deserialize packet data
* - applySync(Player) - apply the sync to the player
* - queueForRetry() - optional, queue for SyncManager retry if player not loaded
*/
public abstract class AbstractPlayerSyncPacket extends AbstractClientPacket {
protected final UUID playerUUID;
/**
* Create a new player sync packet.
*
* @param playerUUID The target player's UUID
*/
protected AbstractPlayerSyncPacket(UUID playerUUID) {
this.playerUUID = playerUUID;
}
/**
* Get the target player's UUID.
*/
public UUID getPlayerUUID() {
return playerUUID;
}
/**
* Encode the player UUID to the buffer.
* Subclasses should call this in their encode() method.
*
* @param buf The buffer to write to
*/
protected void encodeUUID(FriendlyByteBuf buf) {
buf.writeUUID(playerUUID);
}
/**
* Decode a player UUID from the buffer.
* Subclasses should call this in their static decode() method.
*
* @param buf The buffer to read from
* @return The decoded UUID
*/
protected static UUID decodeUUID(FriendlyByteBuf buf) {
return buf.readUUID();
}
/**
* Client-side packet handling implementation.
* Looks up the player by UUID and calls applySync if found.
*/
@Override
@OnlyIn(Dist.CLIENT)
protected void handleClientImpl() {
if (net.minecraft.client.Minecraft.getInstance().level == null) {
return;
}
Player player =
net.minecraft.client.Minecraft.getInstance().level.getPlayerByUUID(
playerUUID
);
if (player == null) {
// Player not loaded yet - subclass may queue for retry
queueForRetry();
return;
}
applySync(player);
}
/**
* Apply the sync to the target player.
* Called when the player is found in the client world.
*
* @param player The target player
*/
@OnlyIn(Dist.CLIENT)
protected abstract void applySync(Player player);
/**
* Queue this packet for retry via SyncManager.
* Called when the player is not yet loaded.
* Default implementation does nothing (ignore).
* Subclasses can override to queue for retry.
*/
@OnlyIn(Dist.CLIENT)
protected void queueForRetry() {
// Default: do nothing (packet is non-critical or will be re-sent)
}
}

View File

@@ -0,0 +1,157 @@
package com.tiedup.remake.network.base;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.tasks.PlayerStateTask;
import java.util.function.BiConsumer;
import java.util.function.Function;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
/**
* Phase 2 Refactoring: Abstract base class for progress packets.
*
* Eliminates code duplication between PacketTying and PacketUntying.
* Both packets share identical structure for encoding/decoding/handling,
* differing only in which client task they update.
*
* Subclasses only need to implement:
* - decode() - static method returning the concrete type
* - getTaskGetter() - returns the function to get the task from PlayerBindState
* - getTaskSetter() - returns the function to set the task on PlayerBindState
* - calculateElapsed() - how to calculate elapsed time from stateInfo
*/
public abstract class AbstractProgressPacket extends AbstractClientPacket {
protected final int stateInfo; // Current time value (-1 = done/cancelled)
protected final int maxState; // Total duration
/**
* Create a new progress packet.
*
* @param stateInfo Current time value (-1 for completion/cancel)
* @param maxState Total duration in seconds
*/
protected AbstractProgressPacket(int stateInfo, int maxState) {
this.stateInfo = stateInfo;
this.maxState = maxState;
}
/**
* Get the current state info value.
*/
public int getStateInfo() {
return stateInfo;
}
/**
* Get the max state value.
*/
public int getMaxState() {
return maxState;
}
/**
* Encode the packet to the network buffer.
*
* @param buf The buffer to write to
*/
public void encode(FriendlyByteBuf buf) {
buf.writeInt(stateInfo);
buf.writeInt(maxState);
}
/**
* Static helper to decode common fields from buffer.
* Subclasses should call this in their static decode() method.
*
* @param buf The buffer to read from
* @return Array of [stateInfo, maxState]
*/
protected static int[] decodeFields(FriendlyByteBuf buf) {
int stateInfo = buf.readInt();
int maxState = buf.readInt();
return new int[] { stateInfo, maxState };
}
/**
* Client-side packet handling from AbstractClientPacket.
* This method is stripped from dedicated server builds.
* Contains references to client-only classes (Minecraft, LocalPlayer).
*/
@Override
@OnlyIn(Dist.CLIENT)
protected void handleClientImpl() {
net.minecraft.client.player.LocalPlayer player =
net.minecraft.client.Minecraft.getInstance().player;
if (player == null) {
return;
}
PlayerBindState playerState = PlayerBindState.getInstance(player);
if (playerState == null) {
return;
}
handleProgressUpdate(playerState);
}
/**
* Implementation of progress update logic.
* Subclasses can override to customize task creation (e.g., with role info).
*
* @param playerState The player's bind state
*/
@OnlyIn(Dist.CLIENT)
protected void handleProgressUpdate(PlayerBindState playerState) {
if (stateInfo == -1) {
// Task completed or cancelled - clear client-side state
getTaskSetter().accept(playerState, null);
} else {
// Update or create client-side task
PlayerStateTask task = getTaskGetter().apply(playerState);
if (task == null || task.isOutdated()) {
// Create new task state
task = new PlayerStateTask(maxState);
getTaskSetter().accept(playerState, task);
}
// Update progress
task.update(calculateElapsed());
}
}
/**
* Get the function to retrieve the task from PlayerBindState.
* Subclasses must implement this.
*
* @return Function that gets the task from PlayerBindState
*/
protected abstract Function<
PlayerBindState,
PlayerStateTask
> getTaskGetter();
/**
* Get the function to set the task on PlayerBindState.
* Subclasses must implement this.
*
* @return BiConsumer that sets the task on PlayerBindState
*/
protected abstract BiConsumer<
PlayerBindState,
PlayerStateTask
> getTaskSetter();
/**
* Calculate the elapsed time value to pass to task.update().
* Default implementation returns stateInfo directly.
* Subclasses can override for different calculation (e.g., untying uses maxState - stateInfo).
*
* @return The elapsed time value
*/
protected int calculateElapsed() {
return stateInfo;
}
}

View File

@@ -0,0 +1,121 @@
package com.tiedup.remake.network.base;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.tasks.PlayerStateTask;
import java.util.function.BiConsumer;
import java.util.function.Function;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
/**
* Extended progress packet that includes role information.
*
* Used for tying/untying packets where we need to show different
* progress text based on whether the player is the active party
* (kidnapper/helper) or the passive party (victim).
*
* Subclasses only need to implement:
* - decode() - static method returning the concrete type
* - getTaskGetter() - returns the function to get the task from PlayerBindState
* - getTaskSetter() - returns the function to set the task on PlayerBindState
*/
public abstract class AbstractProgressPacketWithRole
extends AbstractProgressPacket
{
protected final boolean isActiveRole;
protected final String otherEntityName;
/**
* Create a new progress packet with role info.
*
* @param stateInfo Current time value (-1 for completion/cancel)
* @param maxState Total duration in seconds
* @param isActiveRole true if recipient is the active party (kidnapper/helper)
* @param otherEntityName Name of the other party
*/
protected AbstractProgressPacketWithRole(
int stateInfo,
int maxState,
boolean isActiveRole,
String otherEntityName
) {
super(stateInfo, maxState);
this.isActiveRole = isActiveRole;
this.otherEntityName = otherEntityName != null ? otherEntityName : "";
}
public boolean isActiveRole() {
return isActiveRole;
}
public String getOtherEntityName() {
return otherEntityName;
}
@Override
public void encode(FriendlyByteBuf buf) {
super.encode(buf);
buf.writeBoolean(isActiveRole);
buf.writeUtf(otherEntityName);
}
/**
* Static helper to decode all fields from buffer.
* Subclasses should call this in their static decode() method.
*
* @param buf The buffer to read from
* @return RolePacketData with all decoded fields
*/
protected static RolePacketData decodeRoleFields(FriendlyByteBuf buf) {
int stateInfo = buf.readInt();
int maxState = buf.readInt();
boolean isActiveRole = buf.readBoolean();
String otherEntityName = buf.readUtf();
return new RolePacketData(
stateInfo,
maxState,
isActiveRole,
otherEntityName
);
}
/**
* Data container for decoded packet fields.
*/
protected record RolePacketData(
int stateInfo,
int maxState,
boolean isActiveRole,
String otherEntityName
) {}
/**
* Override to create task with role info.
*/
@Override
@OnlyIn(Dist.CLIENT)
protected void handleProgressUpdate(PlayerBindState playerState) {
if (stateInfo == -1) {
// Task completed or cancelled - clear client-side state
getTaskSetter().accept(playerState, null);
} else {
// Update or create client-side task
PlayerStateTask task = getTaskGetter().apply(playerState);
if (task == null || task.isOutdated()) {
// Create new task state WITH ROLE INFO
task = new PlayerStateTask(
maxState,
isActiveRole,
otherEntityName
);
getTaskSetter().accept(playerState, task);
}
// Update progress
task.update(calculateElapsed());
}
}
}

View File

@@ -0,0 +1,64 @@
package com.tiedup.remake.network.bounty;
import com.tiedup.remake.bounty.BountyManager;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.network.PacketRateLimiter;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.network.NetworkEvent;
/**
* Packet: Client requests to delete/cancel a bounty.
*
* Phase 17: Bounty System
*
* Only the bounty client or an admin can delete.
* If client deletes, reward is returned.
*/
public class PacketDeleteBounty {
private final String bountyId;
public PacketDeleteBounty(String bountyId) {
this.bountyId = bountyId;
}
public void encode(FriendlyByteBuf buf) {
buf.writeUtf(bountyId);
}
public static PacketDeleteBounty decode(FriendlyByteBuf buf) {
return new PacketDeleteBounty(buf.readUtf(256));
}
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx
.get()
.enqueueWork(() -> {
ServerPlayer player = ctx.get().getSender();
if (player == null) return;
if (!PacketRateLimiter.allowPacket(player, "action")) return;
BountyManager manager = BountyManager.get(player.serverLevel());
boolean success = manager.cancelBounty(player, bountyId);
if (!success) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
"Cannot delete this bounty."
);
}
TiedUpMod.LOGGER.debug(
"[BOUNTY] Delete request from {}: bounty={}, success={}",
player.getName().getString(),
bountyId,
success
);
});
ctx.get().setPacketHandled(true);
}
}

View File

@@ -0,0 +1,54 @@
package com.tiedup.remake.network.bounty;
import com.tiedup.remake.bounty.Bounty;
import com.tiedup.remake.bounty.BountyManager;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.PacketRateLimiter;
import java.util.List;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.network.NetworkEvent;
/**
* Packet: Client requests bounty list from server.
*
* Phase 17: Bounty System
*
* Flow: Client → Server → PacketSendBounties → Client
*/
public class PacketRequestBounties {
public PacketRequestBounties() {}
public void encode(FriendlyByteBuf buf) {
// No data needed
}
public static PacketRequestBounties decode(FriendlyByteBuf buf) {
return new PacketRequestBounties();
}
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx
.get()
.enqueueWork(() -> {
ServerPlayer player = ctx.get().getSender();
if (player == null) return;
if (!PacketRateLimiter.allowPacket(player, "ui")) return;
BountyManager manager = BountyManager.get(player.serverLevel());
List<Bounty> bounties = manager.getBounties(
player.serverLevel()
);
boolean isAdmin = player.hasPermissions(2);
// Send bounty list back to client
ModNetwork.sendToPlayer(
new PacketSendBounties(bounties, isAdmin),
player
);
});
ctx.get().setPacketHandled(true);
}
}

View File

@@ -0,0 +1,82 @@
package com.tiedup.remake.network.bounty;
import com.tiedup.remake.bounty.Bounty;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.fml.loading.FMLEnvironment;
import net.minecraftforge.network.NetworkEvent;
/**
* Packet: Server sends bounty list to client.
*
* Phase 17: Bounty System
*
* Flow: Server → Client (opens BountyListScreen)
*/
public class PacketSendBounties {
private final List<Bounty> bounties;
private final boolean isAdmin;
public PacketSendBounties(List<Bounty> bounties, boolean isAdmin) {
this.bounties = bounties;
this.isAdmin = isAdmin;
}
public void encode(FriendlyByteBuf buf) {
buf.writeBoolean(isAdmin);
// Serialize bounties as NBT list
ListTag listTag = new ListTag();
for (Bounty bounty : bounties) {
listTag.add(bounty.save());
}
CompoundTag wrapper = new CompoundTag();
wrapper.put("bounties", listTag);
buf.writeNbt(wrapper);
}
public static PacketSendBounties decode(FriendlyByteBuf buf) {
boolean isAdmin = buf.readBoolean();
CompoundTag wrapper = buf.readNbt();
List<Bounty> bounties = new ArrayList<>();
if (wrapper != null && wrapper.contains("bounties")) {
ListTag listTag = wrapper.getList("bounties", Tag.TAG_COMPOUND);
for (int i = 0; i < listTag.size(); i++) {
bounties.add(Bounty.load(listTag.getCompound(i)));
}
}
return new PacketSendBounties(bounties, isAdmin);
}
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx
.get()
.enqueueWork(() -> {
if (FMLEnvironment.dist == Dist.CLIENT) {
handleClient();
}
});
ctx.get().setPacketHandled(true);
}
@OnlyIn(Dist.CLIENT)
private void handleClient() {
net.minecraft.client.Minecraft.getInstance().setScreen(
new com.tiedup.remake.client.gui.screens.BountyListScreen(
bounties,
isAdmin
)
);
}
}

View File

@@ -0,0 +1,212 @@
package com.tiedup.remake.network.cell;
import com.tiedup.remake.cells.CellDataV2;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.cells.CellRegistryV2;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.EntityDamsel;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.network.PacketRateLimiter;
import com.tiedup.remake.network.sync.SyncManager;
import com.tiedup.remake.personality.PersonalityState;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.KidnappedHelper;
import java.util.UUID;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.network.NetworkEvent;
/**
* Packet to assign (or clear) a cell on a collar.
* Client -> Server
*/
public class PacketAssignCellToCollar {
private final UUID targetEntityUUID; // The entity wearing the collar
private final UUID cellId; // The cell to assign (null = clear)
public PacketAssignCellToCollar(UUID targetEntityUUID, UUID cellId) {
this.targetEntityUUID = targetEntityUUID;
this.cellId = cellId;
}
public static void encode(
PacketAssignCellToCollar msg,
FriendlyByteBuf buf
) {
buf.writeUUID(msg.targetEntityUUID);
buf.writeBoolean(msg.cellId != null);
if (msg.cellId != null) {
buf.writeUUID(msg.cellId);
}
}
public static PacketAssignCellToCollar decode(FriendlyByteBuf buf) {
UUID targetEntityUUID = buf.readUUID();
UUID cellId = buf.readBoolean() ? buf.readUUID() : null;
return new PacketAssignCellToCollar(targetEntityUUID, cellId);
}
public static void handle(
PacketAssignCellToCollar msg,
Supplier<NetworkEvent.Context> ctx
) {
ctx
.get()
.enqueueWork(() -> {
ServerPlayer sender = ctx.get().getSender();
if (sender == null) return;
// MEDIUM FIX: Rate limiting to prevent cell assignment spam
if (!PacketRateLimiter.allowPacket(sender, "action")) {
return;
}
// Find target entity
LivingEntity target = findEntity(sender, msg.targetEntityUUID);
if (target == null) {
TiedUpMod.LOGGER.debug(
"[PacketAssignCellToCollar] Target not found: {}",
msg.targetEntityUUID
);
return;
}
// Check distance (max 10 blocks)
if (sender.distanceTo(target) > 10.0) {
TiedUpMod.LOGGER.debug(
"[PacketAssignCellToCollar] Target too far"
);
return;
}
// Get target's kidnapped state
IBondageState state = KidnappedHelper.getKidnappedState(target);
if (state == null || !state.hasCollar()) {
TiedUpMod.LOGGER.debug(
"[PacketAssignCellToCollar] Target has no collar"
);
return;
}
ItemStack collarStack = state.getEquipment(BodyRegionV2.NECK);
if (!(collarStack.getItem() instanceof ItemCollar collar)) {
TiedUpMod.LOGGER.debug(
"[PacketAssignCellToCollar] Invalid collar item"
);
return;
}
// Security: Verify sender owns the collar (or is admin)
if (
!collar.isOwner(collarStack, sender) &&
!sender.hasPermissions(2)
) {
TiedUpMod.LOGGER.debug(
"[PacketAssignCellToCollar] Sender is not collar owner"
);
return;
}
// If assigning a cell, verify ownership
CellDataV2 cell = null;
if (msg.cellId != null) {
CellRegistryV2 registry = CellRegistryV2.get(
sender.serverLevel()
);
cell = registry.getCell(msg.cellId);
if (cell == null) {
TiedUpMod.LOGGER.debug(
"[PacketAssignCellToCollar] Cell not found"
);
return;
}
if (
!cell.isOwnedBy(sender.getUUID()) &&
!sender.hasPermissions(2)
) {
TiedUpMod.LOGGER.debug(
"[PacketAssignCellToCollar] Sender doesn't own the cell"
);
return;
}
}
// Set the cell ID on the collar
collar.setCellId(collarStack, msg.cellId);
// Sync PersonalityState for damsels
if (target instanceof EntityDamsel damsel) {
PersonalityState pState = damsel.getPersonalityState();
if (pState != null) {
if (msg.cellId != null && cell != null) {
pState.assignCell(
msg.cellId,
cell,
damsel.getUUID()
);
} else {
pState.unassignCell();
}
}
}
// Sync changes
if (target instanceof ServerPlayer targetPlayer) {
SyncManager.syncAll(targetPlayer);
}
// Send feedback
if (msg.cellId != null) {
String cellName =
cell != null && cell.getName() != null
? cell.getName()
: msg.cellId.toString().substring(0, 8);
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.INFO,
"Cell '" + cellName + "' assigned to collar"
);
} else {
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.INFO,
"Cell cleared from collar"
);
}
TiedUpMod.LOGGER.info(
"[PacketAssignCellToCollar] {} {} cell on {}'s collar",
sender.getName().getString(),
msg.cellId != null ? "assigned" : "cleared",
target.getName().getString()
);
});
ctx.get().setPacketHandled(true);
}
private static LivingEntity findEntity(ServerPlayer sender, UUID entityId) {
// Try player first
Player player = sender.level().getPlayerByUUID(entityId);
if (player != null) return player;
// Search nearby entities
var searchBox = sender.getBoundingBox().inflate(64);
for (LivingEntity entity : sender
.level()
.getEntitiesOfClass(LivingEntity.class, searchBox)) {
if (entity.getUUID().equals(entityId)) {
return entity;
}
}
return null;
}
}

View File

@@ -0,0 +1,437 @@
package com.tiedup.remake.network.cell;
import com.tiedup.remake.blocks.BlockMarker;
import com.tiedup.remake.cells.CellDataV2;
import com.tiedup.remake.cells.CellRegistryV2;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.TiedUpMod;
import java.util.UUID;
import java.util.function.Supplier;
import net.minecraft.core.BlockPos;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraftforge.network.NetworkEvent;
/**
* Packet for cell management actions.
* Client -> Server
*/
public class PacketCellAction {
/**
* Action to perform on the cell/prisoner.
*/
public enum Action {
RELEASE, // Release a prisoner from cell
TRANSFER, // Transfer a prisoner to another cell
TELEPORT, // Teleport prisoner to cell
DELETE_CELL, // Delete the cell
}
private final Action action;
private final UUID cellId;
private final UUID prisonerId; // For RELEASE/TRANSFER/TELEPORT
private final UUID targetCellId; // For TRANSFER
public PacketCellAction(
Action action,
UUID cellId,
UUID prisonerId,
UUID targetCellId
) {
this.action = action;
this.cellId = cellId;
this.prisonerId = prisonerId;
this.targetCellId = targetCellId;
}
public static void encode(PacketCellAction msg, FriendlyByteBuf buf) {
buf.writeEnum(msg.action);
buf.writeUUID(msg.cellId);
buf.writeBoolean(msg.prisonerId != null);
if (msg.prisonerId != null) {
buf.writeUUID(msg.prisonerId);
}
buf.writeBoolean(msg.targetCellId != null);
if (msg.targetCellId != null) {
buf.writeUUID(msg.targetCellId);
}
}
public static PacketCellAction decode(FriendlyByteBuf buf) {
Action action = buf.readEnum(Action.class);
UUID cellId = buf.readUUID();
UUID prisonerId = buf.readBoolean() ? buf.readUUID() : null;
UUID targetCellId = buf.readBoolean() ? buf.readUUID() : null;
return new PacketCellAction(action, cellId, prisonerId, targetCellId);
}
public static void handle(
PacketCellAction msg,
Supplier<NetworkEvent.Context> ctx
) {
ctx
.get()
.enqueueWork(() -> {
ServerPlayer sender = ctx.get().getSender();
if (sender == null) return;
// CRITICAL FIX: Add rate limiting to prevent DoS via packet spam
if (
!com.tiedup.remake.network.PacketRateLimiter.allowPacket(
sender,
"action"
)
) {
return;
}
CellRegistryV2 registry = CellRegistryV2.get(
sender.serverLevel()
);
CellDataV2 cell = registry.getCell(msg.cellId);
if (cell == null) {
TiedUpMod.LOGGER.debug(
"[PacketCellAction] Cell not found: {}",
msg.cellId
);
return;
}
// Verify ownership
if (
!cell.canPlayerManage(
sender.getUUID(),
sender.hasPermissions(2)
)
) {
TiedUpMod.LOGGER.debug(
"[PacketCellAction] Player is not cell owner"
);
return;
}
switch (msg.action) {
case RELEASE -> handleRelease(
sender,
registry,
cell,
msg.prisonerId
);
case TRANSFER -> handleTransfer(
sender,
registry,
cell,
msg.prisonerId,
msg.targetCellId
);
case TELEPORT -> handleTeleport(
sender,
registry,
cell,
msg.prisonerId
);
case DELETE_CELL -> handleDeleteCell(
sender,
registry,
cell
);
}
});
ctx.get().setPacketHandled(true);
}
private static void handleRelease(
ServerPlayer sender,
CellRegistryV2 registry,
CellDataV2 cell,
UUID prisonerId
) {
if (prisonerId == null) {
TiedUpMod.LOGGER.debug(
"[PacketCellAction] No prisoner ID for RELEASE"
);
return;
}
if (!cell.hasPrisoner(prisonerId)) {
TiedUpMod.LOGGER.debug("[PacketCellAction] Prisoner not in cell");
return;
}
// Release the prisoner from the cell (HIGH FIX: pass server for state cleanup)
registry.releasePrisoner(cell.getId(), prisonerId, sender.getServer());
// Get prisoner name for message
String prisonerName = getPrisonerName(sender, prisonerId);
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.PRISONER_RELEASED,
prisonerName
);
TiedUpMod.LOGGER.info(
"[PacketCellAction] {} released {} from cell {}",
sender.getName().getString(),
prisonerName,
cell.getId().toString().substring(0, 8)
);
}
private static void handleTransfer(
ServerPlayer sender,
CellRegistryV2 registry,
CellDataV2 sourceCell,
UUID prisonerId,
UUID targetCellId
) {
if (prisonerId == null || targetCellId == null) {
TiedUpMod.LOGGER.debug(
"[PacketCellAction] Missing IDs for TRANSFER"
);
return;
}
// Security: Verify the entity is actually a prisoner in the source cell
if (!sourceCell.hasPrisoner(prisonerId)) {
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.ERROR,
"Entity is not a prisoner in the source cell"
);
TiedUpMod.LOGGER.debug(
"[PacketCellAction] TRANSFER denied: entity {} is not prisoner in cell {}",
prisonerId.toString().substring(0, 8),
sourceCell.getId().toString().substring(0, 8)
);
return;
}
CellDataV2 targetCell = registry.getCell(targetCellId);
if (targetCell == null) {
TiedUpMod.LOGGER.debug("[PacketCellAction] Target cell not found");
return;
}
// Verify ownership of target cell too
if (
!targetCell.canPlayerManage(
sender.getUUID(),
sender.hasPermissions(2)
)
) {
TiedUpMod.LOGGER.debug(
"[PacketCellAction] Player doesn't own target cell"
);
return;
}
if (targetCell.isFull()) {
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.ERROR,
"Target cell is full"
);
return;
}
// Transfer (HIGH FIX: pass server for state cleanup)
registry.releasePrisoner(
sourceCell.getId(),
prisonerId,
sender.getServer()
);
// BUG FIX: Check if assignment succeeded to prevent data desync
boolean assigned = registry.assignPrisoner(targetCellId, prisonerId);
if (!assigned) {
// Assignment failed - re-assign to source cell to prevent data loss
registry.assignPrisoner(sourceCell.getId(), prisonerId);
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.ERROR,
"Failed to transfer prisoner - cell assignment failed"
);
TiedUpMod.LOGGER.error(
"[PacketCellAction] Failed to assign prisoner {} to target cell {}",
prisonerId,
targetCellId
);
return;
}
String prisonerName = getPrisonerName(sender, prisonerId);
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.INFO,
prisonerName + " transferred to " + targetCell.getName()
);
TiedUpMod.LOGGER.info(
"[PacketCellAction] {} transferred {} from cell {} to cell {}",
sender.getName().getString(),
prisonerName,
sourceCell.getId().toString().substring(0, 8),
targetCellId.toString().substring(0, 8)
);
}
private static void handleTeleport(
ServerPlayer sender,
CellRegistryV2 registry,
CellDataV2 cell,
UUID prisonerId
) {
if (prisonerId == null) {
TiedUpMod.LOGGER.debug(
"[PacketCellAction] No prisoner ID for TELEPORT"
);
return;
}
// Security: Verify the entity is actually a prisoner in this cell
if (!cell.hasPrisoner(prisonerId)) {
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.ERROR,
"Entity is not a prisoner in this cell"
);
TiedUpMod.LOGGER.debug(
"[PacketCellAction] TELEPORT denied: entity {} is not prisoner in cell {}",
prisonerId.toString().substring(0, 8),
cell.getId().toString().substring(0, 8)
);
return;
}
// Find the prisoner entity
LivingEntity prisoner = findEntity(sender, prisonerId);
if (prisoner == null) {
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.ERROR,
"Prisoner not found or offline"
);
return;
}
// Teleport to cell core position
prisoner.teleportTo(
cell.getCorePos().getX() + 0.5,
cell.getCorePos().getY(),
cell.getCorePos().getZ() + 0.5
);
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.INFO,
prisoner.getName().getString() + " teleported to cell"
);
TiedUpMod.LOGGER.info(
"[PacketCellAction] {} teleported {} to cell {}",
sender.getName().getString(),
prisoner.getName().getString(),
cell.getId().toString().substring(0, 8)
);
}
private static void handleDeleteCell(
ServerPlayer sender,
CellRegistryV2 registry,
CellDataV2 cell
) {
UUID cellId = cell.getId();
String cellName =
cell.getName() != null
? cell.getName()
: cellId.toString().substring(0, 8);
ServerLevel level = sender.serverLevel();
// Release all prisoners first (HIGH FIX: pass server for state cleanup)
for (UUID prisonerId : cell.getPrisonerIds()) {
registry.releasePrisoner(cellId, prisonerId, sender.getServer());
}
// Remove the Cell Core block at the core position
BlockPos corePos = cell.getCorePos();
BlockState state = level.getBlockState(corePos);
if (state.getBlock() instanceof BlockMarker) {
// Destroy the core block (this also triggers cleanup in BlockMarker.onRemove)
level.destroyBlock(corePos, false);
TiedUpMod.LOGGER.debug(
"[PacketCellAction] Destroyed Cell Core at {}",
corePos.toShortString()
);
}
// Remove the cell from registry (includes all linked positions)
// Note: This may already be handled by BlockMarker.onRemove, but we do it
// explicitly to ensure cleanup even if the marker block was already gone
registry.removeCell(cellId);
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.CELL_DELETED
);
TiedUpMod.LOGGER.info(
"[PacketCellAction] {} deleted cell {}",
sender.getName().getString(),
cellName
);
}
private static String getPrisonerName(
ServerPlayer sender,
UUID prisonerId
) {
LivingEntity entity = findEntity(sender, prisonerId);
return entity != null
? entity.getName().getString()
: prisonerId.toString().substring(0, 8);
}
/**
* CRITICAL FIX: Bounded entity search to prevent DoS.
* Previous implementation used getAllEntities() which scans EVERY entity in the world.
* New implementation uses spatial search with 200-block radius.
*/
private static LivingEntity findEntity(ServerPlayer sender, UUID entityId) {
// Try player first (fast path - O(1) lookup)
Player player = sender.getServer().getPlayerList().getPlayer(entityId);
if (player != null) return player;
// CRITICAL FIX: Use bounded spatial search instead of getAllEntities()
// Search within 200 blocks of the player (reasonable range for cell operations)
final int SEARCH_RADIUS = 200;
net.minecraft.world.phys.AABB searchBox =
new net.minecraft.world.phys.AABB(
sender.getX() - SEARCH_RADIUS,
sender.getY() - SEARCH_RADIUS,
sender.getZ() - SEARCH_RADIUS,
sender.getX() + SEARCH_RADIUS,
sender.getY() + SEARCH_RADIUS,
sender.getZ() + SEARCH_RADIUS
);
// Search only LivingEntity instances within the bounded area
for (LivingEntity entity : sender
.serverLevel()
.getEntitiesOfClass(LivingEntity.class, searchBox, e ->
e.getUUID().equals(entityId)
)) {
return entity; // Found the entity
}
return null; // Entity not found within search radius
}
}

View File

@@ -0,0 +1,200 @@
package com.tiedup.remake.network.cell;
import com.tiedup.remake.blocks.entity.CellCoreBlockEntity;
import com.tiedup.remake.cells.*;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.network.PacketRateLimiter;
import java.util.function.Supplier;
import net.minecraft.ChatFormatting;
import net.minecraft.core.BlockPos;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraftforge.network.NetworkEvent;
/**
* Client → Server packet: handles Cell Core menu button actions.
*/
public class PacketCoreMenuAction {
public enum Action {
SET_SPAWN,
SET_DELIVERY,
SET_DISGUISE,
RESCAN,
}
private final BlockPos corePos;
private final Action action;
public PacketCoreMenuAction(BlockPos corePos, Action action) {
this.corePos = corePos;
this.action = action;
}
public static void encode(PacketCoreMenuAction msg, FriendlyByteBuf buf) {
buf.writeBlockPos(msg.corePos);
buf.writeEnum(msg.action);
}
public static PacketCoreMenuAction decode(FriendlyByteBuf buf) {
return new PacketCoreMenuAction(
buf.readBlockPos(),
buf.readEnum(Action.class)
);
}
public static void handle(
PacketCoreMenuAction msg,
Supplier<NetworkEvent.Context> ctx
) {
ctx
.get()
.enqueueWork(() -> {
ServerPlayer sender = ctx.get().getSender();
if (sender == null) return;
if (!PacketRateLimiter.allowPacket(sender, "core_menu")) {
return;
}
ServerLevel level = sender.serverLevel();
BlockEntity be = level.getBlockEntity(msg.corePos);
if (
!(be instanceof CellCoreBlockEntity core) ||
core.getCellId() == null
) {
return;
}
CellRegistryV2 registry = CellRegistryV2.get(level);
CellDataV2 cell = registry.getCell(core.getCellId());
if (cell == null) return;
// Verify ownership
if (
!cell.canPlayerManage(
sender.getUUID(),
sender.hasPermissions(2)
)
) {
sender.displayClientMessage(
Component.translatable(
"msg.tiedup.cell_core.not_owner"
),
true
);
return;
}
switch (msg.action) {
case SET_SPAWN -> {
CellSelectionManager.startSelection(
sender.getUUID(),
SelectionMode.SET_SPAWN,
msg.corePos,
cell.getId(),
sender.blockPosition()
);
sender.displayClientMessage(
Component.translatable(
"msg.tiedup.cell_core.selection.spawn"
).withStyle(ChatFormatting.YELLOW),
true
);
}
case SET_DELIVERY -> {
CellSelectionManager.startSelection(
sender.getUUID(),
SelectionMode.SET_DELIVERY,
msg.corePos,
cell.getId(),
sender.blockPosition()
);
sender.displayClientMessage(
Component.translatable(
"msg.tiedup.cell_core.selection.delivery"
).withStyle(ChatFormatting.YELLOW),
true
);
}
case SET_DISGUISE -> {
CellSelectionManager.startSelection(
sender.getUUID(),
SelectionMode.SET_DISGUISE,
msg.corePos,
cell.getId(),
sender.blockPosition()
);
sender.displayClientMessage(
Component.translatable(
"msg.tiedup.cell_core.selection.disguise"
).withStyle(ChatFormatting.YELLOW),
true
);
}
case RESCAN -> handleRescan(
sender,
registry,
cell,
core,
level,
msg.corePos
);
}
});
ctx.get().setPacketHandled(true);
}
private static void handleRescan(
ServerPlayer sender,
CellRegistryV2 registry,
CellDataV2 cell,
CellCoreBlockEntity core,
ServerLevel level,
BlockPos corePos
) {
FloodFillResult result = FloodFillAlgorithm.tryFill(level, corePos);
if (result.isSuccess()) {
registry.rescanCell(cell.getId(), result);
// Sync spawn/delivery from Core BE to CellDataV2
if (core.getSpawnPoint() != null) {
cell.setSpawnPoint(core.getSpawnPoint());
}
if (core.getDeliveryPoint() != null) {
cell.setDeliveryPoint(core.getDeliveryPoint());
}
sender.displayClientMessage(
Component.translatable(
"msg.tiedup.cell_core.rescan_success",
result.getInterior().size(),
result.getWalls().size()
).withStyle(ChatFormatting.GREEN),
true
);
TiedUpMod.LOGGER.info(
"[PacketCoreMenuAction] {} rescanned cell {} ({} interior, {} walls)",
sender.getName().getString(),
cell.getId().toString().substring(0, 8),
result.getInterior().size(),
result.getWalls().size()
);
} else {
sender.displayClientMessage(
Component.translatable(
"msg.tiedup.cell_core.rescan_fail",
Component.translatable(result.getErrorKey())
).withStyle(ChatFormatting.RED),
true
);
}
}
}

View File

@@ -0,0 +1,218 @@
package com.tiedup.remake.network.cell;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.network.base.AbstractClientPacket;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import net.minecraft.core.BlockPos;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
/**
* Packet to open the CellManager screen on the client.
* Sends list of cells accessible to the player.
*
* For operators: sends ALL cells with ability to manage any
* For non-operators: sends only owned cells
*
* Server -> Client
*/
public class PacketOpenCellManager extends AbstractClientPacket {
private final List<CellSyncData> cells;
private final boolean isOperator;
/**
* Network-serializable cell data.
*/
public static class CellSyncData {
public final UUID cellId;
public final String name;
public final BlockPos spawnPoint;
public final int prisonerCount;
public final int maxPrisoners;
public final List<PrisonerInfo> prisoners;
public final boolean isOwned; // true if the viewing player owns this cell
public final String ownerName; // Owner's name (for OPs viewing other players' cells)
public CellSyncData(
UUID cellId,
String name,
BlockPos spawnPoint,
int prisonerCount,
int maxPrisoners,
List<PrisonerInfo> prisoners,
boolean isOwned,
String ownerName
) {
this.cellId = cellId;
this.name = name;
this.spawnPoint = spawnPoint;
this.prisonerCount = prisonerCount;
this.maxPrisoners = maxPrisoners;
this.prisoners = prisoners != null ? prisoners : new ArrayList<>();
this.isOwned = isOwned;
this.ownerName = ownerName;
}
/**
* Legacy constructor for backward compatibility.
*/
public CellSyncData(
UUID cellId,
String name,
BlockPos spawnPoint,
int prisonerCount,
int maxPrisoners,
List<PrisonerInfo> prisoners
) {
this(
cellId,
name,
spawnPoint,
prisonerCount,
maxPrisoners,
prisoners,
true,
null
);
}
/**
* Get display name (name or truncated UUID).
*/
public String getDisplayName() {
if (name != null && !name.isEmpty()) {
return name;
}
return "Cell " + cellId.toString().substring(0, 8);
}
}
/**
* Network-serializable prisoner info.
*/
public static class PrisonerInfo {
public final UUID prisonerId;
public final String prisonerName;
public PrisonerInfo(UUID prisonerId, String prisonerName) {
this.prisonerId = prisonerId;
this.prisonerName = prisonerName;
}
}
public PacketOpenCellManager(List<CellSyncData> cells, boolean isOperator) {
this.cells = cells != null ? cells : new ArrayList<>();
this.isOperator = isOperator;
}
/**
* Legacy constructor for backward compatibility (assumes not operator).
*/
public PacketOpenCellManager(List<CellSyncData> cells) {
this(cells, false);
}
public void encode(FriendlyByteBuf buf) {
buf.writeBoolean(this.isOperator);
buf.writeInt(this.cells.size());
for (CellSyncData cell : this.cells) {
buf.writeUUID(cell.cellId);
buf.writeBoolean(cell.name != null);
if (cell.name != null) {
buf.writeUtf(cell.name, 64);
}
buf.writeBlockPos(cell.spawnPoint);
buf.writeInt(cell.prisonerCount);
buf.writeInt(cell.maxPrisoners);
buf.writeBoolean(cell.isOwned);
buf.writeBoolean(cell.ownerName != null);
if (cell.ownerName != null) {
buf.writeUtf(cell.ownerName, 64);
}
// Write prisoners
buf.writeInt(cell.prisoners.size());
for (PrisonerInfo prisoner : cell.prisoners) {
buf.writeUUID(prisoner.prisonerId);
buf.writeUtf(prisoner.prisonerName, 64);
}
}
}
public static PacketOpenCellManager decode(FriendlyByteBuf buf) {
boolean isOperator = buf.readBoolean();
int cellCount = buf.readInt();
List<CellSyncData> cells = new ArrayList<>();
for (int i = 0; i < cellCount; i++) {
UUID cellId = buf.readUUID();
String name = buf.readBoolean() ? buf.readUtf(64) : null;
BlockPos spawnPoint = buf.readBlockPos();
int prisonerCount = buf.readInt();
int maxPrisoners = buf.readInt();
boolean isOwned = buf.readBoolean();
String ownerName = buf.readBoolean() ? buf.readUtf(64) : null;
// Read prisoners
int prisonerListSize = buf.readInt();
List<PrisonerInfo> prisoners = new ArrayList<>();
for (int j = 0; j < prisonerListSize; j++) {
UUID prisonerId = buf.readUUID();
String prisonerName = buf.readUtf(64);
prisoners.add(new PrisonerInfo(prisonerId, prisonerName));
}
cells.add(
new CellSyncData(
cellId,
name,
spawnPoint,
prisonerCount,
maxPrisoners,
prisoners,
isOwned,
ownerName
)
);
}
return new PacketOpenCellManager(cells, isOperator);
}
@Override
@OnlyIn(Dist.CLIENT)
protected void handleClientImpl() {
ClientHandler.handle(this);
}
@OnlyIn(Dist.CLIENT)
private static class ClientHandler {
private static void handle(PacketOpenCellManager pkt) {
net.minecraft.client.Minecraft mc = net.minecraft.client.Minecraft.getInstance();
if (mc.player == null) return;
TiedUpMod.LOGGER.info(
"[PacketOpenCellManager] Opening cell manager with {} cells (operator: {})",
pkt.cells.size(),
pkt.isOperator
);
mc.setScreen(new com.tiedup.remake.client.gui.screens.CellManagerScreen(pkt.cells, pkt.isOperator));
}
}
public List<CellSyncData> getCells() {
return cells;
}
public boolean isOperator() {
return isOperator;
}
}

View File

@@ -0,0 +1,112 @@
package com.tiedup.remake.network.cell;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.network.base.AbstractClientPacket;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
/**
* Packet to open the CellSelector screen on the client.
* Server -> Client
*/
public class PacketOpenCellSelector extends AbstractClientPacket {
private final UUID targetEntityUUID; // The entity whose collar we want to assign
private final List<CellOption> cells;
/**
* Cell option for the selector.
*/
public static class CellOption {
public final UUID cellId;
public final String displayName;
public final int prisonerCount;
public final int maxPrisoners;
public CellOption(
UUID cellId,
String displayName,
int prisonerCount,
int maxPrisoners
) {
this.cellId = cellId;
this.displayName = displayName;
this.prisonerCount = prisonerCount;
this.maxPrisoners = maxPrisoners;
}
}
public PacketOpenCellSelector(
UUID targetEntityUUID,
List<CellOption> cells
) {
this.targetEntityUUID = targetEntityUUID;
this.cells = cells != null ? cells : new ArrayList<>();
}
public void encode(FriendlyByteBuf buf) {
buf.writeUUID(this.targetEntityUUID);
buf.writeInt(this.cells.size());
for (CellOption cell : this.cells) {
buf.writeUUID(cell.cellId);
buf.writeUtf(cell.displayName, 64);
buf.writeInt(cell.prisonerCount);
buf.writeInt(cell.maxPrisoners);
}
}
public static PacketOpenCellSelector decode(FriendlyByteBuf buf) {
UUID targetEntityUUID = buf.readUUID();
int cellCount = buf.readInt();
List<CellOption> cells = new ArrayList<>();
for (int i = 0; i < cellCount; i++) {
UUID cellId = buf.readUUID();
String displayName = buf.readUtf(64);
int prisonerCount = buf.readInt();
int maxPrisoners = buf.readInt();
cells.add(
new CellOption(cellId, displayName, prisonerCount, maxPrisoners)
);
}
return new PacketOpenCellSelector(targetEntityUUID, cells);
}
@Override
@OnlyIn(Dist.CLIENT)
protected void handleClientImpl() {
ClientHandler.handle(this);
}
@OnlyIn(Dist.CLIENT)
private static class ClientHandler {
private static void handle(PacketOpenCellSelector pkt) {
net.minecraft.client.Minecraft mc = net.minecraft.client.Minecraft.getInstance();
if (mc.player == null) return;
TiedUpMod.LOGGER.info(
"[PacketOpenCellSelector] Opening cell selector with {} options for target {}",
pkt.cells.size(),
pkt.targetEntityUUID.toString().substring(0, 8)
);
mc.setScreen(new com.tiedup.remake.client.gui.screens.CellSelectorScreen(pkt.targetEntityUUID, pkt.cells));
}
}
public UUID getTargetEntityUUID() {
return targetEntityUUID;
}
public List<CellOption> getCells() {
return cells;
}
}

View File

@@ -0,0 +1,128 @@
package com.tiedup.remake.network.cell;
import com.tiedup.remake.network.base.AbstractClientPacket;
import java.util.UUID;
import net.minecraft.core.BlockPos;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
/**
* Server → Client packet: opens the Cell Core menu with all cell info.
* Carries all data so the Info panel works without a round-trip.
*/
public class PacketOpenCoreMenu extends AbstractClientPacket {
private final BlockPos corePos;
private final UUID cellId;
private final String cellName;
private final String stateName;
private final int interiorVolume;
private final int wallCount;
private final int breachCount;
private final int prisonerCount;
private final int bedCount;
private final int doorCount;
private final int anchorCount;
private final boolean hasSpawn;
private final boolean hasDelivery;
private final boolean hasDisguise;
public PacketOpenCoreMenu(
BlockPos corePos,
UUID cellId,
String cellName,
String stateName,
int interiorVolume,
int wallCount,
int breachCount,
int prisonerCount,
int bedCount,
int doorCount,
int anchorCount,
boolean hasSpawn,
boolean hasDelivery,
boolean hasDisguise
) {
this.corePos = corePos;
this.cellId = cellId;
this.cellName = cellName;
this.stateName = stateName;
this.interiorVolume = interiorVolume;
this.wallCount = wallCount;
this.breachCount = breachCount;
this.prisonerCount = prisonerCount;
this.bedCount = bedCount;
this.doorCount = doorCount;
this.anchorCount = anchorCount;
this.hasSpawn = hasSpawn;
this.hasDelivery = hasDelivery;
this.hasDisguise = hasDisguise;
}
public static void encode(PacketOpenCoreMenu msg, FriendlyByteBuf buf) {
buf.writeBlockPos(msg.corePos);
buf.writeUUID(msg.cellId);
buf.writeUtf(msg.cellName, 64);
buf.writeUtf(msg.stateName, 32);
buf.writeVarInt(msg.interiorVolume);
buf.writeVarInt(msg.wallCount);
buf.writeVarInt(msg.breachCount);
buf.writeVarInt(msg.prisonerCount);
buf.writeVarInt(msg.bedCount);
buf.writeVarInt(msg.doorCount);
buf.writeVarInt(msg.anchorCount);
buf.writeBoolean(msg.hasSpawn);
buf.writeBoolean(msg.hasDelivery);
buf.writeBoolean(msg.hasDisguise);
}
public static PacketOpenCoreMenu decode(FriendlyByteBuf buf) {
return new PacketOpenCoreMenu(
buf.readBlockPos(),
buf.readUUID(),
buf.readUtf(64),
buf.readUtf(32),
buf.readVarInt(),
buf.readVarInt(),
buf.readVarInt(),
buf.readVarInt(),
buf.readVarInt(),
buf.readVarInt(),
buf.readVarInt(),
buf.readBoolean(),
buf.readBoolean(),
buf.readBoolean()
);
}
@Override
@OnlyIn(Dist.CLIENT)
protected void handleClientImpl() {
ClientHandler.handle(this);
}
@OnlyIn(Dist.CLIENT)
private static class ClientHandler {
private static void handle(PacketOpenCoreMenu pkt) {
net.minecraft.client.Minecraft.getInstance().setScreen(
new com.tiedup.remake.client.gui.screens.CellCoreScreen(
pkt.corePos,
pkt.cellId,
pkt.cellName,
pkt.stateName,
pkt.interiorVolume,
pkt.wallCount,
pkt.breachCount,
pkt.prisonerCount,
pkt.bedCount,
pkt.doorCount,
pkt.anchorCount,
pkt.hasSpawn,
pkt.hasDelivery,
pkt.hasDisguise
)
);
}
}
}

View File

@@ -0,0 +1,102 @@
package com.tiedup.remake.network.cell;
import com.tiedup.remake.cells.CellDataV2;
import com.tiedup.remake.cells.CellRegistryV2;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.network.PacketRateLimiter;
import java.util.UUID;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.network.NetworkEvent;
/**
* Packet to rename a cell.
* Client -> Server
*/
public class PacketRenameCell {
private final UUID cellId;
private final String newName;
public PacketRenameCell(UUID cellId, String newName) {
this.cellId = cellId;
this.newName = newName;
}
public static void encode(PacketRenameCell msg, FriendlyByteBuf buf) {
buf.writeUUID(msg.cellId);
buf.writeUtf(msg.newName, 64);
}
public static PacketRenameCell decode(FriendlyByteBuf buf) {
UUID cellId = buf.readUUID();
String newName = buf.readUtf(64);
return new PacketRenameCell(cellId, newName);
}
public static void handle(
PacketRenameCell msg,
Supplier<NetworkEvent.Context> ctx
) {
ctx
.get()
.enqueueWork(() -> {
ServerPlayer sender = ctx.get().getSender();
if (sender == null) return;
if (!PacketRateLimiter.allowPacket(sender, "action")) return;
CellRegistryV2 registry = CellRegistryV2.get(
sender.serverLevel()
);
CellDataV2 cell = registry.getCell(msg.cellId);
if (cell == null) {
TiedUpMod.LOGGER.debug(
"[PacketRenameCell] Cell not found: {}",
msg.cellId
);
return;
}
// Verify ownership
if (
!cell.canPlayerManage(
sender.getUUID(),
sender.hasPermissions(2)
)
) {
TiedUpMod.LOGGER.debug(
"[PacketRenameCell] Player is not cell owner"
);
return;
}
// Sanitize name
String sanitizedName = msg.newName.trim();
if (sanitizedName.length() > 32) {
sanitizedName = sanitizedName.substring(0, 32);
}
// Set name (null to clear)
cell.setName(sanitizedName.isEmpty() ? null : sanitizedName);
registry.setDirty();
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.CELL_RENAMED,
sanitizedName.isEmpty() ? "(cleared)" : sanitizedName
);
TiedUpMod.LOGGER.info(
"[PacketRenameCell] {} renamed cell {} to '{}'",
sender.getName().getString(),
msg.cellId.toString().substring(0, 8),
sanitizedName
);
});
ctx.get().setPacketHandled(true);
}
}

View File

@@ -0,0 +1,92 @@
package com.tiedup.remake.network.cell;
import com.tiedup.remake.cells.CellDataV2;
import com.tiedup.remake.cells.CellRegistryV2;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.PacketRateLimiter;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.player.Player;
import net.minecraftforge.network.NetworkEvent;
/**
* Packet to request the list of cells owned by the player.
* Used to populate the CellSelectorScreen.
*
* Client -> Server (triggers PacketOpenCellSelector response)
*/
public class PacketRequestCellList {
private final UUID targetEntityUUID; // The entity whose collar we want to assign
public PacketRequestCellList(UUID targetEntityUUID) {
this.targetEntityUUID = targetEntityUUID;
}
public static void encode(PacketRequestCellList msg, FriendlyByteBuf buf) {
buf.writeUUID(msg.targetEntityUUID);
}
public static PacketRequestCellList decode(FriendlyByteBuf buf) {
UUID targetEntityUUID = buf.readUUID();
return new PacketRequestCellList(targetEntityUUID);
}
public static void handle(
PacketRequestCellList msg,
Supplier<NetworkEvent.Context> ctx
) {
ctx
.get()
.enqueueWork(() -> {
ServerPlayer sender = ctx.get().getSender();
if (sender == null) return;
if (!PacketRateLimiter.allowPacket(sender, "ui")) return;
CellRegistryV2 registry = CellRegistryV2.get(
sender.serverLevel()
);
List<CellDataV2> playerCells = registry.getCellsByOwner(
sender.getUUID()
);
// Build cell list for packet
List<PacketOpenCellSelector.CellOption> options =
new ArrayList<>();
for (CellDataV2 cell : playerCells) {
String displayName =
cell.getName() != null
? cell.getName()
: "Cell " + cell.getId().toString().substring(0, 8);
options.add(
new PacketOpenCellSelector.CellOption(
cell.getId(),
displayName,
cell.getPrisonerCount(),
4 // MAX_PRISONERS
)
);
}
// Send response packet to open the selector
ModNetwork.sendToPlayer(
new PacketOpenCellSelector(msg.targetEntityUUID, options),
sender
);
TiedUpMod.LOGGER.debug(
"[PacketRequestCellList] Sent {} cells to {}",
options.size(),
sender.getName().getString()
);
});
ctx.get().setPacketHandled(true);
}
}

View File

@@ -0,0 +1,256 @@
package com.tiedup.remake.network.cell;
import com.tiedup.remake.cells.CellDataV2;
import com.tiedup.remake.cells.MarkerType;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.network.base.AbstractClientPacket;
import java.util.*;
import net.minecraft.core.BlockPos;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
/**
* Packet to sync cell data from server to client.
* This enables cell outline rendering on dedicated servers.
*
* Server -> Client
*
* Phase: Kidnapper Revamp - Cell System Network Sync
*/
public class PacketSyncCellData extends AbstractClientPacket {
private final UUID cellId;
private final BlockPos spawnPoint;
private final Map<MarkerType, Set<BlockPos>> positions;
private final List<BlockPos> pathWaypoints;
private final String name;
private final UUID ownerId;
/**
* Create a sync packet for a V2 cell.
* Maps V2 feature lists back to MarkerType sets for wire format compatibility.
*/
public PacketSyncCellData(CellDataV2 cell) {
this.cellId = cell.getId();
this.spawnPoint = cell.getCorePos();
this.positions = new EnumMap<>(MarkerType.class);
this.pathWaypoints = new ArrayList<>(cell.getPathWaypoints());
this.name = cell.getName();
this.ownerId = cell.getOwnerId();
// Map V2 feature lists to MarkerType sets for serialization
if (!cell.getWallBlocks().isEmpty()) {
this.positions.put(
MarkerType.WALL,
new HashSet<>(cell.getWallBlocks())
);
}
if (!cell.getAnchors().isEmpty()) {
this.positions.put(
MarkerType.ANCHOR,
new HashSet<>(cell.getAnchors())
);
}
if (!cell.getDoors().isEmpty()) {
this.positions.put(MarkerType.DOOR, new HashSet<>(cell.getDoors()));
}
if (!cell.getBeds().isEmpty()) {
this.positions.put(MarkerType.BED, new HashSet<>(cell.getBeds()));
}
BlockPos delivery = cell.getDeliveryPoint();
if (delivery != null) {
Set<BlockPos> deliverySet = new HashSet<>();
deliverySet.add(delivery);
this.positions.put(MarkerType.DELIVERY, deliverySet);
}
}
/**
* Private constructor for decoding.
*/
private PacketSyncCellData(
UUID cellId,
BlockPos spawnPoint,
Map<MarkerType, Set<BlockPos>> positions,
List<BlockPos> pathWaypoints,
String name,
UUID ownerId
) {
this.cellId = cellId;
this.spawnPoint = spawnPoint;
this.positions = positions;
this.pathWaypoints = pathWaypoints;
this.name = name;
this.ownerId = ownerId;
}
public void encode(FriendlyByteBuf buf) {
buf.writeUUID(this.cellId);
buf.writeBlockPos(this.spawnPoint);
// Write name (nullable)
buf.writeBoolean(this.name != null);
if (this.name != null) {
buf.writeUtf(this.name, 64);
}
// Write owner (nullable)
buf.writeBoolean(this.ownerId != null);
if (this.ownerId != null) {
buf.writeUUID(this.ownerId);
}
// Write positions by type
// First, count non-empty types
int typeCount = 0;
for (MarkerType type : MarkerType.values()) {
if (
this.positions.containsKey(type) &&
!this.positions.get(type).isEmpty()
) {
typeCount++;
}
}
buf.writeInt(typeCount);
// Write each non-empty type
for (MarkerType type : MarkerType.values()) {
Set<BlockPos> typePositions = this.positions.get(type);
if (typePositions != null && !typePositions.isEmpty()) {
buf.writeUtf(type.getSerializedName(), 32);
buf.writeInt(typePositions.size());
for (BlockPos pos : typePositions) {
buf.writeBlockPos(pos);
}
}
}
// Write path waypoints
buf.writeInt(this.pathWaypoints.size());
for (BlockPos wp : this.pathWaypoints) {
buf.writeBlockPos(wp);
}
}
public static PacketSyncCellData decode(FriendlyByteBuf buf) {
UUID cellId = buf.readUUID();
BlockPos spawnPoint = buf.readBlockPos();
// Read name
String name = buf.readBoolean() ? buf.readUtf(64) : null;
// Read owner
UUID ownerId = buf.readBoolean() ? buf.readUUID() : null;
// Read positions
Map<MarkerType, Set<BlockPos>> positions = new EnumMap<>(
MarkerType.class
);
int typeCount = buf.readInt();
for (int i = 0; i < typeCount; i++) {
String typeName = buf.readUtf(32);
MarkerType type = MarkerType.fromString(typeName);
int posCount = buf.readInt();
Set<BlockPos> typePositions = new HashSet<>();
for (int j = 0; j < posCount; j++) {
typePositions.add(buf.readBlockPos());
}
positions.put(type, typePositions);
}
// Read path waypoints
int waypointCount = buf.readInt();
List<BlockPos> pathWaypoints = new ArrayList<>(waypointCount);
for (int i = 0; i < waypointCount; i++) {
pathWaypoints.add(buf.readBlockPos());
}
return new PacketSyncCellData(
cellId,
spawnPoint,
positions,
pathWaypoints,
name,
ownerId
);
}
@Override
@OnlyIn(Dist.CLIENT)
protected void handleClientImpl() {
ClientHandler.handle(this);
}
@OnlyIn(Dist.CLIENT)
private static class ClientHandler {
private static void handle(PacketSyncCellData pkt) {
TiedUpMod.LOGGER.debug(
"[PacketSyncCellData] Received cell sync: {} at {} with {} position types, {} waypoints",
pkt.cellId.toString().substring(0, 8),
pkt.spawnPoint.toShortString(),
pkt.positions.size(),
pkt.pathWaypoints.size()
);
// Reconstruct a CellDataV2 from the wire data
CellDataV2 cell = new CellDataV2(pkt.cellId, pkt.spawnPoint);
cell.setName(pkt.name);
cell.setOwnerId(pkt.ownerId);
// Populate geometry from MarkerType positions
for (BlockPos pos : pkt.positions.getOrDefault(
MarkerType.WALL,
Collections.emptySet()
)) {
cell.addWallBlock(pos);
}
for (BlockPos pos : pkt.positions.getOrDefault(
MarkerType.ANCHOR,
Collections.emptySet()
)) {
cell.addAnchor(pos);
}
for (BlockPos pos : pkt.positions.getOrDefault(
MarkerType.DOOR,
Collections.emptySet()
)) {
cell.addDoor(pos);
}
for (BlockPos pos : pkt.positions.getOrDefault(
MarkerType.BED,
Collections.emptySet()
)) {
cell.addBed(pos);
}
Set<BlockPos> delivery = pkt.positions.getOrDefault(
MarkerType.DELIVERY,
Collections.emptySet()
);
if (!delivery.isEmpty()) {
cell.setDeliveryPoint(delivery.iterator().next());
}
// Add path waypoints
cell.setPathWaypoints(pkt.pathWaypoints);
// Update the client-side cache
com.tiedup.remake.client.events.CellHighlightHandler.updateCachedCell(cell);
}
}
// Getters for testing/debugging
public UUID getCellId() {
return cellId;
}
public BlockPos getSpawnPoint() {
return spawnPoint;
}
public Map<MarkerType, Set<BlockPos>> getPositions() {
return Collections.unmodifiableMap(positions);
}
}

View File

@@ -0,0 +1,65 @@
package com.tiedup.remake.network.conversation;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.conversation.ConversationManager;
import com.tiedup.remake.entities.EntityDamsel;
import com.tiedup.remake.network.PacketRateLimiter;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraftforge.network.NetworkEvent;
/**
* Client-to-Server packet: player closes conversation GUI.
*
* Split from the former bidirectional PacketEndConversation (H16 fix).
*/
public class PacketEndConversationC2S {
private final int entityId;
public PacketEndConversationC2S(int entityId) {
this.entityId = entityId;
}
public void encode(FriendlyByteBuf buf) {
buf.writeInt(entityId);
}
public static PacketEndConversationC2S decode(FriendlyByteBuf buf) {
return new PacketEndConversationC2S(buf.readInt());
}
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx.get().enqueueWork(() -> {
ServerPlayer sender = ctx.get().getSender();
if (sender == null) return;
if (!PacketRateLimiter.allowPacket(sender, "action")) return;
TiedUpMod.LOGGER.info(
"[PacketEndConversationC2S] {} ended conversation with entity {}",
sender.getName().getString(),
entityId
);
// Get the damsel entity to properly end with cooldown
EntityDamsel damsel = null;
Entity entity = sender.level().getEntity(entityId);
if (entity instanceof EntityDamsel d) {
damsel = d;
}
// Always clean up conversation state — this is a teardown packet.
// Distance check removed: blocking cleanup causes permanent state leak
// in ConversationManager.activeConversations (reviewer H18 BUG-001).
ConversationManager.endConversation(sender, damsel);
});
ctx.get().setPacketHandled(true);
}
public int getEntityId() {
return entityId;
}
}

View File

@@ -0,0 +1,63 @@
package com.tiedup.remake.network.conversation;
import com.tiedup.remake.core.TiedUpMod;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.network.NetworkEvent;
/**
* Server-to-Client packet: server forces conversation end
* (e.g., NPC died, player moved too far).
*
* Split from the former bidirectional PacketEndConversation (H16 fix).
*/
public class PacketEndConversationS2C {
private final int entityId;
public PacketEndConversationS2C(int entityId) {
this.entityId = entityId;
}
public void encode(FriendlyByteBuf buf) {
buf.writeInt(entityId);
}
public static PacketEndConversationS2C decode(FriendlyByteBuf buf) {
return new PacketEndConversationS2C(buf.readInt());
}
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx.get().enqueueWork(this::handleClient);
ctx.get().setPacketHandled(true);
}
@OnlyIn(Dist.CLIENT)
private void handleClient() {
ClientHandler.handle(this);
}
public int getEntityId() {
return entityId;
}
@OnlyIn(Dist.CLIENT)
private static class ClientHandler {
private static void handle(PacketEndConversationS2C pkt) {
TiedUpMod.LOGGER.info(
"[PacketEndConversationS2C] Server ended conversation with entity {}",
pkt.entityId
);
// Close any open conversation screen
if (
net.minecraft.client.Minecraft.getInstance().screen instanceof
com.tiedup.remake.client.gui.screens.ConversationScreen
) {
net.minecraft.client.Minecraft.getInstance().setScreen(null);
}
}
}
}

View File

@@ -0,0 +1,109 @@
package com.tiedup.remake.network.conversation;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.conversation.ConversationTopic;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.network.NetworkEvent;
/**
* Packet sent from server to client to open the conversation GUI.
* Contains the entity ID, NPC name, and available conversation topics.
*
* Phase 14: Conversation System
*/
public class PacketOpenConversation {
private final int entityId;
private final String npcName;
private final List<String> availableTopics;
public PacketOpenConversation(
int entityId,
String npcName,
List<ConversationTopic> topics
) {
this.entityId = entityId;
this.npcName = npcName;
this.availableTopics = new ArrayList<>();
for (ConversationTopic topic : topics) {
this.availableTopics.add(topic.name());
}
}
private PacketOpenConversation(
int entityId,
String npcName,
List<String> topicNames,
boolean raw
) {
this.entityId = entityId;
this.npcName = npcName;
this.availableTopics = topicNames;
}
public void encode(FriendlyByteBuf buf) {
buf.writeInt(entityId);
buf.writeUtf(npcName, 64);
buf.writeInt(availableTopics.size());
for (String topic : availableTopics) {
buf.writeUtf(topic, 64);
}
}
public static PacketOpenConversation decode(FriendlyByteBuf buf) {
int entityId = buf.readInt();
String npcName = buf.readUtf(64);
int topicCount = buf.readInt();
List<String> topics = new ArrayList<>(topicCount);
for (int i = 0; i < topicCount; i++) {
topics.add(buf.readUtf(64));
}
return new PacketOpenConversation(entityId, npcName, topics, true);
}
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx.get().enqueueWork(() -> handleClient());
ctx.get().setPacketHandled(true);
}
private void handleClient() {
// DISABLED: Conversation system not in use
TiedUpMod.LOGGER.info(
"[PacketOpenConversation] Conversation system disabled - ignoring request"
);
/*
// Convert topic names back to enum values
List<ConversationTopic> topics = new ArrayList<>();
for (String topicName : availableTopics) {
try {
topics.add(ConversationTopic.valueOf(topicName));
} catch (IllegalArgumentException e) {
TiedUpMod.LOGGER.warn(
"[PacketOpenConversation] Unknown topic: {}",
topicName
);
}
}
// Open the conversation screen
// Delegate to client-only helper to avoid class loading on server
com.tiedup.remake.client.network.ClientPacketHandler.openConversationScreen(entityId, npcName, topics);
*/
}
public int getEntityId() {
return entityId;
}
public String getNpcName() {
return npcName;
}
public List<String> getAvailableTopics() {
return availableTopics;
}
}

View File

@@ -0,0 +1,108 @@
package com.tiedup.remake.network.conversation;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.IDialogueSpeaker;
import com.tiedup.remake.dialogue.conversation.ConversationManager;
import com.tiedup.remake.network.PacketRateLimiter;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraftforge.network.NetworkEvent;
/**
* Packet sent from client to server to request opening a conversation.
* Sent when player clicks the "Ask..." button in DialogueScreen.
*
* Phase 14: Conversation System
*
* DISABLED: Conversation system not in use. Kept because it is still registered
* in ModNetwork — removing a registered packet would shift packet IDs.
*/
public class PacketRequestConversation {
/** Maximum distance for conversation request */
private static final double MAX_DISTANCE = 6.0;
private final int entityId;
public PacketRequestConversation(int entityId) {
this.entityId = entityId;
}
public void encode(FriendlyByteBuf buf) {
buf.writeInt(entityId);
}
public static PacketRequestConversation decode(FriendlyByteBuf buf) {
int entityId = buf.readInt();
return new PacketRequestConversation(entityId);
}
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx
.get()
.enqueueWork(() -> {
ServerPlayer sender = ctx.get().getSender();
if (sender == null) return;
handleServer(sender);
});
ctx.get().setPacketHandled(true);
}
private void handleServer(ServerPlayer sender) {
if (!PacketRateLimiter.allowPacket(sender, "action")) return;
// Find the target entity
Entity entity = sender.level().getEntity(entityId);
if (entity == null) {
TiedUpMod.LOGGER.warn(
"[PacketRequestConversation] Entity {} not found",
entityId
);
return;
}
// Check if entity is a dialogue speaker
if (!(entity instanceof IDialogueSpeaker speaker)) {
TiedUpMod.LOGGER.warn(
"[PacketRequestConversation] Entity {} is not a dialogue speaker",
entityId
);
return;
}
// Validate distance
double distance = sender.distanceTo(entity);
if (distance > MAX_DISTANCE) {
TiedUpMod.LOGGER.warn(
"[PacketRequestConversation] Player {} too far from entity {} ({})",
sender.getName().getString(),
entityId,
distance
);
return;
}
TiedUpMod.LOGGER.info(
"[PacketRequestConversation] {} requesting conversation with {}",
sender.getName().getString(),
speaker.getDialogueName()
);
// Open the conversation - this will send PacketOpenConversation to client
boolean success = ConversationManager.openConversation(speaker, sender);
if (!success) {
TiedUpMod.LOGGER.warn(
"[PacketRequestConversation] Failed to open conversation with {}",
speaker.getDialogueName()
);
}
}
public int getEntityId() {
return entityId;
}
}

View File

@@ -0,0 +1,125 @@
package com.tiedup.remake.network.conversation;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.IDialogueSpeaker;
import com.tiedup.remake.dialogue.conversation.ConversationManager;
import com.tiedup.remake.dialogue.conversation.ConversationTopic;
import com.tiedup.remake.entities.EntityDamsel;
import com.tiedup.remake.network.PacketRateLimiter;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraftforge.network.NetworkEvent;
/**
* Packet sent from client to server when player selects a conversation topic.
*
* Phase 14: Conversation System
*/
public class PacketSelectTopic {
/** Maximum distance for conversation */
private static final double MAX_DISTANCE = 5.0;
private final int entityId;
private final String topicName;
public PacketSelectTopic(int entityId, ConversationTopic topic) {
this.entityId = entityId;
this.topicName = topic.name();
}
private PacketSelectTopic(int entityId, String topicName) {
this.entityId = entityId;
this.topicName = topicName;
}
public void encode(FriendlyByteBuf buf) {
buf.writeInt(entityId);
buf.writeUtf(topicName, 64);
}
public static PacketSelectTopic decode(FriendlyByteBuf buf) {
int entityId = buf.readInt();
String topicName = buf.readUtf(64);
return new PacketSelectTopic(entityId, topicName);
}
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx
.get()
.enqueueWork(() -> {
ServerPlayer sender = ctx.get().getSender();
if (sender == null) return;
handleServer(sender);
});
ctx.get().setPacketHandled(true);
}
private void handleServer(ServerPlayer sender) {
if (!PacketRateLimiter.allowPacket(sender, "action")) return;
// Find the target entity
Entity entity = sender.level().getEntity(entityId);
if (entity == null) {
TiedUpMod.LOGGER.warn(
"[PacketSelectTopic] Entity {} not found",
entityId
);
return;
}
// Check if entity is a dialogue speaker
if (!(entity instanceof IDialogueSpeaker speaker)) {
TiedUpMod.LOGGER.warn(
"[PacketSelectTopic] Entity {} is not a dialogue speaker",
entityId
);
return;
}
// Validate distance
double distance = sender.distanceTo(entity);
if (distance > MAX_DISTANCE) {
TiedUpMod.LOGGER.warn(
"[PacketSelectTopic] Player {} too far from entity {} ({})",
sender.getName().getString(),
entityId,
distance
);
return;
}
// Parse topic
ConversationTopic topic;
try {
topic = ConversationTopic.valueOf(topicName);
} catch (IllegalArgumentException e) {
TiedUpMod.LOGGER.warn(
"[PacketSelectTopic] Unknown topic: {}",
topicName
);
return;
}
TiedUpMod.LOGGER.info(
"[PacketSelectTopic] {} selected topic {} for {}",
sender.getName().getString(),
topic.name(),
speaker.getDialogueName()
);
// Handle the topic selection - this sends the response dialogue
ConversationManager.handleTopicSelection(speaker, sender, topic);
}
public int getEntityId() {
return entityId;
}
public String getTopicName() {
return topicName;
}
}

View File

@@ -0,0 +1,179 @@
package com.tiedup.remake.network.item;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.AdjustmentHelper;
import com.tiedup.remake.items.base.IAdjustable;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.network.sync.SyncManager;
import com.tiedup.remake.state.PlayerBindState;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.network.NetworkEvent;
/**
* Phase 16: Packet for adjusting item Y position (Client to Server).
*
* Sent by client when player adjusts a gag or blindfold position.
* Server validates and applies the adjustment, then syncs to all clients.
*/
public class PacketAdjustItem {
private final BodyRegionV2 region;
private final float adjustmentValue;
private final float scaleValue;
/**
* Create adjustment packet.
*
* @param region The body region (MOUTH or EYES)
* @param adjustmentValue The adjustment value (-4.0 to +4.0)
* @param scaleValue The scale value (0.5 to 2.0)
*/
public PacketAdjustItem(
BodyRegionV2 region,
float adjustmentValue,
float scaleValue
) {
this.region = region;
this.adjustmentValue = adjustmentValue;
this.scaleValue = scaleValue;
}
/**
* Encode the packet to the network buffer.
*
* @param buf The buffer to write to
*/
public void encode(FriendlyByteBuf buf) {
buf.writeEnum(region);
buf.writeFloat(adjustmentValue);
buf.writeFloat(scaleValue);
}
/**
* Decode the packet from the network buffer.
*
* @param buf The buffer to read from
* @return The decoded packet
*/
public static PacketAdjustItem decode(FriendlyByteBuf buf) {
BodyRegionV2 region = buf.readEnum(BodyRegionV2.class);
float value = buf.readFloat();
float scale = buf.readFloat();
return new PacketAdjustItem(region, value, scale);
}
/**
* Handle the packet on the receiving side (SERVER SIDE).
*
* @param ctx The network context
*/
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx
.get()
.enqueueWork(() -> {
ServerPlayer player = ctx.get().getSender();
if (player == null) {
return;
}
// Rate limiting: Prevent adjustment spam
if (
!com.tiedup.remake.network.PacketRateLimiter.allowPacket(
player,
"action"
)
) {
return;
}
handleServer(player);
});
ctx.get().setPacketHandled(true);
}
/**
* Handle the packet on the server side.
* Validates the adjustment and applies it to the player's item.
*
* @param player The player who sent the packet
*/
private void handleServer(ServerPlayer player) {
PlayerBindState state = PlayerBindState.getInstance(player);
if (state == null) {
TiedUpMod.LOGGER.warn(
"[PACKET] PacketAdjustItem received but PlayerBindState is null for {}",
player.getName().getString()
);
return;
}
// SECURITY: Validate adjustment value (prevent NaN, Infinity, out-of-bounds)
if (
Float.isNaN(adjustmentValue) ||
Float.isInfinite(adjustmentValue) ||
Float.isNaN(scaleValue) ||
Float.isInfinite(scaleValue)
) {
TiedUpMod.LOGGER.warn(
"SECURITY: Invalid adjustment value from {}: adj={}, scale={}",
player.getName().getString(),
adjustmentValue,
scaleValue
);
return;
}
// Valid range check (-5.0 to +5.0 pixels, with margin)
if (adjustmentValue < -5.0f || adjustmentValue > 5.0f) {
TiedUpMod.LOGGER.debug(
"[PACKET] Adjustment value out of bounds from {}: {}",
player.getName().getString(),
adjustmentValue
);
return;
}
// Get the item to adjust
ItemStack stack = switch (region) {
case MOUTH -> state.getEquipment(BodyRegionV2.MOUTH);
case EYES -> state.getEquipment(BodyRegionV2.EYES);
default -> ItemStack.EMPTY;
};
// Validate
if (stack.isEmpty()) {
TiedUpMod.LOGGER.debug(
"[PACKET] PacketAdjustItem: No {} equipped for {}",
region,
player.getName().getString()
);
return;
}
if (!(stack.getItem() instanceof IAdjustable)) {
TiedUpMod.LOGGER.warn(
"[PACKET] PacketAdjustItem: Item {} is not adjustable",
stack.getItem()
);
return;
}
// Apply adjustment (AdjustmentHelper clamps the value)
AdjustmentHelper.setAdjustment(stack, adjustmentValue);
AdjustmentHelper.setScale(stack, scaleValue);
TiedUpMod.LOGGER.debug(
"[PACKET] Applied adjustment {} scale {} to {} for {}",
adjustmentValue,
scaleValue,
region,
player.getName().getString()
);
// Sync inventory to all tracking players
SyncManager.syncInventory(player);
}
}

View File

@@ -0,0 +1,270 @@
package com.tiedup.remake.network.item;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.AdjustmentHelper;
import com.tiedup.remake.items.base.IAdjustable;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.network.sync.SyncManager;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.state.PlayerCaptorManager;
import java.util.UUID;
import java.util.function.Supplier;
import net.minecraft.ChatFormatting;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.network.NetworkEvent;
/**
* Packet for adjusting a slave's gag/blindfold remotely.
*
* Phase 16: GUI Revamp - Remote adjustment packet
*
* Security: Distance and dimension validation added to prevent griefing
*/
public class PacketAdjustRemote {
/** Maximum interaction range for remote adjustments (blocks) */
private static final double MAX_INTERACTION_RANGE = 100.0;
private final UUID targetId;
private final BodyRegionV2 region;
private final float adjustmentValue;
private final float scaleValue;
public PacketAdjustRemote(
UUID targetId,
BodyRegionV2 region,
float adjustmentValue,
float scaleValue
) {
this.targetId = targetId;
this.region = region;
this.adjustmentValue = adjustmentValue;
this.scaleValue = scaleValue;
}
public void encode(FriendlyByteBuf buf) {
buf.writeUUID(targetId);
buf.writeEnum(region);
buf.writeFloat(adjustmentValue);
buf.writeFloat(scaleValue);
}
public static PacketAdjustRemote decode(FriendlyByteBuf buf) {
UUID id = buf.readUUID();
BodyRegionV2 region = buf.readEnum(BodyRegionV2.class);
float value = buf.readFloat();
float scale = buf.readFloat();
return new PacketAdjustRemote(id, region, value, scale);
}
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx
.get()
.enqueueWork(() -> {
ServerPlayer sender = ctx.get().getSender();
if (sender == null) return;
// Rate limiting: Prevent adjustment spam
if (
!com.tiedup.remake.network.PacketRateLimiter.allowPacket(
sender,
"action"
)
) {
return;
}
handleServer(sender);
});
ctx.get().setPacketHandled(true);
}
private void handleServer(ServerPlayer sender) {
// Get sender's kidnapper manager
PlayerBindState senderState = PlayerBindState.getInstance(sender);
if (senderState == null) {
return;
}
// SECURITY: Validate adjustment value (prevent NaN, Infinity, out-of-bounds)
if (
Float.isNaN(adjustmentValue) ||
Float.isInfinite(adjustmentValue) ||
Float.isNaN(scaleValue) ||
Float.isInfinite(scaleValue)
) {
TiedUpMod.LOGGER.warn(
"SECURITY: Invalid adjustment value from {}: adj={}, scale={}",
sender.getName().getString(),
adjustmentValue,
scaleValue
);
return;
}
// Valid range check (-5.0 to +5.0 pixels, with margin)
if (adjustmentValue < -5.0f || adjustmentValue > 5.0f) {
TiedUpMod.LOGGER.debug(
"[PACKET] Adjustment value out of bounds from {}: {}",
sender.getName().getString(),
adjustmentValue
);
return;
}
// Find the target - first try captives, then collar-owned entities
IBondageState targetCaptive = null;
// 1. Check captives first
PlayerCaptorManager manager = senderState.getCaptorManager();
if (manager != null) {
for (IBondageState captive : manager.getCaptives()) {
LivingEntity entity = captive.asLivingEntity();
if (entity != null && entity.getUUID().equals(targetId)) {
targetCaptive = captive;
break;
}
}
}
// 2. If not found in captives, check nearby collar-owned entities
if (targetCaptive == null) {
targetCaptive = findCollarOwnedEntity(sender);
}
if (targetCaptive == null) {
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.ERROR,
"Target not found or not under your control!"
);
return;
}
LivingEntity targetEntity = targetCaptive.asLivingEntity();
if (targetEntity == null) {
TiedUpMod.LOGGER.warn(
"[PACKET] PacketAdjustRemote: Target entity is null"
);
return;
}
String targetName = targetCaptive.getKidnappedName();
// Security: Validate dimension and distance
if (sender.level() != targetEntity.level()) {
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.ERROR,
targetName + " is in a different dimension!"
);
return;
}
double distance = sender.distanceTo(targetEntity);
if (distance > MAX_INTERACTION_RANGE) {
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.ERROR,
targetName +
" is too far away! (Distance: " +
(int) distance +
" blocks, Max: " +
(int) MAX_INTERACTION_RANGE +
")"
);
return;
}
// Get the item to adjust
ItemStack stack = switch (region) {
case MOUTH -> targetCaptive.getEquipment(BodyRegionV2.MOUTH);
case EYES -> targetCaptive.getEquipment(BodyRegionV2.EYES);
default -> ItemStack.EMPTY;
};
if (stack.isEmpty()) {
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.ERROR,
"Target has no " + region.name().toLowerCase() + " item!"
);
return;
}
if (!(stack.getItem() instanceof IAdjustable)) {
TiedUpMod.LOGGER.warn(
"[PACKET] PacketAdjustRemote: Item {} is not adjustable",
stack.getItem()
);
return;
}
// Apply adjustment
AdjustmentHelper.setAdjustment(stack, adjustmentValue);
AdjustmentHelper.setScale(stack, scaleValue);
SystemMessageManager.sendToPlayer(
sender,
"Adjusted " +
targetName +
"'s " +
region.name().toLowerCase() +
" item to " +
adjustmentValue,
ChatFormatting.GREEN
);
TiedUpMod.LOGGER.debug(
"[PACKET] {} adjusted {}'s {} to {}",
sender.getName().getString(),
targetName,
region,
adjustmentValue
);
// Sync if target is a player
if (targetEntity instanceof ServerPlayer targetPlayer) {
SyncManager.syncInventory(targetPlayer);
}
}
/**
* Find a collar-owned entity by UUID.
* Searches nearby entities for one with a collar owned by the sender.
*/
private IBondageState findCollarOwnedEntity(ServerPlayer sender) {
net.minecraft.world.phys.AABB searchBox = sender
.getBoundingBox()
.inflate(32); // Security: reduced from 100
for (LivingEntity entity : sender
.level()
.getEntitiesOfClass(LivingEntity.class, searchBox)) {
if (entity == sender) continue;
if (!entity.getUUID().equals(targetId)) continue;
IBondageState kidnapped =
com.tiedup.remake.util.KidnappedHelper.getKidnappedState(
entity
);
if (kidnapped != null && kidnapped.hasCollar()) {
ItemStack collarStack = kidnapped.getEquipment(BodyRegionV2.NECK);
if (
collarStack.getItem() instanceof
com.tiedup.remake.items.base.ItemCollar collar
) {
if (collar.isOwner(collarStack, sender)) {
return kidnapped;
}
}
}
}
return null;
}
}

View File

@@ -0,0 +1,105 @@
package com.tiedup.remake.network.labor;
import com.tiedup.remake.network.base.AbstractClientPacket;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
/**
* Server-to-Client packet for synchronizing labor task progress.
* Sent when:
* - A task is assigned (start)
* - Progress is made (increment)
* - A task is completed or cancelled (clear)
*/
public class PacketSyncLaborProgress extends AbstractClientPacket {
private final boolean hasTask;
private final String taskDescription;
private final int progress;
private final int quota;
private final int valueEmeralds;
/**
* Create a packet with task data.
*/
public PacketSyncLaborProgress(
String taskDescription,
int progress,
int quota,
int valueEmeralds
) {
this.hasTask = true;
this.taskDescription = taskDescription;
this.progress = progress;
this.quota = quota;
this.valueEmeralds = valueEmeralds;
}
/**
* Create a packet to clear the task (no active task).
*/
public PacketSyncLaborProgress() {
this.hasTask = false;
this.taskDescription = "";
this.progress = 0;
this.quota = 0;
this.valueEmeralds = 0;
}
/**
* Encode packet data to buffer.
*/
public void encode(FriendlyByteBuf buf) {
buf.writeBoolean(hasTask);
if (hasTask) {
buf.writeUtf(taskDescription);
buf.writeInt(progress);
buf.writeInt(quota);
buf.writeInt(valueEmeralds);
}
}
/**
* Decode packet data from buffer.
*/
public static PacketSyncLaborProgress decode(FriendlyByteBuf buf) {
boolean hasTask = buf.readBoolean();
if (hasTask) {
String taskDescription = buf.readUtf();
int progress = buf.readInt();
int quota = buf.readInt();
int valueEmeralds = buf.readInt();
return new PacketSyncLaborProgress(
taskDescription,
progress,
quota,
valueEmeralds
);
} else {
return new PacketSyncLaborProgress();
}
}
@Override
@OnlyIn(Dist.CLIENT)
protected void handleClientImpl() {
ClientHandler.handle(this);
}
@OnlyIn(Dist.CLIENT)
private static class ClientHandler {
private static void handle(PacketSyncLaborProgress pkt) {
if (pkt.hasTask) {
com.tiedup.remake.client.state.ClientLaborState.setTask(
pkt.taskDescription,
pkt.progress,
pkt.quota,
pkt.valueEmeralds
);
} else {
com.tiedup.remake.client.state.ClientLaborState.clearTask();
}
}
}
}

View File

@@ -0,0 +1,103 @@
package com.tiedup.remake.network.master;
import com.tiedup.remake.entities.EntityMaster;
import com.tiedup.remake.entities.ai.master.MasterState;
import com.tiedup.remake.network.base.AbstractClientPacket;
import java.util.UUID;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
/**
* Packet to synchronize Master entity state to tracking clients.
*
* Direction: Server → Client (S2C)
*
* Syncs:
* - Current MasterState
* - Pet player UUID
* - Remaining distraction ticks
*/
public class PacketMasterStateSync extends AbstractClientPacket {
private final int entityId;
private final int stateOrdinal;
private final UUID petPlayerUUID;
private final long remainingDistractionTicks;
/**
* Create a state sync packet.
*
* @param entityId The master entity ID
* @param stateOrdinal The state ordinal value
* @param petPlayerUUID The pet player UUID (may be null)
* @param remainingDistractionTicks Remaining ticks of distraction
*/
public PacketMasterStateSync(
int entityId,
int stateOrdinal,
UUID petPlayerUUID,
long remainingDistractionTicks
) {
this.entityId = entityId;
this.stateOrdinal = stateOrdinal;
this.petPlayerUUID = petPlayerUUID;
this.remainingDistractionTicks = remainingDistractionTicks;
}
/**
* Encode the packet to buffer.
*/
public void encode(FriendlyByteBuf buf) {
buf.writeInt(entityId);
buf.writeInt(stateOrdinal);
buf.writeBoolean(petPlayerUUID != null);
if (petPlayerUUID != null) {
buf.writeUUID(petPlayerUUID);
}
buf.writeLong(remainingDistractionTicks);
}
/**
* Decode the packet from buffer.
*/
public static PacketMasterStateSync decode(FriendlyByteBuf buf) {
int entityId = buf.readInt();
int stateOrdinal = buf.readInt();
UUID petPlayerUUID = null;
if (buf.readBoolean()) {
petPlayerUUID = buf.readUUID();
}
long remainingDistractionTicks = buf.readLong();
return new PacketMasterStateSync(
entityId,
stateOrdinal,
petPlayerUUID,
remainingDistractionTicks
);
}
/**
* Handle packet on client side.
*/
@Override
@OnlyIn(Dist.CLIENT)
protected void handleClientImpl() {
ClientHandler.handle(this);
}
@OnlyIn(Dist.CLIENT)
private static class ClientHandler {
private static void handle(PacketMasterStateSync pkt) {
net.minecraft.client.Minecraft mc = net.minecraft.client.Minecraft.getInstance();
if (mc.level == null) return;
net.minecraft.world.entity.Entity entity = mc.level.getEntity(pkt.entityId);
if (entity instanceof EntityMaster master) {
// Apply state sync
// Note: Client-side state is read-only via synced entity data
// This packet is for extended state info like distraction timer
}
}
}
}

View File

@@ -0,0 +1,76 @@
package com.tiedup.remake.network.master;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.network.NetworkEvent;
/**
* Packet sent from server to client to open the pet request menu.
*
* Direction: Server → Client (S2C)
*
* Contains:
* - Master entity ID
* - Master name (for display)
*/
public class PacketOpenPetRequestMenu {
private final int entityId;
private final String masterName;
/**
* Create a packet to open the pet request menu.
*
* @param entityId The master entity ID
* @param masterName The master's display name
*/
public PacketOpenPetRequestMenu(int entityId, String masterName) {
this.entityId = entityId;
this.masterName = masterName;
}
/**
* Encode the packet to buffer.
*/
public void encode(FriendlyByteBuf buf) {
buf.writeInt(entityId);
buf.writeUtf(masterName, 64);
}
/**
* Decode the packet from buffer.
*/
public static PacketOpenPetRequestMenu decode(FriendlyByteBuf buf) {
int entityId = buf.readInt();
String masterName = buf.readUtf(64);
return new PacketOpenPetRequestMenu(entityId, masterName);
}
/**
* Handle the packet on the network thread.
*/
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx.get().enqueueWork(() -> handleClient());
ctx.get().setPacketHandled(true);
}
/**
* Client-side handling.
* This method is only called on the client, avoiding class loading issues.
*/
private void handleClient() {
// Delegate to client-only helper to avoid class loading on server
com.tiedup.remake.client.network.ClientPacketHandler.openPetRequestScreen(
entityId,
masterName
);
}
public int getEntityId() {
return entityId;
}
public String getMasterName() {
return masterName;
}
}

View File

@@ -0,0 +1,145 @@
package com.tiedup.remake.network.master;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.conversation.PetRequest;
import com.tiedup.remake.dialogue.conversation.PetRequestManager;
import com.tiedup.remake.entities.EntityMaster;
import com.tiedup.remake.network.PacketRateLimiter;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraftforge.network.NetworkEvent;
/**
* Packet sent from client to server when player selects a pet request option.
*
* Direction: Client → Server (C2S)
*
* Contains:
* - Master entity ID
* - Request type (enum name)
*/
public class PacketPetRequest {
/** Maximum distance for request interaction */
private static final double MAX_DISTANCE = 8.0;
private final int entityId;
private final String requestName;
/**
* Create a pet request packet.
*
* @param entityId The master entity ID
* @param request The request type
*/
public PacketPetRequest(int entityId, PetRequest request) {
this.entityId = entityId;
this.requestName = request.name();
}
private PacketPetRequest(int entityId, String requestName) {
this.entityId = entityId;
this.requestName = requestName;
}
/**
* Encode the packet to buffer.
*/
public void encode(FriendlyByteBuf buf) {
buf.writeInt(entityId);
buf.writeUtf(requestName, 64);
}
/**
* Decode the packet from buffer.
*/
public static PacketPetRequest decode(FriendlyByteBuf buf) {
int entityId = buf.readInt();
String requestName = buf.readUtf(64);
return new PacketPetRequest(entityId, requestName);
}
/**
* Handle the packet on server side.
*/
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx
.get()
.enqueueWork(() -> {
ServerPlayer sender = ctx.get().getSender();
if (sender == null) return;
handleServer(sender);
});
ctx.get().setPacketHandled(true);
}
private void handleServer(ServerPlayer sender) {
// MEDIUM FIX: Rate limiting to prevent pet request spam
if (!PacketRateLimiter.allowPacket(sender, "action")) {
return;
}
// Find the master entity
Entity entity = sender.level().getEntity(entityId);
if (entity == null) {
TiedUpMod.LOGGER.warn(
"[PacketPetRequest] Entity {} not found",
entityId
);
return;
}
if (!(entity instanceof EntityMaster master)) {
TiedUpMod.LOGGER.warn(
"[PacketPetRequest] Entity {} is not a Master",
entityId
);
return;
}
// Validate distance
double distance = sender.distanceTo(entity);
if (distance > MAX_DISTANCE) {
TiedUpMod.LOGGER.warn(
"[PacketPetRequest] Player {} too far from master {} ({})",
sender.getName().getString(),
entityId,
distance
);
return;
}
// Parse request
PetRequest request;
try {
request = PetRequest.valueOf(requestName);
} catch (IllegalArgumentException e) {
TiedUpMod.LOGGER.warn(
"[PacketPetRequest] Unknown request: {}",
requestName
);
return;
}
TiedUpMod.LOGGER.info(
"[PacketPetRequest] {} sent request {} to {}",
sender.getName().getString(),
request.name(),
master.getNpcName()
);
// Handle the request
PetRequestManager.handleRequest(master, sender, request);
}
public int getEntityId() {
return entityId;
}
public String getRequestName() {
return requestName;
}
}

View File

@@ -0,0 +1,88 @@
package com.tiedup.remake.network.merchant;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.EntityKidnapperMerchant;
import com.tiedup.remake.network.PacketRateLimiter;
import java.util.UUID;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraftforge.network.NetworkEvent;
/**
* Packet sent from client to server when player closes the merchant trading screen.
*
* Contains:
* - Merchant entity ID
*/
public class PacketCloseMerchantScreen {
private final UUID merchantUUID;
/**
* Create packet to notify merchant screen closed.
*
* @param merchantUUID The UUID of the merchant entity (persistent across restarts)
*/
public PacketCloseMerchantScreen(UUID merchantUUID) {
this.merchantUUID = merchantUUID;
}
/**
* Encode the packet to the network buffer.
*/
public void encode(FriendlyByteBuf buf) {
buf.writeUUID(merchantUUID);
}
/**
* Decode the packet from the network buffer.
*/
public static PacketCloseMerchantScreen decode(FriendlyByteBuf buf) {
UUID uuid = buf.readUUID();
return new PacketCloseMerchantScreen(uuid);
}
/**
* Handle the packet on the receiving side (SERVER SIDE).
*/
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx
.get()
.enqueueWork(() -> {
ServerPlayer player = ctx.get().getSender();
if (player == null) {
return;
}
handleServer(player);
});
ctx.get().setPacketHandled(true);
}
/**
* Handle the packet on the server side.
* Marks that the player is no longer trading with the merchant.
*/
private void handleServer(ServerPlayer player) {
if (!PacketRateLimiter.allowPacket(player, "action")) return;
// Find the merchant entity by UUID (persistent across restarts)
Entity entity = (
(net.minecraft.server.level.ServerLevel) player.level()
).getEntity(merchantUUID);
if (!(entity instanceof EntityKidnapperMerchant merchant)) {
TiedUpMod.LOGGER.warn(
"[PacketCloseMerchantScreen] Entity {} is not a merchant or not found",
merchantUUID
);
return;
}
// Always clean up trading state — this is a teardown packet.
// Distance check removed: blocking cleanup causes permanent state leak
// in tradingPlayers map (reviewer H18 BUG-002).
merchant.stopTrading(player.getUUID());
}
}

View File

@@ -0,0 +1,105 @@
package com.tiedup.remake.network.merchant;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.MerchantTrade;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.function.Supplier;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.fml.loading.FMLEnvironment;
import net.minecraftforge.network.NetworkEvent;
/**
* Packet sent from server to client to open the merchant trading screen.
*
* Contains:
* - Merchant entity ID
* - List of available trades
*/
public class PacketOpenMerchantScreen {
private final UUID merchantUUID;
private final List<MerchantTrade> trades;
/**
* Create packet to open merchant screen.
*
* @param merchantUUID The UUID of the merchant entity (persistent across restarts)
* @param trades List of available trades
*/
public PacketOpenMerchantScreen(
UUID merchantUUID,
List<MerchantTrade> trades
) {
this.merchantUUID = merchantUUID;
this.trades = new ArrayList<>(trades);
}
/**
* Encode the packet to the network buffer.
*/
public void encode(FriendlyByteBuf buf) {
buf.writeUUID(merchantUUID);
buf.writeInt(trades.size());
for (MerchantTrade trade : trades) {
buf.writeNbt(trade.save());
}
}
/**
* Decode the packet from the network buffer.
*/
public static PacketOpenMerchantScreen decode(FriendlyByteBuf buf) {
UUID uuid = buf.readUUID();
int tradeCount = buf.readInt();
List<MerchantTrade> trades = new ArrayList<>();
for (int i = 0; i < tradeCount; i++) {
CompoundTag tag = buf.readNbt();
if (tag != null) {
trades.add(MerchantTrade.load(tag));
}
}
return new PacketOpenMerchantScreen(uuid, trades);
}
/**
* Handle the packet on the receiving side (CLIENT SIDE).
*/
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx
.get()
.enqueueWork(() -> {
if (FMLEnvironment.dist == Dist.CLIENT) {
handleClient();
}
});
ctx.get().setPacketHandled(true);
}
/**
* Handle the packet on the client side.
* Opens the merchant trading screen.
*/
@OnlyIn(Dist.CLIENT)
private void handleClient() {
net.minecraft.client.Minecraft mc =
net.minecraft.client.Minecraft.getInstance();
mc.setScreen(
new com.tiedup.remake.client.gui.screens.MerchantTradingScreen(
merchantUUID,
trades
)
);
TiedUpMod.LOGGER.debug(
"[PacketOpenMerchantScreen] Opening merchant screen with {} trades",
trades.size()
);
}
}

View File

@@ -0,0 +1,234 @@
package com.tiedup.remake.network.merchant;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.EntityKidnapperMerchant;
import com.tiedup.remake.entities.MerchantTrade;
import java.util.List;
import java.util.UUID;
import java.util.function.Supplier;
import net.minecraft.ChatFormatting;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraftforge.network.NetworkEvent;
/**
* Packet sent from client to server when player attempts to purchase a trade.
*
* Contains:
* - Merchant entity ID
* - Trade index (which trade to purchase)
*/
public class PacketPurchaseTrade {
private final UUID merchantUUID;
private final int tradeIndex;
/**
* Create packet to purchase a trade.
*
* @param merchantUUID The UUID of the merchant entity (persistent across restarts)
* @param tradeIndex Index of the trade to purchase (0-based)
*/
public PacketPurchaseTrade(UUID merchantUUID, int tradeIndex) {
this.merchantUUID = merchantUUID;
this.tradeIndex = tradeIndex;
}
/**
* Encode the packet to the network buffer.
*/
public void encode(FriendlyByteBuf buf) {
buf.writeUUID(merchantUUID);
buf.writeInt(tradeIndex);
}
/**
* Decode the packet from the network buffer.
*/
public static PacketPurchaseTrade decode(FriendlyByteBuf buf) {
UUID uuid = buf.readUUID();
int tradeIndex = buf.readInt();
return new PacketPurchaseTrade(uuid, tradeIndex);
}
/**
* Handle the packet on the receiving side (SERVER SIDE).
*/
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx
.get()
.enqueueWork(() -> {
ServerPlayer player = ctx.get().getSender();
if (player == null) {
return;
}
// CRITICAL FIX: Add rate limiting to prevent DoS via packet spam
if (
!com.tiedup.remake.network.PacketRateLimiter.allowPacket(
player,
"action"
)
) {
return;
}
handleServer(player);
});
ctx.get().setPacketHandled(true);
}
/**
* Handle the packet on the server side.
* Validates and executes the trade.
*/
private void handleServer(ServerPlayer player) {
// Find the merchant entity by UUID (persistent across restarts)
Entity entity = (
(net.minecraft.server.level.ServerLevel) player.level()
).getEntity(merchantUUID);
if (!(entity instanceof EntityKidnapperMerchant merchant)) {
TiedUpMod.LOGGER.warn(
"[PacketPurchaseTrade] Entity {} is not a merchant or not found",
merchantUUID
);
return;
}
// Validate merchant is in range (10 blocks)
if (player.distanceTo(merchant) > 10.0) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
"Merchant is too far away!"
);
return;
}
// Validate merchant is in merchant mode
if (!merchant.isMerchant()) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
"The merchant is too angry to trade!"
);
return;
}
// Get the trade
List<MerchantTrade> trades = merchant.getTrades();
if (tradeIndex < 0 || tradeIndex >= trades.size()) {
TiedUpMod.LOGGER.warn(
"[PacketPurchaseTrade] Invalid trade index: {}",
tradeIndex
);
return;
}
MerchantTrade trade = trades.get(tradeIndex);
// Count total gold in player inventory (across all slots)
int totalIngots = countGoldIngots(player);
int totalNuggets = countGoldNuggets(player);
// Check if player can afford
if (
totalIngots < trade.getIngotPrice() ||
totalNuggets < trade.getNuggetPrice()
) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
"Not enough gold!"
);
return;
}
// Consume payment from multiple slots if needed
consumeGoldIngots(player, trade.getIngotPrice());
consumeGoldNuggets(player, trade.getNuggetPrice());
// Give item to player
ItemStack purchasedItem = trade.getItem().copy();
if (!player.getInventory().add(purchasedItem)) {
// Inventory full, drop at feet
player.drop(purchasedItem, false);
}
// Success message
SystemMessageManager.sendChatToPlayer(
player,
"Purchased: " + trade.getItemName().getString(),
ChatFormatting.GREEN
);
// Merchant response
merchant.talkToPlayersInRadius("Pleasure doing business!", 10);
TiedUpMod.LOGGER.info(
"[PacketPurchaseTrade] {} purchased {} from merchant",
player.getName().getString(),
trade.getItemName().getString()
);
}
/**
* Count total gold ingots across all inventory slots.
*/
private int countGoldIngots(ServerPlayer player) {
int total = 0;
for (ItemStack stack : player.getInventory().items) {
if (stack.is(Items.GOLD_INGOT)) {
total += stack.getCount();
}
}
return total;
}
/**
* Count total gold nuggets across all inventory slots.
*/
private int countGoldNuggets(ServerPlayer player) {
int total = 0;
for (ItemStack stack : player.getInventory().items) {
if (stack.is(Items.GOLD_NUGGET)) {
total += stack.getCount();
}
}
return total;
}
/**
* Consume gold ingots from inventory (multiple slots if needed).
*/
private void consumeGoldIngots(ServerPlayer player, int amount) {
int remaining = amount;
for (ItemStack stack : player.getInventory().items) {
if (remaining <= 0) break;
if (stack.is(Items.GOLD_INGOT)) {
int toRemove = Math.min(remaining, stack.getCount());
stack.shrink(toRemove);
remaining -= toRemove;
}
}
}
/**
* Consume gold nuggets from inventory (multiple slots if needed).
*/
private void consumeGoldNuggets(ServerPlayer player, int amount) {
int remaining = amount;
for (ItemStack stack : player.getInventory().items) {
if (remaining <= 0) break;
if (stack.is(Items.GOLD_NUGGET)) {
int toRemove = Math.min(remaining, stack.getCount());
stack.shrink(toRemove);
remaining -= toRemove;
}
}
}
}

View File

@@ -0,0 +1,109 @@
package com.tiedup.remake.network.minigame;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.minigame.ContinuousStruggleMiniGameState;
import com.tiedup.remake.minigame.StruggleSessionManager;
import com.tiedup.remake.network.PacketRateLimiter;
import java.util.UUID;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.network.NetworkEvent;
/**
* Client to Server packet for continuous struggle key hold state.
*
* Sent every 5 ticks (4 times per second) while the struggle screen is open.
* Reports which direction key the player is holding (or -1 if none).
*/
public class PacketContinuousStruggleHold {
private final UUID sessionId;
private final int heldDirection; // -1 if not holding any direction key
private final boolean isHolding;
public PacketContinuousStruggleHold(
UUID sessionId,
int heldDirection,
boolean isHolding
) {
this.sessionId = sessionId;
this.heldDirection = heldDirection;
this.isHolding = isHolding;
}
public void encode(FriendlyByteBuf buf) {
buf.writeUUID(sessionId);
buf.writeVarInt(heldDirection);
buf.writeBoolean(isHolding);
}
public static PacketContinuousStruggleHold decode(FriendlyByteBuf buf) {
UUID sessionId = buf.readUUID();
int heldDirection = buf.readVarInt();
boolean isHolding = buf.readBoolean();
return new PacketContinuousStruggleHold(
sessionId,
heldDirection,
isHolding
);
}
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx
.get()
.enqueueWork(() -> {
ServerPlayer player = ctx.get().getSender();
if (player == null) {
return;
}
handleServer(player);
});
ctx.get().setPacketHandled(true);
}
private void handleServer(ServerPlayer player) {
// HIGH FIX #18: Rate limiting to prevent packet spam exploit
// Designed for 4/sec, allow up to 12/sec with buffer
if (!PacketRateLimiter.allowPacket(player, "minigame")) {
return;
}
StruggleSessionManager manager = StruggleSessionManager.getInstance();
ContinuousStruggleMiniGameState session =
manager.getContinuousStruggleSession(player.getUUID());
if (session == null) {
TiedUpMod.LOGGER.debug(
"[PacketContinuousStruggleHold] No active session for {}",
player.getName().getString()
);
return;
}
// Validate session ID
if (!session.getSessionId().equals(sessionId)) {
TiedUpMod.LOGGER.debug(
"[PacketContinuousStruggleHold] Session ID mismatch for {}",
player.getName().getString()
);
return;
}
// Update held direction
session.updateHeldDirection(heldDirection, isHolding);
}
// Getters
public UUID getSessionId() {
return sessionId;
}
public int getHeldDirection() {
return heldDirection;
}
public boolean isHolding() {
return isHolding;
}
}

View File

@@ -0,0 +1,177 @@
package com.tiedup.remake.network.minigame;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.minigame.ContinuousStruggleMiniGameState.UpdateType;
import com.tiedup.remake.network.base.AbstractClientPacket;
import java.util.UUID;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
/**
* Server to Client packet for continuous struggle mini-game state updates.
*
* Sent when:
* - START: Session begins (opens the GUI)
* - DIRECTION_CHANGE: Direction changed
* - RESISTANCE_UPDATE: Resistance reduced
* - SHOCK: Shock collar triggered
* - ESCAPE: Player escaped successfully
* - END: Session ended (cancelled or completed)
*/
public class PacketContinuousStruggleState extends AbstractClientPacket {
private final UUID sessionId;
private final UpdateType updateType;
private final int currentDirection;
private final int currentResistance;
private final int maxResistance;
private final boolean isLocked;
public PacketContinuousStruggleState(
UUID sessionId,
UpdateType updateType,
int currentDirection,
int currentResistance,
int maxResistance,
boolean isLocked
) {
this.sessionId = sessionId;
this.updateType = updateType;
this.currentDirection = currentDirection;
this.currentResistance = currentResistance;
this.maxResistance = maxResistance;
this.isLocked = isLocked;
}
public void encode(FriendlyByteBuf buf) {
buf.writeUUID(sessionId);
buf.writeVarInt(updateType.ordinal());
buf.writeVarInt(currentDirection);
buf.writeVarInt(currentResistance);
buf.writeVarInt(maxResistance);
buf.writeBoolean(isLocked);
}
public static PacketContinuousStruggleState decode(FriendlyByteBuf buf) {
UUID sessionId = buf.readUUID();
UpdateType updateType = UpdateType.values()[buf.readVarInt()];
int currentDirection = buf.readVarInt();
int currentResistance = buf.readVarInt();
int maxResistance = buf.readVarInt();
boolean isLocked = buf.readBoolean();
return new PacketContinuousStruggleState(
sessionId,
updateType,
currentDirection,
currentResistance,
maxResistance,
isLocked
);
}
@Override
@OnlyIn(Dist.CLIENT)
protected void handleClientImpl() {
ClientHandler.handle(this);
}
@OnlyIn(Dist.CLIENT)
private static class ClientHandler {
private static void handle(PacketContinuousStruggleState pkt) {
net.minecraft.client.Minecraft mc = net.minecraft.client.Minecraft.getInstance();
if (mc.player == null) {
TiedUpMod.LOGGER.warn(
"[PacketContinuousStruggleState] Player is null, cannot handle packet"
);
return;
}
TiedUpMod.LOGGER.info(
"[PacketContinuousStruggleState] Received update: type={}, direction={}, resistance={}/{}",
pkt.updateType,
pkt.currentDirection,
pkt.currentResistance,
pkt.maxResistance
);
switch (pkt.updateType) {
case START -> {
// Open the continuous struggle screen
TiedUpMod.LOGGER.info(
"[PacketContinuousStruggleState] Opening ContinuousStruggleMiniGameScreen"
);
mc.setScreen(
new com.tiedup.remake.client.gui.screens.ContinuousStruggleMiniGameScreen(
pkt.sessionId,
pkt.currentDirection,
pkt.currentResistance,
pkt.maxResistance,
pkt.isLocked
)
);
}
case DIRECTION_CHANGE -> {
if (
mc.screen instanceof com.tiedup.remake.client.gui.screens.ContinuousStruggleMiniGameScreen screen
) {
screen.onDirectionChange(pkt.currentDirection);
}
}
case RESISTANCE_UPDATE -> {
if (
mc.screen instanceof com.tiedup.remake.client.gui.screens.ContinuousStruggleMiniGameScreen screen
) {
screen.onResistanceUpdate(pkt.currentResistance);
}
}
case SHOCK -> {
if (
mc.screen instanceof com.tiedup.remake.client.gui.screens.ContinuousStruggleMiniGameScreen screen
) {
screen.onShock();
}
}
case ESCAPE -> {
if (
mc.screen instanceof com.tiedup.remake.client.gui.screens.ContinuousStruggleMiniGameScreen screen
) {
screen.onEscape();
}
}
case END -> {
if (
mc.screen instanceof com.tiedup.remake.client.gui.screens.ContinuousStruggleMiniGameScreen screen
) {
screen.onEnd();
}
}
}
}
}
// Getters for potential external use
public UUID getSessionId() {
return sessionId;
}
public UpdateType getUpdateType() {
return updateType;
}
public int getCurrentDirection() {
return currentDirection;
}
public int getCurrentResistance() {
return currentResistance;
}
public int getMaxResistance() {
return maxResistance;
}
public boolean isLocked() {
return isLocked;
}
}

View File

@@ -0,0 +1,99 @@
package com.tiedup.remake.network.minigame;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.minigame.ContinuousStruggleMiniGameState;
import com.tiedup.remake.minigame.StruggleSessionManager;
import com.tiedup.remake.network.PacketRateLimiter;
import com.tiedup.remake.network.sync.SyncManager;
import com.tiedup.remake.state.PlayerBindState;
import java.util.UUID;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.network.NetworkEvent;
/**
* Client to Server packet to stop the continuous struggle session.
*
* Sent when:
* - Player presses ESC to close the screen
* - Player takes damage (detected client-side)
*/
public class PacketContinuousStruggleStop {
private final UUID sessionId;
public PacketContinuousStruggleStop(UUID sessionId) {
this.sessionId = sessionId;
}
public void encode(FriendlyByteBuf buf) {
buf.writeUUID(sessionId);
}
public static PacketContinuousStruggleStop decode(FriendlyByteBuf buf) {
UUID sessionId = buf.readUUID();
return new PacketContinuousStruggleStop(sessionId);
}
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx
.get()
.enqueueWork(() -> {
ServerPlayer player = ctx.get().getSender();
if (player == null) {
return;
}
handleServer(player);
});
ctx.get().setPacketHandled(true);
}
private void handleServer(ServerPlayer player) {
if (!PacketRateLimiter.allowPacket(player, "minigame")) return;
StruggleSessionManager manager = StruggleSessionManager.getInstance();
ContinuousStruggleMiniGameState session =
manager.getContinuousStruggleSession(player.getUUID());
if (session == null) {
TiedUpMod.LOGGER.debug(
"[PacketContinuousStruggleStop] No active session for {}",
player.getName().getString()
);
return;
}
// Validate session ID
if (!session.getSessionId().equals(sessionId)) {
TiedUpMod.LOGGER.debug(
"[PacketContinuousStruggleStop] Session ID mismatch for {}",
player.getName().getString()
);
return;
}
TiedUpMod.LOGGER.info(
"[PacketContinuousStruggleStop] Player {} stopped struggle session",
player.getName().getString()
);
// Cancel the session
session.cancel();
// Clear struggle animation state and sync
PlayerBindState state = PlayerBindState.getInstance(player);
if (state != null) {
state.setStruggling(false, 0);
SyncManager.syncStruggleState(player);
}
// End the session in the manager
manager.endContinuousStruggleSession(player.getUUID(), false);
}
// Getter
public UUID getSessionId() {
return sessionId;
}
}

View File

@@ -0,0 +1,447 @@
package com.tiedup.remake.network.minigame;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.ItemLockpick;
import com.tiedup.remake.items.base.ILockable;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import com.tiedup.remake.v2.furniture.EntityFurniture;
import com.tiedup.remake.v2.furniture.FurnitureDefinition;
import com.tiedup.remake.v2.furniture.network.PacketSyncFurnitureState;
import com.tiedup.remake.minigame.LockpickMiniGameState;
import com.tiedup.remake.minigame.LockpickMiniGameState.PickAttemptResult;
import com.tiedup.remake.minigame.LockpickSessionManager;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.PacketRateLimiter;
import com.tiedup.remake.network.sync.SyncManager;
import com.tiedup.remake.state.PlayerBindState;
import java.util.UUID;
import java.util.function.Supplier;
import net.minecraft.ChatFormatting;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.network.NetworkEvent;
/**
* Packet for lockpick attempt at current position (Client to Server).
*
* Sent when player presses SPACE to attempt picking at current position.
*/
public class PacketLockpickAttempt {
private final UUID sessionId;
public PacketLockpickAttempt(UUID sessionId) {
this.sessionId = sessionId;
}
public void encode(FriendlyByteBuf buf) {
buf.writeUUID(sessionId);
}
public static PacketLockpickAttempt decode(FriendlyByteBuf buf) {
UUID sessionId = buf.readUUID();
return new PacketLockpickAttempt(sessionId);
}
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx
.get()
.enqueueWork(() -> {
ServerPlayer player = ctx.get().getSender();
if (player == null) {
return;
}
handleServer(player);
});
ctx.get().setPacketHandled(true);
}
private void handleServer(ServerPlayer player) {
// Rate limiting to prevent lockpick attempt spam exploit
if (!PacketRateLimiter.allowPacket(player, "minigame")) {
return;
}
LockpickSessionManager manager = LockpickSessionManager.getInstance();
// Validate session
if (!manager.validateLockpickSession(player.getUUID(), sessionId)) {
TiedUpMod.LOGGER.warn(
"[PacketLockpickAttempt] Invalid session {} for player {}",
sessionId.toString().substring(0, 8),
player.getName().getString()
);
return;
}
LockpickMiniGameState session = manager.getLockpickSession(
player.getUUID()
);
if (session == null || session.isComplete()) {
return;
}
// Play sound and notify guards
manager.onLockpickAttempt(player);
// Attempt to pick
PickAttemptResult result = session.attemptPick();
TiedUpMod.LOGGER.debug(
"[PacketLockpickAttempt] Player {} attempted pick, result: {}",
player.getName().getString(),
result
);
// Handle result
switch (result) {
case SUCCESS -> handleSuccess(player, session);
case OUT_OF_PICKS -> handleOutOfPicks(player, session);
case MISSED -> handleMissed(player, session);
default -> {
// Session already complete
}
}
}
private void handleSuccess(
ServerPlayer player,
LockpickMiniGameState session
) {
// Check for furniture lockpick context FIRST — if present, this is a
// furniture seat lockpick, not a body item lockpick. The context tag is
// written by PacketFurnitureEscape.handleLockpick() when starting the session.
CompoundTag furnitureCtx = player.getPersistentData()
.getCompound("tiedup_furniture_lockpick_ctx");
if (furnitureCtx != null && furnitureCtx.contains("furniture_id")) {
// H18: Distance check BEFORE ending session — prevents consuming session
// without reward if player moved away (reviewer H18 RISK-001)
int furnitureId = furnitureCtx.getInt("furniture_id");
Entity furnitureEntity = player.level().getEntity(furnitureId);
if (furnitureEntity == null || player.distanceTo(furnitureEntity) > 10.0) {
return;
}
// Session validated — now end it
LockpickSessionManager.getInstance().endLockpickSession(
player.getUUID(),
true
);
handleFurnitureLockpickSuccess(player, furnitureCtx);
player.getPersistentData().remove("tiedup_furniture_lockpick_ctx");
damageLockpick(player);
// Send result to client
ModNetwork.sendToPlayer(
new PacketLockpickMiniGameResult(
session.getSessionId(),
PacketLockpickMiniGameResult.ResultType.SUCCESS,
0
),
player
);
return;
}
// Body item lockpick — self-targeting, no distance check needed
LockpickSessionManager.getInstance().endLockpickSession(
player.getUUID(),
true
);
// Body item lockpick path: targetSlot stores BodyRegionV2 ordinal
BodyRegionV2 targetRegion =
BodyRegionV2.values()[session.getTargetSlot()];
ItemStack targetStack = V2EquipmentHelper.getInRegion(
player,
targetRegion
);
if (
!targetStack.isEmpty() &&
targetStack.getItem() instanceof ILockable lockable
) {
// Get lock resistance BEFORE clearing it
int lockResistance = lockable.getCurrentLockResistance(targetStack);
// Unlock the item
lockable.setLockedByKeyUUID(targetStack, null);
lockable.clearLockResistance(targetStack);
TiedUpMod.LOGGER.info(
"[PacketLockpickAttempt] Player {} successfully picked lock on {} ({}, resistance was {})",
player.getName().getString(),
targetStack.getDisplayName().getString(),
targetRegion,
lockResistance
);
// Deduct lock resistance from bind resistance
PlayerBindState state = PlayerBindState.getInstance(player);
if (state != null && state.isTiedUp() && lockResistance > 0) {
int currentBindResistance = state.getCurrentBindResistance();
int newBindResistance = Math.max(
0,
currentBindResistance - lockResistance
);
state.setCurrentBindResistance(newBindResistance);
TiedUpMod.LOGGER.info(
"[PacketLockpickAttempt] Deducted {} from bind resistance: {} -> {}",
lockResistance,
currentBindResistance,
newBindResistance
);
// Check if player escaped (resistance = 0)
if (newBindResistance <= 0) {
state.getStruggleBinds().successActionExternal(state);
TiedUpMod.LOGGER.info(
"[PacketLockpickAttempt] Player {} escaped via lockpick!",
player.getName().getString()
);
}
}
}
// Damage lockpick
damageLockpick(player);
// Sync to all players so unlock is visible immediately
SyncManager.syncInventory(player);
// Send result to client
ModNetwork.sendToPlayer(
new PacketLockpickMiniGameResult(
session.getSessionId(),
PacketLockpickMiniGameResult.ResultType.SUCCESS,
0
),
player
);
}
/**
* Handle a successful furniture seat lockpick: unlock the seat, dismount
* the passenger, play the unlock sound, and broadcast the updated state.
*/
private void handleFurnitureLockpickSuccess(
ServerPlayer player,
CompoundTag ctx
) {
int furnitureEntityId = ctx.getInt("furniture_id");
String seatId = ctx.getString("seat_id");
Entity entity = player.level().getEntity(furnitureEntityId);
if (!(entity instanceof EntityFurniture furniture)) {
TiedUpMod.LOGGER.warn(
"[PacketLockpickAttempt] Furniture entity {} not found or wrong type for lockpick success",
furnitureEntityId
);
return;
}
// Unlock the seat
furniture.setSeatLocked(seatId, false);
// Dismount the passenger in that seat
Entity passenger = furniture.findPassengerInSeat(seatId);
if (passenger != null) {
// Clear reconnection tag before dismount
if (passenger instanceof ServerPlayer passengerPlayer) {
passengerPlayer.getPersistentData().remove("tiedup_locked_furniture");
}
passenger.stopRiding();
}
// Play unlock sound from the furniture definition
FurnitureDefinition def = furniture.getDefinition();
if (def != null && def.feedback().unlockSound() != null) {
player.level().playSound(
null,
entity.getX(), entity.getY(), entity.getZ(),
SoundEvent.createVariableRangeEvent(def.feedback().unlockSound()),
SoundSource.BLOCKS, 1.0f, 1.0f
);
}
// Broadcast updated lock/anim state to all tracking clients
PacketSyncFurnitureState.sendToTracking(furniture);
TiedUpMod.LOGGER.info(
"[PacketLockpickAttempt] Player {} picked furniture lock on entity {} seat '{}'",
player.getName().getString(), furnitureEntityId, seatId
);
}
private void handleOutOfPicks(
ServerPlayer player,
LockpickMiniGameState session
) {
LockpickSessionManager.getInstance().endLockpickSession(
player.getUUID(),
false
);
// Destroy the lockpick
ItemStack lockpickStack = ItemLockpick.findLockpickInInventory(player);
if (!lockpickStack.isEmpty()) {
lockpickStack.shrink(1);
}
TiedUpMod.LOGGER.info(
"[PacketLockpickAttempt] Player {} ran out of lockpicks",
player.getName().getString()
);
// Trigger shock if wearing shock collar
triggerShockIfCollar(player);
// Send result to client
ModNetwork.sendToPlayer(
new PacketLockpickMiniGameResult(
session.getSessionId(),
PacketLockpickMiniGameResult.ResultType.OUT_OF_PICKS,
0
),
player
);
}
private void handleMissed(
ServerPlayer player,
LockpickMiniGameState session
) {
// Calculate distance BEFORE damaging (for animation feedback)
float distance = session.getDistanceToSweetSpot();
// Damage lockpick
damageLockpick(player);
// Update remaining uses from actual lockpick
int remainingUses = 0;
ItemStack lockpickStack = ItemLockpick.findLockpickInInventory(player);
if (!lockpickStack.isEmpty()) {
remainingUses =
lockpickStack.getMaxDamage() - lockpickStack.getDamageValue();
}
session.setRemainingUses(remainingUses);
// Check for JAM (5% chance on miss) — only applies to body item lockpick sessions.
// Furniture seat locks do not have a jam mechanic (there is no ILockable item to jam).
boolean jammed = false;
boolean isFurnitureSession = player.getPersistentData()
.getCompound("tiedup_furniture_lockpick_ctx")
.contains("furniture_id");
if (!isFurnitureSession && player.getRandom().nextFloat() < 0.05f) {
int targetSlot = session.getTargetSlot();
if (targetSlot >= 0 && targetSlot < BodyRegionV2.values().length) {
BodyRegionV2 targetRegion = BodyRegionV2.values()[targetSlot];
ItemStack targetStack = V2EquipmentHelper.getInRegion(
player,
targetRegion
);
if (
!targetStack.isEmpty() &&
targetStack.getItem() instanceof ILockable lockable
) {
lockable.setJammed(targetStack, true);
jammed = true;
player.sendSystemMessage(
Component.literal(
"The lock jammed! Only struggle can open it now."
).withStyle(ChatFormatting.RED)
);
TiedUpMod.LOGGER.info(
"[PacketLockpickAttempt] Player {} jammed the lock on {} ({})",
player.getName().getString(),
targetStack.getDisplayName().getString(),
targetRegion
);
// End session since lock is jammed
LockpickSessionManager.getInstance().endLockpickSession(
player.getUUID(),
false
);
// Sync jam state to all players
SyncManager.syncInventory(player);
}
}
}
// Trigger shock if wearing shock collar
triggerShockIfCollar(player);
// Send result to client with distance for animation
if (jammed) {
// Send cancelled result since lock is jammed
ModNetwork.sendToPlayer(
new PacketLockpickMiniGameResult(
session.getSessionId(),
PacketLockpickMiniGameResult.ResultType.CANCELLED,
0,
distance
),
player
);
} else {
ModNetwork.sendToPlayer(
new PacketLockpickMiniGameResult(
session.getSessionId(),
PacketLockpickMiniGameResult.ResultType.MISSED,
remainingUses,
distance
),
player
);
}
}
/**
* Damage the player's lockpick by 1 durability. If durability is exhausted,
* the lockpick item is consumed (shrunk). Shared by handleSuccess, handleMissed,
* and handleFurnitureLockpickSuccess paths.
*/
private void damageLockpick(ServerPlayer player) {
ItemStack lockpickStack = ItemLockpick.findLockpickInInventory(player);
if (!lockpickStack.isEmpty()) {
lockpickStack.setDamageValue(lockpickStack.getDamageValue() + 1);
if (lockpickStack.getDamageValue() >= lockpickStack.getMaxDamage()) {
lockpickStack.shrink(1);
}
}
}
private void triggerShockIfCollar(ServerPlayer player) {
PlayerBindState state = PlayerBindState.getInstance(player);
if (state == null) return;
ItemStack collar = V2EquipmentHelper.getInRegion(
player, BodyRegionV2.NECK
);
if (collar.isEmpty()) return;
if (
collar.getItem() instanceof com.tiedup.remake.items.ItemShockCollar
) {
state.shockKidnapped(" (Failed lockpick attempt)", 2.0f);
TiedUpMod.LOGGER.info(
"[PacketLockpickAttempt] Player {} shocked for failed lockpick",
player.getName().getString()
);
}
}
}

View File

@@ -0,0 +1,87 @@
package com.tiedup.remake.network.minigame;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.minigame.LockpickMiniGameState;
import com.tiedup.remake.minigame.LockpickSessionManager;
import java.util.UUID;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.network.NetworkEvent;
/**
* Phase 2: Packet for player position update during lockpick mini-game (Client to Server).
*
* Contains:
* - Session UUID
* - New position (0.0 to 1.0)
*/
public class PacketLockpickMiniGameMove {
private final UUID sessionId;
private final float newPosition;
public PacketLockpickMiniGameMove(UUID sessionId, float newPosition) {
this.sessionId = sessionId;
this.newPosition = newPosition;
}
public void encode(FriendlyByteBuf buf) {
buf.writeUUID(sessionId);
buf.writeFloat(newPosition);
}
public static PacketLockpickMiniGameMove decode(FriendlyByteBuf buf) {
UUID sessionId = buf.readUUID();
float newPosition = buf.readFloat();
return new PacketLockpickMiniGameMove(sessionId, newPosition);
}
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx
.get()
.enqueueWork(() -> {
ServerPlayer player = ctx.get().getSender();
if (player == null) {
return;
}
// Rate limiting: Prevent lockpick spam
if (
!com.tiedup.remake.network.PacketRateLimiter.allowPacket(
player,
"minigame"
)
) {
return;
}
handleServer(player);
});
ctx.get().setPacketHandled(true);
}
private void handleServer(ServerPlayer player) {
LockpickSessionManager manager = LockpickSessionManager.getInstance();
// Validate session
if (!manager.validateLockpickSession(player.getUUID(), sessionId)) {
TiedUpMod.LOGGER.warn(
"[PacketLockpickMiniGameMove] Invalid session {} for player {}",
sessionId.toString().substring(0, 8),
player.getName().getString()
);
return;
}
LockpickMiniGameState session = manager.getLockpickSession(
player.getUUID()
);
if (session == null || session.isComplete()) {
return;
}
// Update position (server-side validation: clamp to 0-1)
session.setCurrentPosition(Math.max(0.0f, Math.min(1.0f, newPosition)));
}
}

View File

@@ -0,0 +1,154 @@
package com.tiedup.remake.network.minigame;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.network.base.AbstractClientPacket;
import java.util.UUID;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
/**
* Phase 2: Packet to send lockpick result to client (Server to Client).
*
* Contains:
* - Session UUID
* - Result type (SUCCESS, MISSED, OUT_OF_PICKS)
* - Remaining uses
*/
public class PacketLockpickMiniGameResult extends AbstractClientPacket {
public enum ResultType {
/**
* Successfully picked the lock
*/
SUCCESS(0),
/**
* Missed the sweet spot
*/
MISSED(1),
/**
* Ran out of lockpick uses
*/
OUT_OF_PICKS(2),
/**
* Session cancelled
*/
CANCELLED(3);
private final int id;
ResultType(int id) {
this.id = id;
}
public int getId() {
return id;
}
public static ResultType fromId(int id) {
return switch (id) {
case 0 -> SUCCESS;
case 1 -> MISSED;
case 2 -> OUT_OF_PICKS;
case 3 -> CANCELLED;
default -> MISSED;
};
}
}
private final UUID sessionId;
private final ResultType resultType;
private final int remainingUses;
private final float distance; // Distance to sweet spot (0.0-1.0), used for MISSED animation
public PacketLockpickMiniGameResult(
UUID sessionId,
ResultType resultType,
int remainingUses
) {
this(sessionId, resultType, remainingUses, 0f);
}
public PacketLockpickMiniGameResult(
UUID sessionId,
ResultType resultType,
int remainingUses,
float distance
) {
this.sessionId = sessionId;
this.resultType = resultType;
this.remainingUses = remainingUses;
this.distance = distance;
}
public void encode(FriendlyByteBuf buf) {
buf.writeUUID(sessionId);
buf.writeVarInt(resultType.getId());
buf.writeVarInt(remainingUses);
buf.writeFloat(distance);
}
public static PacketLockpickMiniGameResult decode(FriendlyByteBuf buf) {
UUID sessionId = buf.readUUID();
ResultType resultType = ResultType.fromId(buf.readVarInt());
int remainingUses = buf.readVarInt();
float distance = buf.readFloat();
return new PacketLockpickMiniGameResult(
sessionId,
resultType,
remainingUses,
distance
);
}
@Override
@OnlyIn(Dist.CLIENT)
protected void handleClientImpl() {
ClientHandler.handle(this);
}
@OnlyIn(Dist.CLIENT)
private static class ClientHandler {
private static void handle(PacketLockpickMiniGameResult pkt) {
net.minecraft.client.Minecraft mc = net.minecraft.client.Minecraft.getInstance();
if (mc.player == null) {
return;
}
TiedUpMod.LOGGER.debug(
"[PacketLockpickMiniGameResult] Received result: type={}, uses={}",
pkt.resultType,
pkt.remainingUses
);
if (mc.screen instanceof com.tiedup.remake.client.gui.screens.LockpickMiniGameScreen screen) {
switch (pkt.resultType) {
case SUCCESS -> screen.onSuccess();
case MISSED -> screen.onMissed(pkt.remainingUses, pkt.distance);
case OUT_OF_PICKS -> screen.onOutOfPicks();
case CANCELLED -> screen.onCancelled();
}
}
}
}
// Getters
public UUID getSessionId() {
return sessionId;
}
public ResultType getResultType() {
return resultType;
}
public int getRemainingUses() {
return remainingUses;
}
public float getDistance() {
return distance;
}
}

View File

@@ -0,0 +1,157 @@
package com.tiedup.remake.network.minigame;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.ItemLockpick;
import com.tiedup.remake.items.base.ILockable;
import com.tiedup.remake.minigame.LockpickMiniGameState;
import com.tiedup.remake.minigame.LockpickSessionManager;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.PacketRateLimiter;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.network.NetworkEvent;
/**
* Phase 2: Packet to start a Lockpick mini-game session (Client to Server).
*
* Sent when player clicks "Lockpick" on a locked item.
*/
public class PacketLockpickMiniGameStart {
private final BodyRegionV2 targetRegion;
public PacketLockpickMiniGameStart(BodyRegionV2 targetRegion) {
this.targetRegion = targetRegion;
}
public void encode(FriendlyByteBuf buf) {
buf.writeEnum(targetRegion);
}
public static PacketLockpickMiniGameStart decode(FriendlyByteBuf buf) {
return new PacketLockpickMiniGameStart(buf.readEnum(BodyRegionV2.class));
}
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx
.get()
.enqueueWork(() -> {
ServerPlayer player = ctx.get().getSender();
if (player == null) {
return;
}
handleServer(player);
});
ctx.get().setPacketHandled(true);
}
private void handleServer(ServerPlayer player) {
if (!PacketRateLimiter.allowPacket(player, "minigame")) return;
PlayerBindState state = PlayerBindState.getInstance(player);
if (state == null) {
return;
}
// Check for mittens
if (state.hasMittens()) {
TiedUpMod.LOGGER.debug(
"[PacketLockpickMiniGameStart] Player {} has mittens, cannot lockpick",
player.getName().getString()
);
return;
}
// Find lockpick in inventory
ItemStack lockpickStack = ItemLockpick.findLockpickInInventory(player);
if (lockpickStack.isEmpty()) {
TiedUpMod.LOGGER.debug(
"[PacketLockpickMiniGameStart] Player {} has no lockpick",
player.getName().getString()
);
return;
}
// Get target item via V2 equipment system
ItemStack targetStack = V2EquipmentHelper.getInRegion(
player,
targetRegion
);
if (
targetStack.isEmpty() ||
!(targetStack.getItem() instanceof ILockable lockable)
) {
TiedUpMod.LOGGER.debug(
"[PacketLockpickMiniGameStart] Target region {} is not lockable",
targetRegion
);
return;
}
if (!lockable.isLocked(targetStack)) {
TiedUpMod.LOGGER.debug(
"[PacketLockpickMiniGameStart] Target region {} is not locked",
targetRegion
);
return;
}
if (lockable.isJammed(targetStack)) {
TiedUpMod.LOGGER.debug(
"[PacketLockpickMiniGameStart] Target region {} is jammed",
targetRegion
);
return;
}
// Determine sweet spot width based on lockpick type
float sweetSpotWidth = 0.03f; // 3% - Very difficult sweet spot (Skyrim-style)
// Get remaining uses
int remainingUses =
lockpickStack.getMaxDamage() - lockpickStack.getDamageValue();
// Start session
LockpickSessionManager manager = LockpickSessionManager.getInstance();
LockpickMiniGameState session = manager.startLockpickSession(
player,
targetRegion.ordinal(),
sweetSpotWidth
);
if (session == null) {
TiedUpMod.LOGGER.warn(
"[PacketLockpickMiniGameStart] Failed to create lockpick session for {}",
player.getName().getString()
);
return;
}
session.setRemainingUses(remainingUses);
TiedUpMod.LOGGER.info(
"[PacketLockpickMiniGameStart] Started lockpick session for {} (region: {}, uses: {})",
player.getName().getString(),
targetRegion,
remainingUses
);
// Send initial state to client
ModNetwork.sendToPlayer(
new PacketLockpickMiniGameState(
session.getSessionId(),
session.getSweetSpotCenter(),
session.getSweetSpotWidth(),
session.getCurrentPosition(),
session.getRemainingUses()
),
player
);
}
}

View File

@@ -0,0 +1,119 @@
package com.tiedup.remake.network.minigame;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.network.base.AbstractClientPacket;
import java.util.UUID;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
/**
* Phase 2: Packet to send lockpick initial state to client (Server to Client).
*
* Contains:
* - Session UUID
* - Sweet spot center and width (server-authoritative)
* - Current position
* - Remaining lockpick uses
*/
public class PacketLockpickMiniGameState extends AbstractClientPacket {
private final UUID sessionId;
private final float sweetSpotCenter;
private final float sweetSpotWidth;
private final float currentPosition;
private final int remainingUses;
public PacketLockpickMiniGameState(
UUID sessionId,
float sweetSpotCenter,
float sweetSpotWidth,
float currentPosition,
int remainingUses
) {
this.sessionId = sessionId;
this.sweetSpotCenter = sweetSpotCenter;
this.sweetSpotWidth = sweetSpotWidth;
this.currentPosition = currentPosition;
this.remainingUses = remainingUses;
}
public void encode(FriendlyByteBuf buf) {
buf.writeUUID(sessionId);
buf.writeFloat(sweetSpotCenter);
buf.writeFloat(sweetSpotWidth);
buf.writeFloat(currentPosition);
buf.writeVarInt(remainingUses);
}
public static PacketLockpickMiniGameState decode(FriendlyByteBuf buf) {
UUID sessionId = buf.readUUID();
float sweetSpotCenter = buf.readFloat();
float sweetSpotWidth = buf.readFloat();
float currentPosition = buf.readFloat();
int remainingUses = buf.readVarInt();
return new PacketLockpickMiniGameState(
sessionId,
sweetSpotCenter,
sweetSpotWidth,
currentPosition,
remainingUses
);
}
@Override
@OnlyIn(Dist.CLIENT)
protected void handleClientImpl() {
ClientHandler.handle(this);
}
@OnlyIn(Dist.CLIENT)
private static class ClientHandler {
private static void handle(PacketLockpickMiniGameState pkt) {
net.minecraft.client.Minecraft mc = net.minecraft.client.Minecraft.getInstance();
if (mc.player == null) {
return;
}
TiedUpMod.LOGGER.debug(
"[PacketLockpickMiniGameState] Received state: sweet={}(w={}), pos={}, uses={}",
pkt.sweetSpotCenter,
pkt.sweetSpotWidth,
pkt.currentPosition,
pkt.remainingUses
);
// Open the lockpick mini-game screen
mc.setScreen(
new com.tiedup.remake.client.gui.screens.LockpickMiniGameScreen(
pkt.sessionId,
pkt.sweetSpotCenter,
pkt.sweetSpotWidth,
pkt.currentPosition,
pkt.remainingUses
)
);
}
}
// Getters
public UUID getSessionId() {
return sessionId;
}
public float getSweetSpotCenter() {
return sweetSpotCenter;
}
public float getSweetSpotWidth() {
return sweetSpotWidth;
}
public float getCurrentPosition() {
return currentPosition;
}
public int getRemainingUses() {
return remainingUses;
}
}

View File

@@ -0,0 +1,13 @@
package com.tiedup.remake.network.personality;
/**
* Discriminator for the unified NPC command packet.
* Each type maps to what was previously a separate packet class.
*/
public enum NpcCommandType {
GIVE_COMMAND,
CANCEL_COMMAND,
SELECT_JOB,
CYCLE_FOLLOW_DISTANCE,
TOGGLE_AUTO_REST
}

View File

@@ -0,0 +1,140 @@
package com.tiedup.remake.network.personality;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.TiedUpMod;
// Discipline now only triggers dialogue, no personality effects
import com.tiedup.remake.dialogue.EntityDialogueManager;
import com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory;
import com.tiedup.remake.entities.EntityDamsel;
import com.tiedup.remake.network.PacketRateLimiter;
import com.tiedup.remake.personality.DisciplineType;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraftforge.network.NetworkEvent;
/**
* Packet sent from client to server to apply discipline to an NPC.
* Used for verbal discipline actions (Praise, Scold, Threaten) from the DialogueScreen.
*
* Training System V2: Verbal discipline support
*/
public class PacketDisciplineAction {
/** Maximum range for discipline actions */
private static final double MAX_RANGE = 32.0;
private final int entityId;
private final String disciplineTypeName;
public PacketDisciplineAction(int entityId, DisciplineType type) {
this.entityId = entityId;
this.disciplineTypeName = type.name();
}
private PacketDisciplineAction(int entityId, String disciplineTypeName) {
this.entityId = entityId;
this.disciplineTypeName = disciplineTypeName;
}
public void encode(FriendlyByteBuf buf) {
buf.writeInt(entityId);
buf.writeUtf(disciplineTypeName, 32);
}
public static PacketDisciplineAction decode(FriendlyByteBuf buf) {
return new PacketDisciplineAction(buf.readInt(), buf.readUtf(32));
}
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx
.get()
.enqueueWork(() -> {
ServerPlayer sender = ctx.get().getSender();
if (sender == null) return;
handleServer(sender);
});
ctx.get().setPacketHandled(true);
}
private void handleServer(ServerPlayer sender) {
// MEDIUM FIX: Rate limiting to prevent discipline spam exploit
if (!PacketRateLimiter.allowPacket(sender, "action")) {
return;
}
// Find the target entity
Entity entity = sender.level().getEntity(entityId);
if (!(entity instanceof EntityDamsel damsel)) {
TiedUpMod.LOGGER.warn(
"[PacketDisciplineAction] Entity {} not found or not a Damsel",
entityId
);
return;
}
// Validate distance
double distance = sender.distanceTo(damsel);
if (distance > MAX_RANGE) {
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.ERROR,
"Target is too far away!"
);
return;
}
// Parse discipline type
DisciplineType type;
try {
type = DisciplineType.valueOf(disciplineTypeName);
} catch (IllegalArgumentException e) {
TiedUpMod.LOGGER.warn(
"[PacketDisciplineAction] Unknown discipline type: {}",
disciplineTypeName
);
return;
}
// Only allow verbal discipline from this packet (Praise, Scold, Threaten)
if (!type.isVerbal()) {
TiedUpMod.LOGGER.warn(
"[PacketDisciplineAction] Non-verbal discipline {} attempted via packet",
type.name()
);
return;
}
// Show appropriate dialogue
DialogueCategory category = switch (type) {
case PRAISE -> DialogueCategory.PRAISE_RESPONSE;
case SCOLD -> DialogueCategory.SCOLD_RESPONSE;
case THREATEN -> DialogueCategory.THREATEN_RESPONSE;
default -> DialogueCategory.SCOLD_RESPONSE;
};
EntityDialogueManager.talkTo(damsel, sender, category);
// Send feedback
String npcName = damsel.getNpcName();
String actionDesc = switch (type) {
case PRAISE -> "praised";
case SCOLD -> "scolded";
case THREATEN -> "threatened";
default -> "disciplined";
};
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.INFO,
"You " + actionDesc + " " + npcName + "."
);
TiedUpMod.LOGGER.debug(
"[PacketDisciplineAction] {} {} {}",
sender.getName().getString(),
actionDesc,
npcName
);
}
}

View File

@@ -0,0 +1,312 @@
package com.tiedup.remake.network.personality;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.EntityDialogueManager;
import com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory;
import com.tiedup.remake.entities.EntityDamsel;
import com.tiedup.remake.items.ItemCommandWand;
import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.PacketRateLimiter;
import com.tiedup.remake.personality.JobExperience;
import com.tiedup.remake.personality.NpcCommand;
import com.tiedup.remake.personality.NpcNeeds;
import com.tiedup.remake.personality.PersonalityState;
import com.tiedup.remake.v2.BodyRegionV2;
import java.util.UUID;
import java.util.function.Supplier;
import org.jetbrains.annotations.Nullable;
import net.minecraft.core.BlockPos;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.network.NetworkEvent;
/**
* Unified C2S packet for all NPC command wand actions.
* Replaces PacketGiveCommand, PacketCancelCommand, PacketSelectJobCommand,
* PacketCycleFollowDistance, and PacketToggleAutoRest.
*/
public class PacketNpcCommand {
private static final double MAX_COMMAND_RANGE = 32.0;
private final UUID entityUUID;
private final NpcCommandType type;
// Optional fields depending on type
private final String commandName; // GIVE_COMMAND, SELECT_JOB
@Nullable
private final BlockPos targetPos; // GIVE_COMMAND only
private final boolean refreshScreen; // CYCLE_FOLLOW_DISTANCE, TOGGLE_AUTO_REST
// --- Constructors for each command type ---
public static PacketNpcCommand giveCommand(UUID entityUUID, NpcCommand command, @Nullable BlockPos targetPos) {
return new PacketNpcCommand(entityUUID, NpcCommandType.GIVE_COMMAND, command.name(), targetPos, false);
}
public static PacketNpcCommand cancelCommand(UUID entityUUID) {
return new PacketNpcCommand(entityUUID, NpcCommandType.CANCEL_COMMAND, "", null, false);
}
public static PacketNpcCommand selectJob(UUID entityUUID, NpcCommand job) {
return new PacketNpcCommand(entityUUID, NpcCommandType.SELECT_JOB, job.name(), null, false);
}
public static PacketNpcCommand cycleFollowDistance(UUID entityUUID, boolean refreshScreen) {
return new PacketNpcCommand(entityUUID, NpcCommandType.CYCLE_FOLLOW_DISTANCE, "", null, refreshScreen);
}
public static PacketNpcCommand toggleAutoRest(UUID entityUUID, boolean refreshScreen) {
return new PacketNpcCommand(entityUUID, NpcCommandType.TOGGLE_AUTO_REST, "", null, refreshScreen);
}
private PacketNpcCommand(UUID entityUUID, NpcCommandType type, String commandName,
@Nullable BlockPos targetPos, boolean refreshScreen) {
this.entityUUID = entityUUID;
this.type = type;
this.commandName = commandName;
this.targetPos = targetPos;
this.refreshScreen = refreshScreen;
}
// --- Wire format ---
public void encode(FriendlyByteBuf buf) {
buf.writeUUID(entityUUID);
buf.writeEnum(type);
switch (type) {
case GIVE_COMMAND -> {
buf.writeUtf(commandName);
buf.writeBoolean(targetPos != null);
if (targetPos != null) {
buf.writeBlockPos(targetPos);
}
}
case SELECT_JOB -> buf.writeUtf(commandName);
case CYCLE_FOLLOW_DISTANCE, TOGGLE_AUTO_REST -> buf.writeBoolean(refreshScreen);
case CANCEL_COMMAND -> {} // no extra data
}
}
public static PacketNpcCommand decode(FriendlyByteBuf buf) {
UUID uuid = buf.readUUID();
NpcCommandType type = buf.readEnum(NpcCommandType.class);
String cmd = "";
BlockPos pos = null;
boolean refresh = false;
switch (type) {
case GIVE_COMMAND -> {
cmd = buf.readUtf(32);
if (buf.readBoolean()) {
pos = buf.readBlockPos();
}
}
case SELECT_JOB -> cmd = buf.readUtf(32);
case CYCLE_FOLLOW_DISTANCE, TOGGLE_AUTO_REST -> refresh = buf.readBoolean();
case CANCEL_COMMAND -> {}
}
return new PacketNpcCommand(uuid, type, cmd, pos, refresh);
}
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx.get().enqueueWork(() -> {
ServerPlayer sender = ctx.get().getSender();
if (sender == null) return;
handleServer(sender);
});
ctx.get().setPacketHandled(true);
}
// --- Server handling ---
private void handleServer(ServerPlayer sender) {
if (!PacketRateLimiter.allowPacket(sender, "action")) return;
Entity entity = ((net.minecraft.server.level.ServerLevel) sender.level()).getEntity(entityUUID);
if (!(entity instanceof EntityDamsel damsel)) {
TiedUpMod.LOGGER.warn("[PacketNpcCommand:{}] Entity {} not found or not a Damsel",
type, entityUUID);
return;
}
double distance = sender.distanceTo(damsel);
if (distance > MAX_COMMAND_RANGE) {
SystemMessageManager.sendToPlayer(sender,
SystemMessageManager.MessageCategory.ERROR, "Target is too far away!");
return;
}
switch (type) {
case GIVE_COMMAND -> handleGiveCommand(sender, damsel);
case CANCEL_COMMAND -> handleCancelCommand(sender, damsel);
case SELECT_JOB -> handleSelectJob(sender, damsel);
case CYCLE_FOLLOW_DISTANCE -> handleCycleFollowDistance(sender, damsel);
case TOGGLE_AUTO_REST -> handleToggleAutoRest(sender, damsel);
}
}
private void handleGiveCommand(ServerPlayer sender, EntityDamsel damsel) {
NpcCommand command = NpcCommand.fromString(commandName);
if (command == NpcCommand.NONE && !"NONE".equals(commandName)) {
TiedUpMod.LOGGER.warn("[PacketNpcCommand:GIVE] Unknown command: {}", commandName);
return;
}
boolean success = damsel.giveCommand(sender, command, targetPos);
String npcName = damsel.getNpcName();
if (success) {
EntityDialogueManager.talkTo(damsel, sender, DialogueCategory.COMMAND_ACCEPT);
SystemMessageManager.sendToPlayer(sender,
SystemMessageManager.MessageCategory.INFO,
npcName + " accepted command: " + command.name());
TiedUpMod.LOGGER.debug("[PacketNpcCommand:GIVE] {} gave command {} to {}",
sender.getName().getString(), command.name(), npcName);
}
}
private void handleCancelCommand(ServerPlayer sender, EntityDamsel damsel) {
if (!validateCollarOwnership(sender, damsel)) return;
damsel.cancelCommand();
String npcName = damsel.getNpcName();
SystemMessageManager.sendToPlayer(sender,
SystemMessageManager.MessageCategory.INFO, npcName + "'s command cancelled.");
TiedUpMod.LOGGER.debug("[PacketNpcCommand:CANCEL] {} cancelled command for {}",
sender.getName().getString(), npcName);
}
private void handleSelectJob(ServerPlayer sender, EntityDamsel damsel) {
NpcCommand job = NpcCommand.fromString(commandName);
if (!job.isWorkCommand()) {
TiedUpMod.LOGGER.warn("[PacketNpcCommand:JOB] {} is not a work command", commandName);
return;
}
PersonalityState state = damsel.getPersonalityState();
if (state == null) return;
String npcName = damsel.getNpcName();
if (state.willObeyCommand(sender, job)) {
ItemStack mainHand = sender.getItemInHand(InteractionHand.MAIN_HAND);
ItemStack offHand = sender.getItemInHand(InteractionHand.OFF_HAND);
ItemStack wand = null;
if (mainHand.getItem() == ModItems.COMMAND_WAND.get()) {
wand = mainHand;
} else if (offHand.getItem() == ModItems.COMMAND_WAND.get()) {
wand = offHand;
}
if (wand != null) {
ItemCommandWand.enterSelectionMode(wand, damsel.getUUID(), job);
EntityDialogueManager.talkTo(damsel, sender, DialogueCategory.COMMAND_ACCEPT);
SystemMessageManager.sendToPlayer(sender,
SystemMessageManager.MessageCategory.INFO,
npcName + " is ready to " + job.name() + ". Click a chest to set work zone!");
TiedUpMod.LOGGER.debug("[PacketNpcCommand:JOB] {} accepted job {} - wand in selection mode",
npcName, job.name());
}
}
}
private void handleCycleFollowDistance(ServerPlayer sender, EntityDamsel damsel) {
if (!validateCollarOwnership(sender, damsel)) return;
PersonalityState state = damsel.getPersonalityState();
if (state == null) return;
NpcCommand.FollowDistance current = state.getFollowDistance();
NpcCommand.FollowDistance next = switch (current) {
case FAR -> NpcCommand.FollowDistance.CLOSE;
case CLOSE -> NpcCommand.FollowDistance.HEEL;
case HEEL -> NpcCommand.FollowDistance.FAR;
};
state.setFollowDistance(next);
TiedUpMod.LOGGER.debug("[PacketNpcCommand:FOLLOW] {} changed {} follow distance to {}",
sender.getName().getString(), damsel.getNpcName(), next.name());
if (refreshScreen) {
sendRefreshedScreen(sender, damsel, state);
}
}
private void handleToggleAutoRest(ServerPlayer sender, EntityDamsel damsel) {
if (!validateCollarOwnership(sender, damsel)) return;
PersonalityState state = damsel.getPersonalityState();
if (state == null) return;
boolean newState = state.toggleAutoRest();
TiedUpMod.LOGGER.debug("[PacketNpcCommand:REST] {} toggled {} auto-rest to {}",
sender.getName().getString(), damsel.getNpcName(), newState ? "ON" : "OFF");
SystemMessageManager.sendToPlayer(sender,
SystemMessageManager.MessageCategory.INFO,
"Auto-Rest: " + (newState ? "ON" : "OFF"));
if (refreshScreen) {
sendRefreshedScreen(sender, damsel, state);
}
}
// --- Shared helpers ---
private boolean validateCollarOwnership(ServerPlayer sender, EntityDamsel damsel) {
if (!damsel.hasCollar()) {
SystemMessageManager.sendToPlayer(sender,
SystemMessageManager.MessageCategory.ERROR,
damsel.getNpcName() + " is not wearing a collar!");
return false;
}
ItemStack collar = damsel.getEquipment(BodyRegionV2.NECK);
if (!(collar.getItem() instanceof ItemCollar collarItem)) {
return false;
}
if (!collarItem.getOwners(collar).contains(sender.getUUID())) {
SystemMessageManager.sendToPlayer(sender,
SystemMessageManager.MessageCategory.ERROR,
"You don't own " + damsel.getNpcName() + "'s collar!");
return false;
}
return true;
}
private static void sendRefreshedScreen(ServerPlayer sender, EntityDamsel damsel,
PersonalityState state) {
NpcNeeds needs = state.getNeeds();
String homeType = state.getHomeType().name();
JobExperience jobExp = state.getJobExperience();
NpcCommand activeCmd = state.getActiveCommand();
String activeJobLevelName = "";
int activeJobXp = 0;
int activeJobXpMax = 10;
if (activeCmd.isActiveJob()) {
JobExperience.JobLevel level = jobExp.getJobLevel(activeCmd);
activeJobLevelName = level.name();
activeJobXp = jobExp.getExperience(activeCmd);
activeJobXpMax = level.maxExp;
}
ModNetwork.sendToPlayer(
new PacketOpenCommandWandScreen(
damsel.getUUID(), damsel.getNpcName(),
state.getPersonality().name(), activeCmd.name(),
needs.getHunger(), needs.getRest(), state.getMood(),
state.getFollowDistance().name(), homeType,
state.isAutoRestEnabled(), "", "",
activeJobLevelName, activeJobXp, activeJobXpMax),
sender);
}
}

View File

@@ -0,0 +1,284 @@
package com.tiedup.remake.network.personality;
import com.tiedup.remake.core.TiedUpMod;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.fml.loading.FMLEnvironment;
import net.minecraftforge.network.NetworkEvent;
/**
* Packet sent from server to client to open the Command Wand screen.
* Contains all personality data needed for the GUI.
*
* Simplified: removed discovery, fear, relationship, secondaryTrait fields.
*/
public class PacketOpenCommandWandScreen {
private final java.util.UUID entityUUID;
private final String npcName;
private final String personalityTypeName;
private final String activeCommandName;
private final float hunger;
private final float rest;
private final float mood;
private final String followDistanceMode;
private final String homeType;
private final boolean autoRestEnabled;
private final String cellName;
private final String cellQualityName;
private final String activeJobLevelName;
private final int activeJobXp;
private final int activeJobXpMax;
/**
* Constructor without cell info or job experience (defaults to "").
*/
public PacketOpenCommandWandScreen(
java.util.UUID entityUUID,
String npcName,
String personalityTypeName,
String activeCommandName,
float hunger,
float rest,
float mood,
String followDistanceMode,
String homeType,
boolean autoRestEnabled
) {
this(
entityUUID,
npcName,
personalityTypeName,
activeCommandName,
hunger,
rest,
mood,
followDistanceMode,
homeType,
autoRestEnabled,
"",
"",
"",
0,
10
);
}
/**
* Constructor with cell info but no job experience.
*/
public PacketOpenCommandWandScreen(
java.util.UUID entityUUID,
String npcName,
String personalityTypeName,
String activeCommandName,
float hunger,
float rest,
float mood,
String followDistanceMode,
String homeType,
boolean autoRestEnabled,
String cellName,
String cellQualityName
) {
this(
entityUUID,
npcName,
personalityTypeName,
activeCommandName,
hunger,
rest,
mood,
followDistanceMode,
homeType,
autoRestEnabled,
cellName,
cellQualityName,
"",
0,
10
);
}
/**
* Full constructor with cell info and job experience.
*/
public PacketOpenCommandWandScreen(
java.util.UUID entityUUID,
String npcName,
String personalityTypeName,
String activeCommandName,
float hunger,
float rest,
float mood,
String followDistanceMode,
String homeType,
boolean autoRestEnabled,
String cellName,
String cellQualityName,
String activeJobLevelName,
int activeJobXp,
int activeJobXpMax
) {
this.entityUUID = entityUUID;
this.npcName = npcName;
this.personalityTypeName = personalityTypeName;
this.activeCommandName = activeCommandName;
this.hunger = hunger;
this.rest = rest;
this.mood = mood;
this.followDistanceMode = followDistanceMode;
this.homeType = homeType;
this.autoRestEnabled = autoRestEnabled;
this.cellName = cellName;
this.cellQualityName = cellQualityName;
this.activeJobLevelName = activeJobLevelName;
this.activeJobXp = activeJobXp;
this.activeJobXpMax = activeJobXpMax;
}
public void encode(FriendlyByteBuf buf) {
buf.writeUUID(entityUUID);
buf.writeUtf(npcName, 64);
buf.writeUtf(personalityTypeName, 32);
buf.writeUtf(activeCommandName, 32);
buf.writeFloat(hunger);
buf.writeFloat(rest);
buf.writeFloat(mood);
buf.writeUtf(followDistanceMode, 16);
buf.writeUtf(homeType, 16);
buf.writeBoolean(autoRestEnabled);
buf.writeUtf(cellName, 64);
buf.writeUtf(cellQualityName, 16);
buf.writeUtf(activeJobLevelName, 32);
buf.writeInt(activeJobXp);
buf.writeInt(activeJobXpMax);
}
public static PacketOpenCommandWandScreen decode(FriendlyByteBuf buf) {
return new PacketOpenCommandWandScreen(
buf.readUUID(),
buf.readUtf(64), // npcName
buf.readUtf(32), // personalityTypeName
buf.readUtf(32), // activeCommandName
buf.readFloat(), // hunger
buf.readFloat(), // rest
buf.readFloat(), // mood
buf.readUtf(16), // followDistanceMode
buf.readUtf(16), // homeType
buf.readBoolean(), // autoRestEnabled
buf.readUtf(64), // cellName
buf.readUtf(16), // cellQualityName
buf.readUtf(32), // activeJobLevelName
buf.readInt(), // activeJobXp
buf.readInt() // activeJobXpMax
);
}
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx
.get()
.enqueueWork(() -> {
if (FMLEnvironment.dist == Dist.CLIENT) {
handleClient();
}
});
ctx.get().setPacketHandled(true);
}
@OnlyIn(Dist.CLIENT)
private void handleClient() {
net.minecraft.client.Minecraft mc =
net.minecraft.client.Minecraft.getInstance();
TiedUpMod.LOGGER.debug(
"[PacketOpenCommandWandScreen] Opening screen for {} (entity {})",
npcName,
entityUUID
);
mc.setScreen(
new com.tiedup.remake.client.gui.screens.CommandWandScreen(
entityUUID,
npcName,
personalityTypeName,
activeCommandName,
hunger,
rest,
mood,
followDistanceMode,
homeType,
autoRestEnabled,
cellName,
cellQualityName,
activeJobLevelName,
activeJobXp,
activeJobXpMax
)
);
}
// --- Getters for GUI use ---
public java.util.UUID getEntityUUID() {
return entityUUID;
}
public String getNpcName() {
return npcName;
}
public String getPersonalityTypeName() {
return personalityTypeName;
}
public String getActiveCommandName() {
return activeCommandName;
}
public float getHunger() {
return hunger;
}
public float getRest() {
return rest;
}
public float getMood() {
return mood;
}
public String getFollowDistanceMode() {
return followDistanceMode;
}
public String getHomeType() {
return homeType;
}
public boolean isAutoRestEnabled() {
return autoRestEnabled;
}
public String getCellName() {
return cellName;
}
public String getCellQualityName() {
return cellQualityName;
}
public String getActiveJobLevelName() {
return activeJobLevelName;
}
public int getActiveJobXp() {
return activeJobXp;
}
public int getActiveJobXpMax() {
return activeJobXpMax;
}
}

View File

@@ -0,0 +1,106 @@
package com.tiedup.remake.network.personality;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.entities.EntityDamsel;
import com.tiedup.remake.network.PacketRateLimiter;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.network.NetworkEvent;
/**
* Packet sent from client to server to request opening the NPC inventory screen.
* Server responds with PacketOpenNpcInventory if validation passes.
*
* Personality System Phase J: NPC Inventory
*/
public class PacketRequestNpcInventory {
private final java.util.UUID entityUUID; // HIGH FIX: use UUID for persistence
/**
* Create a request packet.
*
* @param entityUUID The NPC entity UUID
*/
public PacketRequestNpcInventory(java.util.UUID entityUUID) {
this.entityUUID = entityUUID;
}
public void encode(FriendlyByteBuf buf) {
buf.writeUUID(entityUUID); // HIGH FIX
}
public static PacketRequestNpcInventory decode(FriendlyByteBuf buf) {
return new PacketRequestNpcInventory(buf.readUUID());
}
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx
.get()
.enqueueWork(() -> {
ServerPlayer player = ctx.get().getSender();
if (player == null) return;
if (!PacketRateLimiter.allowPacket(player, "ui")) return;
// HIGH FIX: lookup by UUID
Entity entity = (
(net.minecraft.server.level.ServerLevel) player.level()
).getEntity(entityUUID);
if (!(entity instanceof EntityDamsel damsel)) {
TiedUpMod.LOGGER.warn(
"[PacketRequestNpcInventory] Entity {} is not a damsel",
entityUUID
);
return;
}
// Verify player is nearby
if (player.distanceTo(damsel) > 6.0) {
TiedUpMod.LOGGER.warn(
"[PacketRequestNpcInventory] Player {} too far from NPC",
player.getName().getString()
);
return;
}
// Verify player is owner of collar (or NPC has no collar)
if (damsel.hasCollar()) {
ItemStack collar = damsel.getEquipment(BodyRegionV2.NECK);
if (
collar.getItem() instanceof
com.tiedup.remake.items.base.ItemCollar collarItem
) {
if (!collarItem.isOwner(collar, player)) {
TiedUpMod.LOGGER.warn(
"[PacketRequestNpcInventory] Player {} is not owner of NPC collar",
player.getName().getString()
);
return;
}
}
}
// Open vanilla container via NetworkHooks
TiedUpMod.LOGGER.debug(
"[PacketRequestNpcInventory] Opening inventory for {} to {}",
damsel.getNpcName(),
player.getName().getString()
);
net.minecraftforge.network.NetworkHooks.openScreen(
player,
damsel,
buf -> {
buf.writeInt(damsel.getId());
buf.writeInt(damsel.getNpcInventorySize());
buf.writeUtf(damsel.getNpcName(), 64);
}
);
});
ctx.get().setPacketHandled(true);
}
}

View File

@@ -0,0 +1,143 @@
package com.tiedup.remake.network.personality;
import java.util.function.Supplier;
import net.minecraft.ChatFormatting;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.chat.Component;
import net.minecraft.sounds.SoundEvents;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.fml.loading.FMLEnvironment;
import net.minecraftforge.network.NetworkEvent;
/**
* Packet sent from server to client to alert an owner that someone
* is trying to free their slave.
*
* Phase 11: Multiplayer protection system
*/
public class PacketSlaveBeingFreed {
private final String slaveName;
private final String liberatorName;
private final int x;
private final int y;
private final int z;
/**
* Create alert packet.
*
* @param slaveName Name of the slave being freed
* @param liberatorName Name of the player trying to free them
* @param x X coordinate
* @param y Y coordinate
* @param z Z coordinate
*/
public PacketSlaveBeingFreed(
String slaveName,
String liberatorName,
int x,
int y,
int z
) {
this.slaveName = slaveName;
this.liberatorName = liberatorName;
this.x = x;
this.y = y;
this.z = z;
}
public void encode(FriendlyByteBuf buf) {
buf.writeUtf(slaveName, 64);
buf.writeUtf(liberatorName, 64);
buf.writeInt(x);
buf.writeInt(y);
buf.writeInt(z);
}
public static PacketSlaveBeingFreed decode(FriendlyByteBuf buf) {
return new PacketSlaveBeingFreed(
buf.readUtf(64),
buf.readUtf(64),
buf.readInt(),
buf.readInt(),
buf.readInt()
);
}
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx
.get()
.enqueueWork(() -> {
if (FMLEnvironment.dist == Dist.CLIENT) {
handleClient();
}
});
ctx.get().setPacketHandled(true);
}
@OnlyIn(Dist.CLIENT)
private void handleClient() {
net.minecraft.client.Minecraft mc =
net.minecraft.client.Minecraft.getInstance();
if (mc.player == null) return;
// Build alert message
Component message = Component.literal("")
.append(
Component.literal("[WARNING] ").withStyle(
ChatFormatting.RED,
ChatFormatting.BOLD
)
)
.append(
Component.literal(liberatorName).withStyle(
ChatFormatting.YELLOW
)
)
.append(
Component.literal(" is trying to free ").withStyle(
ChatFormatting.RED
)
)
.append(
Component.literal(slaveName).withStyle(ChatFormatting.YELLOW)
)
.append(Component.literal(" at ").withStyle(ChatFormatting.RED))
.append(
Component.literal(
"[" + x + ", " + y + ", " + z + "]"
).withStyle(ChatFormatting.AQUA)
)
.append(Component.literal("!").withStyle(ChatFormatting.RED));
// Send to player chat
mc.player.displayClientMessage(message, false);
// Play warning sound
mc.player.playSound(SoundEvents.BELL_BLOCK, 1.0f, 1.0f);
}
// --- Getters ---
public String getSlaveName() {
return slaveName;
}
public String getLiberatorName() {
return liberatorName;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getZ() {
return z;
}
}

View File

@@ -0,0 +1,357 @@
package com.tiedup.remake.network.selfbondage;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.*;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.PacketRateLimiter;
import com.tiedup.remake.network.action.PacketTying;
import com.tiedup.remake.network.sync.SyncManager;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.tasks.TyingPlayerTask;
import com.tiedup.remake.tasks.TyingTask;
import com.tiedup.remake.tasks.V2TyingPlayerTask;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.core.SettingsAccessor;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.IV2BondageItem;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.network.NetworkEvent;
/**
* Packet sent from client to server when player attempts self-bondage.
* Triggered by left-click with bondage item in hand.
*
* Self-bondage rules:
* - Binds: Requires tying task (same duration as normal tying)
* - Accessories (gag, blindfold, mittens, earplugs): Instant, can be equipped anytime
* - Collar: NOT allowed (cannot self-collar)
*/
public class PacketSelfBondage {
private final InteractionHand hand;
public PacketSelfBondage(InteractionHand hand) {
this.hand = hand;
}
public static void encode(PacketSelfBondage msg, FriendlyByteBuf buf) {
buf.writeEnum(msg.hand);
}
public static PacketSelfBondage decode(FriendlyByteBuf buf) {
return new PacketSelfBondage(buf.readEnum(InteractionHand.class));
}
public static void handle(
PacketSelfBondage msg,
Supplier<NetworkEvent.Context> ctx
) {
ctx
.get()
.enqueueWork(() -> {
ServerPlayer player = ctx.get().getSender();
if (player == null) return;
// Rate limiting (dedicated bucket: 15 tokens, 6/sec refill for continuous 5 pkt/sec)
if (!PacketRateLimiter.allowPacket(player, "selfbondage")) {
return;
}
ItemStack stack = player.getItemInHand(msg.hand);
if (stack.isEmpty()) return;
Item item = stack.getItem();
// Get player's kidnapped state
IBondageState state = KidnappedHelper.getKidnappedState(player);
if (state == null) return;
// Route to appropriate handler based on item type
// V2 bondage items — use tying task with V2 equip
if (item instanceof IV2BondageItem v2Item) {
handleV2SelfBondage(player, stack, v2Item, state);
return;
}
// V1 routes below (legacy)
if (item instanceof ItemBind bind) {
handleSelfBind(player, stack, bind, state);
} else if (item instanceof ItemGag) {
handleSelfAccessory(
player,
stack,
state,
"gag",
s -> s.isGagged(),
s -> s.getEquipment(BodyRegionV2.MOUTH),
s -> s.unequip(BodyRegionV2.MOUTH),
(s, i) -> s.equip(BodyRegionV2.MOUTH, i)
);
} else if (item instanceof ItemBlindfold) {
handleSelfAccessory(
player,
stack,
state,
"blindfold",
s -> s.isBlindfolded(),
s -> s.getEquipment(BodyRegionV2.EYES),
s -> s.unequip(BodyRegionV2.EYES),
(s, i) -> s.equip(BodyRegionV2.EYES, i)
);
} else if (item instanceof ItemMittens) {
handleSelfAccessory(
player,
stack,
state,
"mittens",
s -> s.hasMittens(),
s -> s.getEquipment(BodyRegionV2.HANDS),
s -> s.unequip(BodyRegionV2.HANDS),
(s, i) -> s.equip(BodyRegionV2.HANDS, i)
);
} else if (item instanceof ItemEarplugs) {
handleSelfAccessory(
player,
stack,
state,
"earplugs",
s -> s.hasEarplugs(),
s -> s.getEquipment(BodyRegionV2.EARS),
s -> s.unequip(BodyRegionV2.EARS),
(s, i) -> s.equip(BodyRegionV2.EARS, i)
);
}
// ItemCollar: NOT handled - cannot self-collar
});
ctx.get().setPacketHandled(true);
}
/**
* Handle self-binding with a bind item (rope, chain, etc.).
* Uses tying task system - requires holding left-click.
*/
private static void handleSelfBind(
ServerPlayer player,
ItemStack stack,
ItemBind bind,
IBondageState state
) {
// Can't self-tie if already tied
if (state.isTiedUp()) {
TiedUpMod.LOGGER.debug(
"[SelfBondage] {} tried to self-tie but is already tied",
player.getName().getString()
);
return;
}
// Get player's bind state for tying task management
PlayerBindState playerState = PlayerBindState.getInstance(player);
if (playerState == null) return;
// Get tying duration from GameRule
int tyingSeconds = SettingsAccessor.getTyingPlayerTime(
player.level().getGameRules()
);
// Create self-tying task (target == kidnapper)
TyingPlayerTask newTask = new TyingPlayerTask(
stack.copy(),
state,
player, // Target is self
tyingSeconds,
player.level(),
player // Kidnapper is also self
);
// Get current tying task
TyingTask currentTask = playerState.getCurrentTyingTask();
// Check if we should start a new task or continue existing one
if (
currentTask == null ||
!currentTask.isSameTarget(player) ||
currentTask.isOutdated() ||
!ItemStack.matches(currentTask.getBind(), stack)
) {
// Start new self-tying task
playerState.setCurrentTyingTask(newTask);
newTask.start();
TiedUpMod.LOGGER.debug(
"[SelfBondage] {} started self-tying ({} seconds)",
player.getName().getString(),
tyingSeconds
);
} else {
// Continue existing task
newTask = (TyingPlayerTask) currentTask;
}
// Update task progress
newTask.update();
// Check if task completed
if (newTask.isStopped()) {
// Self-tying complete! Consume the item
stack.shrink(1);
playerState.setCurrentTyingTask(null);
TiedUpMod.LOGGER.info(
"[SelfBondage] {} successfully self-tied",
player.getName().getString()
);
}
}
/**
* Handle self-bondage with a V2 bondage item.
* Uses V2TyingPlayerTask for progress, V2EquipmentHelper for equip.
*/
private static void handleV2SelfBondage(
ServerPlayer player,
ItemStack stack,
IV2BondageItem v2Item,
IBondageState state
) {
// Check if all target regions are already occupied or blocked
boolean allBlocked = true;
for (BodyRegionV2 region : v2Item.getOccupiedRegions(stack)) {
if (!V2EquipmentHelper.isRegionOccupied(player, region)
&& !V2EquipmentHelper.isRegionBlocked(player, region)) {
allBlocked = false;
break;
}
}
if (allBlocked) {
TiedUpMod.LOGGER.debug(
"[SelfBondage] {} tried V2 self-equip but all regions occupied",
player.getName().getString()
);
return;
}
PlayerBindState playerState = PlayerBindState.getInstance(player);
if (playerState == null) return;
int tyingSeconds = SettingsAccessor.getTyingPlayerTime(
player.level().getGameRules()
);
// Create V2 tying task (uses V2EquipmentHelper on completion, NOT putBindOn)
V2TyingPlayerTask newTask = new V2TyingPlayerTask(
stack.copy(), // copy for display/matching
stack, // live reference for consumption
state,
player, // target is self
tyingSeconds,
player.level(),
player // kidnapper is also self
);
TyingTask currentTask = playerState.getCurrentTyingTask();
if (currentTask == null
|| !currentTask.isSameTarget(player)
|| currentTask.isOutdated()
|| !ItemStack.matches(currentTask.getBind(), stack)) {
// Start new task
playerState.setCurrentTyingTask(newTask);
newTask.start();
TiedUpMod.LOGGER.debug(
"[SelfBondage] {} started V2 self-tying ({} seconds)",
player.getName().getString(),
tyingSeconds
);
} else {
// Continue existing task — just mark active
currentTask.update();
}
// If we started a new task, mark it active too
if (playerState.getCurrentTyingTask() == newTask) {
newTask.update();
}
}
/**
* Handle self-equipping an accessory (gag, blindfold, mittens, earplugs).
* Can be used anytime (no need to be tied).
* Blocked only if arms are fully bound.
* Supports swapping: if same type already equipped (and not locked), swap them.
*/
private static void handleSelfAccessory(
ServerPlayer player,
ItemStack stack,
IBondageState state,
String itemType,
java.util.function.Predicate<IBondageState> isEquipped,
java.util.function.Function<IBondageState, ItemStack> getCurrent,
java.util.function.Function<IBondageState, ItemStack> takeOff,
java.util.function.BiConsumer<IBondageState, ItemStack> putOn
) {
// Can't equip if arms are fully bound (need hands to put on accessories)
ItemStack currentBind = state.getEquipment(BodyRegionV2.ARMS);
if (!currentBind.isEmpty()) {
if (ItemBind.hasArmsBound(currentBind)) {
TiedUpMod.LOGGER.debug(
"[SelfBondage] {} can't self-{} - arms are bound",
player.getName().getString(),
itemType
);
return;
}
}
// Already equipped? Try to swap
if (isEquipped.test(state)) {
ItemStack currentItem = getCurrent.apply(state);
// Check if current item is locked
if (
currentItem.getItem() instanceof ILockable lockable &&
lockable.isLocked(currentItem)
) {
TiedUpMod.LOGGER.debug(
"[SelfBondage] {} can't swap {} - current is locked",
player.getName().getString(),
itemType
);
return;
}
// Remove current and drop it
ItemStack removed = takeOff.apply(state);
if (!removed.isEmpty()) {
state.kidnappedDropItem(removed);
TiedUpMod.LOGGER.debug(
"[SelfBondage] {} swapping {} - dropped old one",
player.getName().getString(),
itemType
);
}
}
// Equip new item on self
putOn.accept(state, stack.copy());
stack.shrink(1);
// Sync to client
SyncManager.syncInventory(player);
TiedUpMod.LOGGER.info(
"[SelfBondage] {} self-equipped {}",
player.getName().getString(),
itemType
);
}
}

View File

@@ -0,0 +1,97 @@
package com.tiedup.remake.network.slave;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.network.PacketRateLimiter;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.IV2BondageItem;
import com.tiedup.remake.v2.bondage.V2EquipResult;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import java.util.UUID;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.network.NetworkEvent;
/**
* Client→Server: Master equips an item from their inventory onto a slave's body region.
*/
public class PacketMasterEquip {
private final UUID targetEntityUUID;
private final BodyRegionV2 region;
private final int inventorySlot;
public PacketMasterEquip(UUID targetEntityUUID, BodyRegionV2 region, int inventorySlot) {
this.targetEntityUUID = targetEntityUUID;
this.region = region;
this.inventorySlot = inventorySlot;
}
public static void encode(PacketMasterEquip msg, FriendlyByteBuf buf) {
buf.writeUUID(msg.targetEntityUUID);
buf.writeEnum(msg.region);
buf.writeVarInt(msg.inventorySlot);
}
public static PacketMasterEquip decode(FriendlyByteBuf buf) {
return new PacketMasterEquip(buf.readUUID(), buf.readEnum(BodyRegionV2.class), buf.readVarInt());
}
public static void handle(PacketMasterEquip msg, Supplier<NetworkEvent.Context> ctxSupplier) {
NetworkEvent.Context ctx = ctxSupplier.get();
ctx.enqueueWork(() -> {
ServerPlayer sender = ctx.getSender();
if (sender == null) return;
if (!PacketRateLimiter.allowPacket(sender, "action")) return;
// Find target entity (reuse package-private method from PacketSlaveItemManage)
LivingEntity target = PacketSlaveItemManage.findTargetEntity(sender, msg.targetEntityUUID);
if (target == null) return;
// Distance check (5 blocks)
if (sender.distanceTo(target) > 5.0f) return;
// Collar ownership check
IBondageState targetState = KidnappedHelper.getKidnappedState(target);
if (targetState == null || !targetState.hasCollar()) return;
ItemStack collarStack = targetState.getEquipment(BodyRegionV2.NECK);
if (collarStack.getItem() instanceof ItemCollar collar) {
if (!collar.isOwner(collarStack, sender) && !sender.hasPermissions(2)) return;
}
// Validate sender's inventory slot
if (msg.inventorySlot < 0 || msg.inventorySlot >= sender.getInventory().getContainerSize()) return;
ItemStack stack = sender.getInventory().getItem(msg.inventorySlot);
if (stack.isEmpty()) return;
if (!(stack.getItem() instanceof IV2BondageItem bondageItem)) return;
if (!bondageItem.getOccupiedRegions(stack).contains(msg.region)) return;
// Furniture seat blocks this region
if (target.isPassenger() && target.getVehicle() instanceof com.tiedup.remake.v2.furniture.ISeatProvider provider) {
com.tiedup.remake.v2.furniture.SeatDefinition seat = provider.getSeatForPassenger(target);
if (seat != null && seat.blockedRegions().contains(msg.region)) {
return; // Region blocked by furniture
}
}
// Equip on target
V2EquipResult result = V2EquipmentHelper.equipItem(target, stack);
if (result.isSuccess()) {
sender.getInventory().removeItem(msg.inventorySlot, 1);
// Return any displaced items to master's inventory
if (result.displaced() != null) {
for (ItemStack displaced : result.displaced()) {
if (!displaced.isEmpty()) {
sender.getInventory().placeItemBackInInventory(displaced);
}
}
}
}
});
ctx.setPacketHandled(true);
}
}

View File

@@ -0,0 +1,394 @@
package com.tiedup.remake.network.slave;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.ItemGpsCollar;
import com.tiedup.remake.items.ItemShockCollar;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.state.IRestrainable;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.state.PlayerCaptorManager;
import com.tiedup.remake.util.KidnappedHelper;
import java.util.UUID;
import java.util.function.Supplier;
import net.minecraft.ChatFormatting;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.phys.AABB;
import net.minecraftforge.network.NetworkEvent;
/**
* Packet for slave management actions from SlaveManagementScreen.
* Handles: SHOCK, LOCATE, FREE actions on owned slaves.
*
* Phase 16: GUI Revamp - Slave management packets
*
* Security: Distance and dimension validation added to prevent griefing
*/
public class PacketSlaveAction {
/** Maximum interaction range for SHOCK and FREE actions (blocks) */
private static final double MAX_INTERACTION_RANGE = 100.0;
public enum Action {
SHOCK,
LOCATE,
FREE,
}
private final UUID targetId;
private final Action action;
public PacketSlaveAction(UUID targetId, Action action) {
this.targetId = targetId;
this.action = action;
}
public void encode(FriendlyByteBuf buf) {
buf.writeUUID(targetId);
buf.writeEnum(action);
}
public static PacketSlaveAction decode(FriendlyByteBuf buf) {
UUID id = buf.readUUID();
Action action = buf.readEnum(Action.class);
return new PacketSlaveAction(id, action);
}
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx
.get()
.enqueueWork(() -> {
ServerPlayer sender = ctx.get().getSender();
if (sender == null) return;
// Rate limiting: Prevent action spam
if (
!com.tiedup.remake.network.PacketRateLimiter.allowPacket(
sender,
"action"
)
) {
return;
}
handleServer(sender);
});
ctx.get().setPacketHandled(true);
}
private void handleServer(ServerPlayer sender) {
// Get sender's kidnapper manager
PlayerBindState senderState = PlayerBindState.getInstance(sender);
if (senderState == null) {
TiedUpMod.LOGGER.warn(
"[PACKET] PacketSlaveAction: No PlayerBindState for sender {}",
sender.getName().getString()
);
return;
}
// Phase 17: PlayerKidnapperManager → PlayerCaptorManager
PlayerCaptorManager manager = senderState.getCaptorManager();
// Find the target - check both formal captives AND collar-owned entities
IRestrainable targetCaptive = null;
boolean isFormalCaptive = false;
// 1. Check formal captives first
if (manager != null && manager.hasCaptives()) {
for (IBondageState captive : manager.getCaptives()) {
LivingEntity entity = captive.asLivingEntity();
if (entity != null && entity.getUUID().equals(targetId)
&& captive instanceof IRestrainable r) {
targetCaptive = r;
isFormalCaptive = true;
break;
}
}
}
// 2. If not found, search for nearby entities with collar owned by sender
if (targetCaptive == null) {
AABB searchBox = sender.getBoundingBox().inflate(32); // Security: reduced from 100
for (LivingEntity entity : sender
.level()
.getEntitiesOfClass(LivingEntity.class, searchBox)) {
if (!entity.getUUID().equals(targetId)) continue;
IRestrainable kidnapped = KidnappedHelper.getKidnappedState(
entity
);
if (kidnapped != null && kidnapped.hasCollar()) {
ItemStack collarStack = kidnapped.getEquipment(BodyRegionV2.NECK);
if (collarStack.getItem() instanceof ItemCollar collar) {
if (collar.isOwner(collarStack, sender)) {
targetCaptive = kidnapped;
break;
}
}
}
}
}
if (targetCaptive == null) {
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.ERROR,
"Target not found or you don't own their collar!"
);
return;
}
LivingEntity targetEntity = targetCaptive.asLivingEntity();
String targetName = targetCaptive.getKidnappedName();
// Security: Validate dimension and distance (except for LOCATE which is GPS-based)
if (action != Action.LOCATE) {
// Check same dimension
if (sender.level() != targetEntity.level()) {
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.ERROR,
targetName + " is in a different dimension!"
);
return;
}
// Check distance
double distance = sender.distanceTo(targetEntity);
if (distance > MAX_INTERACTION_RANGE) {
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.ERROR,
targetName +
" is too far away! (Distance: " +
(int) distance +
" blocks, Max: " +
(int) MAX_INTERACTION_RANGE +
")"
);
return;
}
}
switch (action) {
case SHOCK -> handleShock(
sender,
targetCaptive,
targetEntity,
targetName
);
case LOCATE -> handleLocate(
sender,
targetCaptive,
targetEntity,
targetName
);
case FREE -> handleFree(
sender,
targetCaptive,
manager,
targetName,
isFormalCaptive
);
}
}
private void handleShock(
ServerPlayer sender,
IRestrainable target,
LivingEntity targetEntity,
String name
) {
// Check if target has shock collar
if (!target.hasCollar()) {
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.ERROR,
name + " is not wearing a collar!"
);
return;
}
ItemStack collarStack = target.getEquipment(BodyRegionV2.NECK);
if (
!(collarStack.getItem() instanceof ItemCollar collar) ||
!collar.canShock()
) {
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.ERROR,
name + " is not wearing a shock collar!"
);
return;
}
// Check if sender is owner of the collar or collar is public
// FIX: Always check permissions for ANY collar that can shock, not just ItemShockCollar
boolean isOwner = collar.isOwner(collarStack, sender);
boolean isPublic = false;
// ItemShockCollar has additional "public mode" that allows anyone to shock
if (collarStack.getItem() instanceof ItemShockCollar shockCollar) {
isPublic = shockCollar.isPublic(collarStack);
}
if (!isOwner && !isPublic) {
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.ERROR,
"You don't have permission to shock " + name + "!"
);
return;
}
// Execute shock
target.shockKidnapped(
" (Remote shock from " + sender.getName().getString() + ")",
3.0f
);
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.SHOCKER_TRIGGERED,
name
);
TiedUpMod.LOGGER.debug(
"[PACKET] {} shocked slave {}",
sender.getName().getString(),
name
);
}
private void handleLocate(
ServerPlayer sender,
IRestrainable target,
LivingEntity targetEntity,
String name
) {
// Check if target has GPS collar
if (!target.hasCollar()) {
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.ERROR,
name + " is not wearing a collar!"
);
return;
}
ItemStack collarStack = target.getEquipment(BodyRegionV2.NECK);
if (
!(collarStack.getItem() instanceof ItemCollar collar) ||
!collar.hasGPS()
) {
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.ERROR,
name + " is not wearing a GPS collar!"
);
return;
}
// Check permissions
if (collarStack.getItem() instanceof ItemGpsCollar gpsCollar) {
boolean isOwner = collar.isOwner(collarStack, sender);
boolean isPublic = gpsCollar.hasPublicTracking(collarStack);
if (!isOwner && !isPublic) {
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.ERROR,
"You don't have permission to track " + name + "!"
);
return;
}
}
// Check same dimension
if (
targetEntity == null ||
!targetEntity.level().dimension().equals(sender.level().dimension())
) {
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.ERROR,
"Cannot locate " + name + " (different dimension or offline)!"
);
return;
}
// Calculate distance and direction
double distance = sender.distanceTo(targetEntity);
String direction = getDirection(sender, targetEntity);
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.LOCATOR_DETECTED,
name + ": " + (int) distance + "m [" + direction + "]"
);
TiedUpMod.LOGGER.debug(
"[PACKET] {} located slave {} at {}m {}",
sender.getName().getString(),
name,
(int) distance,
direction
);
}
// Phase 17: PlayerKidnapperManager → PlayerCaptorManager, isFormalSlave → isFormalCaptive
private void handleFree(
ServerPlayer sender,
IRestrainable target,
PlayerCaptorManager manager,
String name,
boolean isFormalCaptive
) {
if (isFormalCaptive && manager != null) {
// Remove formal captive from manager
manager.removeCaptive(target, false);
SystemMessageManager.sendToPlayer(
sender,
"Freed " + name + "!",
ChatFormatting.GREEN
);
TiedUpMod.LOGGER.debug(
"[PACKET] {} freed captive {}",
sender.getName().getString(),
name
);
} else {
// For collar-owned entities, just remove collar ownership
ItemStack collarStack = target.getEquipment(BodyRegionV2.NECK);
if (collarStack.getItem() instanceof ItemCollar collar) {
collar.removeOwner(collarStack, sender.getUUID());
SystemMessageManager.sendToPlayer(
sender,
"Released collar control of " + name + "!",
ChatFormatting.GREEN
);
TiedUpMod.LOGGER.debug(
"[PACKET] {} released collar control of {}",
sender.getName().getString(),
name
);
}
}
}
private String getDirection(LivingEntity source, LivingEntity target) {
double dx = target.getX() - source.getX();
double dz = target.getZ() - source.getZ();
if (Math.abs(dx) > Math.abs(dz)) {
return dx > 0 ? "EAST" : "WEST";
} else {
return dz > 0 ? "SOUTH" : "NORTH";
}
}
}

View File

@@ -0,0 +1,657 @@
package com.tiedup.remake.network.slave;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.ILockable;
import com.tiedup.remake.network.sync.SyncManager;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import java.util.UUID;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.network.NetworkEvent;
/**
* Packet sent from client to server when master uses key to manage slave's items.
* Handles LOCK, UNLOCK, and REMOVE operations using the keyUUID system.
*
* Phase 20: Key-Lock System - Slave Item Management
*/
public class PacketSlaveItemManage {
/**
* Action to perform on the item.
*/
public enum Action {
LOCK,
UNLOCK,
REMOVE,
TOGGLE_BONDAGE_SERVICE,
}
private final UUID targetEntityUUID;
private final BodyRegionV2 region;
private final Action action;
private final UUID keyUUID;
private final boolean isMasterKey;
public PacketSlaveItemManage(
UUID targetEntityUUID,
BodyRegionV2 region,
Action action,
UUID keyUUID,
boolean isMasterKey
) {
this.targetEntityUUID = targetEntityUUID;
this.region = region;
this.action = action;
this.keyUUID = keyUUID;
this.isMasterKey = isMasterKey;
}
/**
* Encode packet to buffer.
*/
public static void encode(PacketSlaveItemManage msg, FriendlyByteBuf buf) {
buf.writeUUID(msg.targetEntityUUID);
buf.writeEnum(msg.region);
buf.writeEnum(msg.action);
buf.writeBoolean(msg.keyUUID != null);
if (msg.keyUUID != null) {
buf.writeUUID(msg.keyUUID);
}
buf.writeBoolean(msg.isMasterKey);
}
/**
* Decode packet from buffer.
*/
public static PacketSlaveItemManage decode(FriendlyByteBuf buf) {
UUID targetEntityUUID = buf.readUUID();
BodyRegionV2 region = buf.readEnum(BodyRegionV2.class);
Action action = buf.readEnum(Action.class);
UUID keyUUID = buf.readBoolean() ? buf.readUUID() : null;
boolean isMasterKey = buf.readBoolean();
return new PacketSlaveItemManage(
targetEntityUUID,
region,
action,
keyUUID,
isMasterKey
);
}
/**
* Handle packet on server.
*/
public static void handle(
PacketSlaveItemManage msg,
Supplier<NetworkEvent.Context> ctx
) {
ctx
.get()
.enqueueWork(() -> {
ServerPlayer sender = ctx.get().getSender();
if (sender == null) return;
// Rate limiting: Prevent item management spam
if (
!com.tiedup.remake.network.PacketRateLimiter.allowPacket(
sender,
"action"
)
) {
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.ERROR,
"Too fast! Wait a moment."
);
return;
}
// Find target entity (player or NPC)
LivingEntity targetEntity = findTargetEntity(
sender,
msg.targetEntityUUID
);
if (targetEntity == null) {
TiedUpMod.LOGGER.debug(
"[PacketSlaveItemManage] Target entity not found: {}",
msg.targetEntityUUID
);
return;
}
// Check distance (max 5 blocks)
if (sender.distanceTo(targetEntity) > 5.0) {
TiedUpMod.LOGGER.debug(
"[PacketSlaveItemManage] Sender too far from target"
);
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.ERROR,
"Too far! Get closer to manage items."
);
return;
}
// Get target's kidnapped state
IBondageState targetState = KidnappedHelper.getKidnappedState(
targetEntity
);
if (targetState == null) {
TiedUpMod.LOGGER.debug(
"[PacketSlaveItemManage] Target has no kidnapped state"
);
return;
}
// Target must have a collar
if (!targetState.hasCollar()) {
TiedUpMod.LOGGER.debug(
"[PacketSlaveItemManage] Target has no collar"
);
return;
}
// Security: Verify sender owns the collar (or is admin)
ItemStack collarStack = targetState.getEquipment(BodyRegionV2.NECK);
if (
collarStack.getItem() instanceof
com.tiedup.remake.items.base.ItemCollar collar
) {
if (
!collar.isOwner(collarStack, sender) &&
!sender.hasPermissions(2)
) {
TiedUpMod.LOGGER.debug(
"[PacketSlaveItemManage] Sender is not collar owner and not admin"
);
return;
}
}
// Furniture seat blocks this region
if (targetEntity.isPassenger() && targetEntity.getVehicle() instanceof com.tiedup.remake.v2.furniture.ISeatProvider provider) {
com.tiedup.remake.v2.furniture.SeatDefinition seat = provider.getSeatForPassenger(targetEntity);
if (seat != null && seat.blockedRegions().contains(msg.region)) {
return; // Region blocked by furniture
}
}
// Get item from region (V2 storage)
ItemStack itemStack = getItemInRegion(
targetState,
msg.region
);
if (itemStack.isEmpty()) {
TiedUpMod.LOGGER.debug(
"[PacketSlaveItemManage] Region {} is empty",
msg.region
);
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.ERROR,
"Region " + msg.region.name() + " is empty."
);
return;
}
// Perform action
switch (msg.action) {
case LOCK -> handleLock(
sender,
targetEntity,
targetState,
msg.region,
itemStack,
msg.keyUUID,
msg.isMasterKey
);
case UNLOCK -> handleUnlock(
sender,
targetEntity,
targetState,
msg.region,
itemStack,
msg.keyUUID,
msg.isMasterKey
);
case REMOVE -> handleRemove(
sender,
targetEntity,
targetState,
msg.region,
itemStack,
msg.keyUUID,
msg.isMasterKey
);
case TOGGLE_BONDAGE_SERVICE -> handleToggleBondageService(
sender,
targetEntity,
targetState,
collarStack
);
}
// Sync changes
if (targetEntity instanceof ServerPlayer targetPlayer) {
SyncManager.syncAll(targetPlayer);
} else {
// For NPCs: V2 sync via EntityDataAccessor (IV2EquipmentHolder)
// or packet-based sync for other entity types
V2EquipmentHelper.sync(targetEntity);
}
TiedUpMod.LOGGER.debug(
"[PacketSlaveItemManage] {} performed {} on {}'s {} region",
sender.getName().getString(),
msg.action,
targetEntity.getName().getString(),
msg.region
);
});
ctx.get().setPacketHandled(true);
}
/**
* Find target entity by UUID (player or NPC).
*/
static LivingEntity findTargetEntity(
ServerPlayer sender,
UUID targetUUID
) {
// Try player first
Player player = sender.level().getPlayerByUUID(targetUUID);
if (player != null) {
return player;
}
// Try other entities within reasonable range (64 blocks)
net.minecraft.world.phys.AABB searchBox = sender
.getBoundingBox()
.inflate(64);
for (LivingEntity entity : sender
.level()
.getEntitiesOfClass(LivingEntity.class, searchBox)) {
if (entity.getUUID().equals(targetUUID)) {
return entity;
}
}
return null;
}
/**
* Get item from a V2 body region.
*/
private static ItemStack getItemInRegion(
IBondageState state,
BodyRegionV2 region
) {
return state.getItemInRegion(region);
}
/**
* SECURITY: Verifies that the player actually holds a Master Key in their inventory.
* Prevents client-side spoofing of the isMasterKey boolean.
*
* @param player The player to check
* @return true if player holds Master Key in main hand or offhand
*/
private static boolean verifyMasterKey(ServerPlayer player) {
ItemStack mainHand = player.getMainHandItem();
ItemStack offHand = player.getOffhandItem();
return (
mainHand.getItem() ==
com.tiedup.remake.items.ModItems.MASTER_KEY.get() ||
offHand.getItem() ==
com.tiedup.remake.items.ModItems.MASTER_KEY.get()
);
}
/**
* SECURITY FIX: Verifies that the player actually holds a physical key with the specified UUID.
* Prevents key spoofing exploit where clients could send arbitrary UUIDs to unlock items.
*
* @param player The player to check
* @param keyUUID The key UUID to verify
* @return true if player possesses a physical ItemKey with matching UUID
*/
private static boolean verifyPlayerHasKey(
ServerPlayer player,
UUID keyUUID
) {
if (keyUUID == null) return false;
// Search entire player inventory for a key with matching UUID
for (ItemStack stack : player.getInventory().items) {
if (
stack.getItem() instanceof com.tiedup.remake.items.ItemKey key
) {
if (keyUUID.equals(key.getKeyUUID(stack))) {
return true; // Found matching key
}
}
}
// Also check offhand
ItemStack offhand = player.getOffhandItem();
if (offhand.getItem() instanceof com.tiedup.remake.items.ItemKey key) {
if (keyUUID.equals(key.getKeyUUID(offhand))) {
return true;
}
}
return false; // Player does not possess this key
}
/**
* Handle LOCK action - locks the item with the key's UUID.
* Item must have a padlock attached (isLockable) to be locked.
*/
private static void handleLock(
ServerPlayer sender,
LivingEntity target,
IBondageState targetState,
BodyRegionV2 region,
ItemStack itemStack,
UUID keyUUID,
boolean clientClaimsMasterKey
) {
// SECURITY: Verify master key server-side (don't trust client boolean)
boolean actuallyHasMasterKey = verifyMasterKey(sender);
// Master key cannot lock items
if (actuallyHasMasterKey) {
TiedUpMod.LOGGER.debug(
"[PacketSlaveItemManage] Master key cannot lock items"
);
return;
}
// SECURITY: Detect spoofing attempt
if (clientClaimsMasterKey && !actuallyHasMasterKey) {
TiedUpMod.LOGGER.warn(
"SECURITY: Player {} attempted to spoof Master Key in LOCK operation!",
sender.getName().getString()
);
return;
}
// Key UUID required for locking
if (keyUUID == null) {
TiedUpMod.LOGGER.debug(
"[PacketSlaveItemManage] No key UUID provided for locking"
);
return;
}
// SECURITY FIX: Verify player actually possesses the key
if (!verifyPlayerHasKey(sender, keyUUID)) {
TiedUpMod.LOGGER.warn(
"SECURITY: Player {} attempted to lock with non-existent key UUID {}!",
sender.getName().getString(),
keyUUID
);
return;
}
// Item must be ILockable
if (!(itemStack.getItem() instanceof ILockable lockable)) {
TiedUpMod.LOGGER.debug(
"[PacketSlaveItemManage] Item is not ILockable: {}",
itemStack
);
return;
}
// Item must have a padlock attached (isLockable = true)
if (!lockable.isLockable(itemStack)) {
TiedUpMod.LOGGER.debug(
"[PacketSlaveItemManage] Item has no padlock attached. Use padlock first."
);
return;
}
// Item must not already be locked
if (lockable.isLocked(itemStack)) {
TiedUpMod.LOGGER.debug(
"[PacketSlaveItemManage] Item is already locked"
);
return;
}
// Lock with keyUUID
lockable.setLockedByKeyUUID(itemStack, keyUUID);
// Phase 21: Add lock resistance (configurable, default 250) to bind items when locked
if (
region == BodyRegionV2.ARMS &&
itemStack.getItem() instanceof
com.tiedup.remake.items.base.ItemBind bind
) {
int currentResistance = bind.getCurrentResistance(
itemStack,
target
);
int lockResistance = lockable.getLockResistance(); // Configurable via ModConfig
bind.setCurrentResistance(
itemStack,
currentResistance + lockResistance
);
TiedUpMod.LOGGER.info(
"[PacketSlaveItemManage] Added {} lock resistance to bind (total: {})",
lockResistance,
currentResistance + lockResistance
);
}
TiedUpMod.LOGGER.info(
"[PacketSlaveItemManage] {} locked {}'s {} with key {}",
sender.getName().getString(),
target.getName().getString(),
region,
keyUUID
);
}
/**
* Handle UNLOCK action - unlocks the item if key matches or master key.
* Drops the padlock and removes the lockable state.
*/
private static void handleUnlock(
ServerPlayer sender,
LivingEntity target,
IBondageState targetState,
BodyRegionV2 region,
ItemStack itemStack,
UUID keyUUID,
boolean clientClaimsMasterKey
) {
// Item must be ILockable
if (!(itemStack.getItem() instanceof ILockable lockable)) {
TiedUpMod.LOGGER.debug(
"[PacketSlaveItemManage] Item is not ILockable: {}",
itemStack
);
return;
}
// Item must be locked
if (!lockable.isLocked(itemStack)) {
TiedUpMod.LOGGER.debug(
"[PacketSlaveItemManage] Item is not locked"
);
return;
}
// SECURITY: Verify master key server-side (don't trust client boolean)
boolean actuallyHasMasterKey = verifyMasterKey(sender);
// SECURITY: Detect spoofing attempt
if (clientClaimsMasterKey && !actuallyHasMasterKey) {
TiedUpMod.LOGGER.warn(
"SECURITY: Player {} attempted to spoof Master Key in UNLOCK operation!",
sender.getName().getString()
);
return;
}
// Check if key matches (or ACTUAL master key bypasses)
if (!actuallyHasMasterKey) {
UUID lockedByUUID = lockable.getLockedByKeyUUID(itemStack);
// SECURITY FIX: Verify player actually possesses the key BEFORE checking if it matches
// Previous exploit: Client could send any UUID that matched lockedByUUID without owning the key
if (keyUUID != null && !verifyPlayerHasKey(sender, keyUUID)) {
TiedUpMod.LOGGER.warn(
"SECURITY: Player {} attempted to unlock with non-existent key UUID {}!",
sender.getName().getString(),
keyUUID
);
return;
}
if (lockedByUUID != null && !lockedByUUID.equals(keyUUID)) {
TiedUpMod.LOGGER.debug(
"[PacketSlaveItemManage] Wrong key! Item locked by different key"
);
return;
}
}
// Unlock the item - just unlock, keep padlock attached (can be re-locked)
lockable.setLockedByKeyUUID(itemStack, null);
lockable.clearLockResistance(itemStack); // Clear any struggle progress
// Note: Padlock is only dropped on REMOVE, not on UNLOCK
TiedUpMod.LOGGER.info(
"[PacketSlaveItemManage] {} unlocked {}'s {} (masterKey={})",
sender.getName().getString(),
target.getName().getString(),
region,
actuallyHasMasterKey
);
}
/**
* Handle REMOVE action - removes item from region and gives to sender.
* Padlock stays attached to the item (permanent via anvil).
*/
private static void handleRemove(
ServerPlayer sender,
LivingEntity target,
IBondageState targetState,
BodyRegionV2 region,
ItemStack itemStack,
UUID keyUUID,
boolean isMasterKey
) {
// Note: Remove button is hidden when locked, so this check is just a safety
if (itemStack.getItem() instanceof ILockable lockable) {
if (lockable.isLocked(itemStack)) {
TiedUpMod.LOGGER.debug(
"[PacketSlaveItemManage] Cannot remove: item is locked!"
);
return;
}
}
// Remove from region (V2 storage)
ItemStack removed = removeFromRegion(targetState, region);
if (!removed.isEmpty()) {
// Give item to sender (or drop if inventory full)
// Note: Padlock stays on the item (lockable state preserved)
if (!sender.getInventory().add(removed.copy())) {
sender.drop(removed.copy(), false);
}
TiedUpMod.LOGGER.info(
"[PacketSlaveItemManage] {} removed {} from {}'s {} region",
sender.getName().getString(),
removed.getDisplayName().getString(),
target.getName().getString(),
region
);
}
}
/**
* Handle TOGGLE_BONDAGE_SERVICE action - toggles bondage service on the collar.
* Requires a prison to be configured on the collar.
*/
private static void handleToggleBondageService(
ServerPlayer sender,
LivingEntity target,
IBondageState targetState,
ItemStack collarStack
) {
if (
collarStack.isEmpty() ||
!(collarStack.getItem() instanceof
com.tiedup.remake.items.base.ItemCollar collar)
) {
TiedUpMod.LOGGER.debug(
"[PacketSlaveItemManage] No collar for bondage service toggle"
);
return;
}
// Check if cell is configured (required for bondage service)
if (!collar.hasCellAssigned(collarStack)) {
TiedUpMod.LOGGER.debug(
"[PacketSlaveItemManage] Cannot enable bondage service: no cell configured"
);
// Send feedback to player
SystemMessageManager.sendToPlayer(
sender,
SystemMessageManager.MessageCategory.ERROR,
"Cannot enable Bondage Service: Assign a cell first!"
);
return;
}
// Toggle bondage service
boolean currentState = collar.isBondageServiceEnabled(collarStack);
collar.setBondageServiceEnabled(collarStack, !currentState);
String newState = !currentState ? "enabled" : "disabled";
TiedUpMod.LOGGER.info(
"[PacketSlaveItemManage] {} {} bondage service on {}'s collar",
sender.getName().getString(),
newState,
target.getName().getString()
);
// Send feedback
SystemMessageManager.sendChatToPlayer(
sender,
"Bondage Service " +
newState +
" on " +
target.getName().getString(),
!currentState
? net.minecraft.ChatFormatting.LIGHT_PURPLE
: net.minecraft.ChatFormatting.GRAY
);
}
/**
* Remove item from a V2 body region.
*/
private static ItemStack removeFromRegion(
IBondageState state,
BodyRegionV2 region
) {
return state.unequipFromRegion(region);
}
}

View File

@@ -0,0 +1,255 @@
package com.tiedup.remake.network.sync;
import com.mojang.logging.LogUtils;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.state.IBondageState;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
import net.minecraft.client.Minecraft;
import net.minecraft.world.entity.player.Player;
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;
import org.slf4j.Logger;
/**
* Client-side sync queue handler.
*
* This class is ONLY loaded on the client side to avoid NoClassDefFoundError
* on dedicated servers. It handles the pending sync queue for players that
* are not yet loaded when packets arrive.
*
* Separated from SyncManager to ensure server-side code doesn't trigger
* class loading of Minecraft client classes.
*/
@OnlyIn(Dist.CLIENT)
@Mod.EventBusSubscriber(modid = TiedUpMod.MOD_ID, value = Dist.CLIENT)
public class ClientSyncHandler {
private static final Logger LOGGER = LogUtils.getLogger();
// ==================== Configuration ====================
/** Maximum ticks before a pending sync entry expires (5 seconds) */
private static final int MAX_PENDING_TICKS = 100;
/** Maximum retry attempts before giving up */
private static final int MAX_RETRIES = 20;
/** Ticks between retry attempts */
private static final int RETRY_INTERVAL = 5;
// ==================== Client-side pending queue ====================
/**
* Queue of pending sync entries waiting to be applied.
* Thread-safe for access from network thread and client tick.
*/
private static final Queue<PendingSyncEntry> pendingQueue =
new ConcurrentLinkedQueue<>();
/**
* Track last known tick for expiration checks.
* Volatile to ensure visibility across network and tick threads.
*/
private static volatile long currentClientTick = 0;
// ==================== Queue methods ====================
/**
* Queue a bind state sync for later processing.
* Called from PacketSyncBindState.handle() when target player is null.
*
* @param targetUUID The UUID of the player to sync
* @param stateFlags The state flags to apply
*/
public static void queueBindStateSync(UUID targetUUID, byte stateFlags) {
PendingSyncEntry entry = PendingSyncEntry.forBindState(
targetUUID,
stateFlags,
currentClientTick
);
pendingQueue.add(entry);
LOGGER.debug(
"[ClientSyncHandler] Queued bind state sync for {} (queue size: {})",
targetUUID,
pendingQueue.size()
);
}
/**
* Queue an enslavement sync for later processing.
* Called from PacketSyncEnslavement.handle() when target player is null.
*
* @param targetUUID The UUID of the player to sync
* @param masterUUID The master's UUID (null if free)
* @param transportUUID The transport entity UUID (null if no transport)
* @param isEnslaved Whether the player is enslaved
*/
public static void queueEnslavementSync(
UUID targetUUID,
UUID masterUUID,
UUID transportUUID,
boolean isEnslaved
) {
PendingSyncEntry entry = PendingSyncEntry.forEnslavement(
targetUUID,
masterUUID,
transportUUID,
isEnslaved,
currentClientTick
);
pendingQueue.add(entry);
LOGGER.debug(
"[ClientSyncHandler] Queued enslavement sync for {} (queue size: {})",
targetUUID,
pendingQueue.size()
);
}
/**
* Process the pending sync queue. Called every few ticks on client.
* Attempts to apply queued syncs and removes expired entries.
*/
private static void processPendingQueue() {
if (pendingQueue.isEmpty()) return;
Minecraft mc = Minecraft.getInstance();
if (mc.level == null) return;
int processed = 0;
int expired = 0;
// Process queue
Iterator<PendingSyncEntry> iterator = pendingQueue.iterator();
while (iterator.hasNext()) {
PendingSyncEntry entry = iterator.next();
// Check expiration
if (
entry.isExpired(
currentClientTick,
MAX_PENDING_TICKS,
MAX_RETRIES
)
) {
iterator.remove();
expired++;
continue;
}
// Check if retry interval has passed
if (entry.getRetryCount() > 0) {
long ticksSinceCreation =
currentClientTick - entry.getCreationTick();
if (ticksSinceCreation % RETRY_INTERVAL != 0) {
continue; // Wait for retry interval
}
}
// Try to find the player
Player player = mc.level.getPlayerByUUID(
entry.getTargetPlayerUUID()
);
if (player == null) {
// Player still not loaded, increment retry count
entry.incrementRetryCount();
continue;
}
// Player found! Apply the sync
boolean success = applyPendingSync(entry, player);
if (success) {
iterator.remove();
processed++;
}
}
if (processed > 0 || expired > 0) {
LOGGER.debug(
"[ClientSyncHandler] Processed {} pending, {} expired",
processed,
expired
);
}
}
/**
* Apply a pending sync entry to a player.
*
* @param entry The sync entry
* @param player The target player
* @return true if successful
*/
private static boolean applyPendingSync(
PendingSyncEntry entry,
Player player
) {
switch (entry.getType()) {
case BIND_STATE -> {
// Bind state is derived from inventory, so just trigger a state check
// The actual flags are informational
return true;
}
case ENSLAVEMENT -> {
// Enslavement state is primarily server-side, but we store it for UI awareness
// The actual leash rendering uses Minecraft's entity system
IBondageState kidnappedState =
com.tiedup.remake.util.KidnappedHelper.getKidnappedState(
player
);
if (kidnappedState != null) {
// Client now knows about this player's enslavement state
// This enables UI indicators and rendering awareness
LOGGER.debug(
"[ClientSyncHandler] Applied enslavement sync for {} (enslaved: {})",
player.getName().getString(),
entry.isEnslaved()
);
}
return true;
}
default -> {
return false;
}
}
}
/**
* Clear all pending syncs for a player (e.g., when they disconnect).
*
* @param playerUUID The player's UUID
*/
public static void clearPendingForPlayer(UUID playerUUID) {
pendingQueue.removeIf(entry ->
entry.getTargetPlayerUUID().equals(playerUUID)
);
}
/**
* Clear the entire pending queue (e.g., when disconnecting from server).
*/
public static void clearAllPending() {
pendingQueue.clear();
LOGGER.debug("[ClientSyncHandler] Cleared all pending syncs");
}
// ==================== Tick handler ====================
/**
* Client tick handler to process pending syncs.
*/
@SubscribeEvent
public static void onClientTick(TickEvent.ClientTickEvent event) {
if (event.phase != TickEvent.Phase.END) return;
currentClientTick++;
// Process queue every RETRY_INTERVAL ticks
if (currentClientTick % RETRY_INTERVAL == 0) {
processPendingQueue();
}
}
}

View File

@@ -0,0 +1,51 @@
package com.tiedup.remake.network.sync;
import com.tiedup.remake.network.base.AbstractPlayerSyncPacket;
import java.util.UUID;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.entity.player.Player;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
/**
* Test packet to play an arbitrary animation on a player.
* Direction: Server → Client (S2C)
* Used by /tiedup testanim command for debugging animations.
*/
public class PacketPlayTestAnimation extends AbstractPlayerSyncPacket {
private final String animId;
public PacketPlayTestAnimation(UUID playerUUID, String animId) {
super(playerUUID);
this.animId = animId;
}
public void encode(FriendlyByteBuf buf) {
encodeUUID(buf);
buf.writeUtf(animId);
}
public static PacketPlayTestAnimation decode(FriendlyByteBuf buf) {
UUID uuid = decodeUUID(buf);
String animId = buf.readUtf();
return new PacketPlayTestAnimation(uuid, animId);
}
@Override
@OnlyIn(Dist.CLIENT)
protected void applySync(Player player) {
ClientHandler.handle(this, player);
}
@OnlyIn(Dist.CLIENT)
private static class ClientHandler {
private static void handle(PacketPlayTestAnimation pkt, Player player) {
if (pkt.animId.isEmpty()) {
com.tiedup.remake.client.animation.BondageAnimationManager.stopAnimation(player);
} else {
com.tiedup.remake.client.animation.BondageAnimationManager.playAnimation(player, pkt.animId);
}
}
}
}

View File

@@ -0,0 +1,140 @@
package com.tiedup.remake.network.sync;
import com.tiedup.remake.network.base.AbstractPlayerSyncPacket;
import com.tiedup.remake.state.PlayerBindState;
import java.util.UUID;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.entity.player.Player;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
/**
* Packet to synchronize player bondage state (tied, gagged, etc.).
* Sends boolean flags for bind state (lighter than full equipment sync).
*
* Direction: Server → Client (S2C)
*/
public class PacketSyncBindState extends AbstractPlayerSyncPacket {
private final byte stateFlags; // Packed boolean flags
// Bit flags for state
private static final byte FLAG_TIED_UP = 1 << 0; // 0x01
private static final byte FLAG_GAGGED = 1 << 1; // 0x02
private static final byte FLAG_BLINDFOLDED = 1 << 2; // 0x04
private static final byte FLAG_HAS_EARPLUGS = 1 << 3; // 0x08
private static final byte FLAG_HAS_COLLAR = 1 << 4; // 0x10
private static final byte FLAG_HAS_CLOTHES = 1 << 5; // 0x20
private static final byte FLAG_ONLINE = 1 << 6; // 0x40
/**
* Create a packet to sync a player's bondage state.
*
* @param playerUUID The player's UUID
* @param stateFlags Packed boolean flags
*/
private PacketSyncBindState(UUID playerUUID, byte stateFlags) {
super(playerUUID);
this.stateFlags = stateFlags;
}
/**
* Create a packet from a player's current state.
*
* @param player The player
* @return The packet, or null if player has no state
*/
public static PacketSyncBindState fromPlayer(Player player) {
PlayerBindState state = PlayerBindState.getInstance(player);
if (state == null) {
return null;
}
byte flags = 0;
if (state.isTiedUp()) flags |= FLAG_TIED_UP;
if (state.isGagged()) flags |= FLAG_GAGGED;
if (state.isBlindfolded()) flags |= FLAG_BLINDFOLDED;
if (state.hasEarplugs()) flags |= FLAG_HAS_EARPLUGS;
if (state.hasCollar()) flags |= FLAG_HAS_COLLAR;
if (state.hasClothes()) flags |= FLAG_HAS_CLOTHES;
if (state.isOnline()) flags |= FLAG_ONLINE;
return new PacketSyncBindState(player.getUUID(), flags);
}
/**
* Encode the packet to a byte buffer.
*/
public void encode(FriendlyByteBuf buf) {
encodeUUID(buf);
buf.writeByte(stateFlags);
}
/**
* Decode the packet from a byte buffer.
*/
public static PacketSyncBindState decode(FriendlyByteBuf buf) {
UUID playerUUID = decodeUUID(buf);
byte stateFlags = buf.readByte();
return new PacketSyncBindState(playerUUID, stateFlags);
}
/**
* Apply the bind state sync to a player.
*
* This method is intentionally a no-op:
* - State is automatically derived from inventory via V2EquipmentHelper
* - Rendering updates are handled by event handlers
* - The packet serves as a lightweight notification that state has changed
*/
@Override
@OnlyIn(Dist.CLIENT)
protected void applySync(Player player) {
// No-op: State is derived from inventory (synced via PacketSyncV2Equipment)
}
@Override
@OnlyIn(Dist.CLIENT)
protected void queueForRetry() {
SyncManager.queueBindStateSync(playerUUID, stateFlags);
}
// Helper method
private boolean hasFlag(byte flag) {
return (stateFlags & flag) != 0;
}
// Getters for state flags
public boolean isTiedUp() {
return hasFlag(FLAG_TIED_UP);
}
public boolean isGagged() {
return hasFlag(FLAG_GAGGED);
}
public boolean isBlindfolded() {
return hasFlag(FLAG_BLINDFOLDED);
}
public boolean hasEarplugs() {
return hasFlag(FLAG_HAS_EARPLUGS);
}
public boolean hasCollar() {
return hasFlag(FLAG_HAS_COLLAR);
}
public boolean hasClothes() {
return hasFlag(FLAG_HAS_CLOTHES);
}
public boolean isOnline() {
return hasFlag(FLAG_ONLINE);
}
public byte getStateFlags() {
return stateFlags;
}
}

View File

@@ -0,0 +1,212 @@
package com.tiedup.remake.network.sync;
import com.tiedup.remake.items.clothes.ClothesProperties;
import com.tiedup.remake.items.clothes.GenericClothes;
import com.tiedup.remake.network.base.AbstractClientPacket;
import java.util.EnumSet;
import java.util.UUID;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
/**
* Packet to synchronize clothes configuration from server to clients.
* Sent when a player's clothes settings change (URL, fullSkin, smallArms, layers).
*
* Direction: Server → Client (S2C)
*
* This packet enables other players to see dynamic textures on clothes worn by players.
*/
public class PacketSyncClothesConfig extends AbstractClientPacket {
private final UUID playerUUID;
private final boolean hasClothes;
private final String dynamicUrl; // May be null/empty
private final boolean fullSkin;
private final boolean smallArms;
private final boolean keepHead;
private final byte layerVisibility; // Bitfield for 6 layers
/**
* Create a packet for a player wearing clothes.
*
* @param playerUUID The player's UUID
* @param clothes The clothes ItemStack (may be empty)
*/
public PacketSyncClothesConfig(UUID playerUUID, ItemStack clothes) {
this.playerUUID = playerUUID;
if (
!clothes.isEmpty() && clothes.getItem() instanceof GenericClothes gc
) {
this.hasClothes = true;
this.dynamicUrl = gc.getDynamicTextureUrl(clothes);
this.fullSkin = gc.isFullSkinEnabled(clothes);
this.smallArms = gc.shouldForceSmallArms(clothes);
this.keepHead = gc.isKeepHeadEnabled(clothes);
this.layerVisibility = encodeLayerVisibility(gc, clothes);
} else {
this.hasClothes = false;
this.dynamicUrl = null;
this.fullSkin = false;
this.smallArms = false;
this.keepHead = false;
this.layerVisibility = 0b111111; // All visible (default)
}
}
/**
* Create a packet indicating no clothes.
*
* @param playerUUID The player's UUID
*/
public static PacketSyncClothesConfig noClothes(UUID playerUUID) {
return new PacketSyncClothesConfig(playerUUID, ItemStack.EMPTY);
}
/**
* Private constructor for decoding.
*/
private PacketSyncClothesConfig(
UUID playerUUID,
boolean hasClothes,
String dynamicUrl,
boolean fullSkin,
boolean smallArms,
boolean keepHead,
byte layerVisibility
) {
this.playerUUID = playerUUID;
this.hasClothes = hasClothes;
this.dynamicUrl = dynamicUrl;
this.fullSkin = fullSkin;
this.smallArms = smallArms;
this.keepHead = keepHead;
this.layerVisibility = layerVisibility;
}
/**
* Encode layer visibility as a byte bitfield.
*/
private byte encodeLayerVisibility(GenericClothes gc, ItemStack stack) {
byte bits = 0;
if (gc.isLayerEnabled(stack, GenericClothes.LAYER_HEAD)) bits |=
0b000001;
if (gc.isLayerEnabled(stack, GenericClothes.LAYER_BODY)) bits |=
0b000010;
if (gc.isLayerEnabled(stack, GenericClothes.LAYER_LEFT_ARM)) bits |=
0b000100;
if (gc.isLayerEnabled(stack, GenericClothes.LAYER_RIGHT_ARM)) bits |=
0b001000;
if (gc.isLayerEnabled(stack, GenericClothes.LAYER_LEFT_LEG)) bits |=
0b010000;
if (gc.isLayerEnabled(stack, GenericClothes.LAYER_RIGHT_LEG)) bits |=
0b100000;
return bits;
}
/**
* Encode the packet to a byte buffer.
*/
public void encode(FriendlyByteBuf buf) {
buf.writeUUID(playerUUID);
buf.writeBoolean(hasClothes);
if (hasClothes) {
// Write URL (empty string if null)
buf.writeUtf(dynamicUrl != null ? dynamicUrl : "", 2048);
buf.writeBoolean(fullSkin);
buf.writeBoolean(smallArms);
buf.writeBoolean(keepHead);
buf.writeByte(layerVisibility);
}
}
/**
* Decode the packet from a byte buffer.
*/
public static PacketSyncClothesConfig decode(FriendlyByteBuf buf) {
UUID playerUUID = buf.readUUID();
boolean hasClothes = buf.readBoolean();
if (hasClothes) {
String dynamicUrl = buf.readUtf(2048);
if (dynamicUrl.isEmpty()) dynamicUrl = null;
boolean fullSkin = buf.readBoolean();
boolean smallArms = buf.readBoolean();
boolean keepHead = buf.readBoolean();
byte layerVisibility = buf.readByte();
return new PacketSyncClothesConfig(
playerUUID,
true,
dynamicUrl,
fullSkin,
smallArms,
keepHead,
layerVisibility
);
} else {
return new PacketSyncClothesConfig(
playerUUID,
false,
null,
false,
false,
false,
(byte) 0b111111
);
}
}
/**
* Client-side handling - update the clothes cache.
*/
@Override
@OnlyIn(Dist.CLIENT)
protected void handleClientImpl() {
if (hasClothes) {
EnumSet<ClothesProperties.LayerPart> layers =
ClothesProperties.decodeLayerVisibility(layerVisibility);
com.tiedup.remake.client.state.ClothesClientCache.updatePlayerClothes(
playerUUID,
dynamicUrl,
fullSkin,
smallArms,
keepHead,
layers
);
} else {
com.tiedup.remake.client.state.ClothesClientCache.removePlayerClothes(
playerUUID
);
}
}
// Getters
public UUID getPlayerUUID() {
return playerUUID;
}
public boolean hasClothes() {
return hasClothes;
}
public String getDynamicUrl() {
return dynamicUrl;
}
public boolean isFullSkin() {
return fullSkin;
}
public boolean isSmallArms() {
return smallArms;
}
public byte getLayerVisibility() {
return layerVisibility;
}
}

View File

@@ -0,0 +1,125 @@
package com.tiedup.remake.network.sync;
import com.tiedup.remake.network.base.AbstractClientPacket;
import java.util.*;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
/**
* Packet to sync collar registry data from server to client.
*
* Sends the list of collar wearers (slaves) owned by the receiving player.
* This allows the client to display slave management UI without spatial queries.
*
* Direction: Server → Client (S2C)
*/
public class PacketSyncCollarRegistry extends AbstractClientPacket {
// Player's owned slaves (wearer UUIDs)
private final Set<UUID> slaveUUIDs;
// Full sync flag (true = replace all, false = incremental update)
private final boolean fullSync;
// For incremental updates: UUIDs to remove
private final Set<UUID> removedUUIDs;
/**
* Create a full sync packet with all slave UUIDs.
*/
public PacketSyncCollarRegistry(Set<UUID> slaveUUIDs) {
this.slaveUUIDs = new HashSet<>(slaveUUIDs);
this.fullSync = true;
this.removedUUIDs = Collections.emptySet();
}
/**
* Create an incremental update packet.
*
* @param addedUUIDs UUIDs to add to the client's cache
* @param removedUUIDs UUIDs to remove from the client's cache
*/
public PacketSyncCollarRegistry(
Set<UUID> addedUUIDs,
Set<UUID> removedUUIDs
) {
this.slaveUUIDs = new HashSet<>(addedUUIDs);
this.fullSync = false;
this.removedUUIDs = new HashSet<>(removedUUIDs);
}
// ==================== ENCODE/DECODE ====================
public void encode(FriendlyByteBuf buf) {
buf.writeBoolean(fullSync);
// Write slave UUIDs
buf.writeVarInt(slaveUUIDs.size());
for (UUID uuid : slaveUUIDs) {
buf.writeUUID(uuid);
}
// Write removed UUIDs (only for incremental)
if (!fullSync) {
buf.writeVarInt(removedUUIDs.size());
for (UUID uuid : removedUUIDs) {
buf.writeUUID(uuid);
}
}
}
public static PacketSyncCollarRegistry decode(FriendlyByteBuf buf) {
boolean fullSync = buf.readBoolean();
// Read slave UUIDs (cap at 10000 to prevent memory exhaustion from malformed packets)
int slaveCount = Math.min(buf.readVarInt(), 10000);
Set<UUID> slaveUUIDs = new HashSet<>(slaveCount);
for (int i = 0; i < slaveCount; i++) {
slaveUUIDs.add(buf.readUUID());
}
if (fullSync) {
return new PacketSyncCollarRegistry(slaveUUIDs);
} else {
// Read removed UUIDs (cap at 10000)
int removedCount = Math.min(buf.readVarInt(), 10000);
Set<UUID> removedUUIDs = new HashSet<>(removedCount);
for (int i = 0; i < removedCount; i++) {
removedUUIDs.add(buf.readUUID());
}
return new PacketSyncCollarRegistry(slaveUUIDs, removedUUIDs);
}
}
// ==================== HANDLE ====================
@Override
@OnlyIn(Dist.CLIENT)
protected void handleClientImpl() {
if (fullSync) {
com.tiedup.remake.client.state.CollarRegistryClient.setSlaves(
slaveUUIDs
);
} else {
com.tiedup.remake.client.state.CollarRegistryClient.updateSlaves(
slaveUUIDs,
removedUUIDs
);
}
}
// ==================== GETTERS ====================
public Set<UUID> getSlaveUUIDs() {
return Collections.unmodifiableSet(slaveUUIDs);
}
public boolean isFullSync() {
return fullSync;
}
public Set<UUID> getRemovedUUIDs() {
return Collections.unmodifiableSet(removedUUIDs);
}
}

View File

@@ -0,0 +1,169 @@
package com.tiedup.remake.network.sync;
import com.tiedup.remake.network.base.AbstractPlayerSyncPacket;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.state.ICaptor;
import com.tiedup.remake.util.KidnappedHelper;
import java.util.UUID;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
/**
* Packet to synchronize enslavement state between server and client.
* Sent when a player is enslaved, freed, or transfers masters.
*
* Direction: Server → Client (S2C)
*/
public class PacketSyncEnslavement extends AbstractPlayerSyncPacket {
private final UUID masterUUID; // The master's UUID (null if free)
private final UUID transportUUID; // The transport entity's UUID (null if no transport)
private final boolean isEnslaved; // Quick flag for state check
/**
* Create a packet to sync a player's enslavement state.
*
* @param targetUUID The slave player's UUID
* @param masterUUID The master's UUID, or null if not enslaved
* @param transportUUID The transport entity's UUID, or null if no transport
* @param isEnslaved Whether the player is currently enslaved
*/
public PacketSyncEnslavement(
UUID targetUUID,
UUID masterUUID,
UUID transportUUID,
boolean isEnslaved
) {
super(targetUUID);
this.masterUUID = masterUUID;
this.transportUUID = transportUUID;
this.isEnslaved = isEnslaved;
}
/**
* Create a packet from a player's current enslavement state.
*
* @param player The player
* @return The packet, or null if player has no kidnapped state
*/
public static PacketSyncEnslavement fromPlayer(Player player) {
IBondageState state = KidnappedHelper.getKidnappedState(player);
if (state == null) {
return null;
}
UUID captorUUID = null;
UUID proxyUUID = null;
boolean isCaptured = state.isCaptive();
if (isCaptured) {
ICaptor captor = state.getCaptor();
if (captor != null && captor.getEntity() != null) {
captorUUID = captor.getEntity().getUUID();
}
Entity proxy = state.getTransport();
if (proxy != null) {
proxyUUID = proxy.getUUID();
}
}
return new PacketSyncEnslavement(
player.getUUID(),
captorUUID,
proxyUUID,
isCaptured
);
}
/**
* Encode the packet to a byte buffer.
*/
public void encode(FriendlyByteBuf buf) {
encodeUUID(buf);
buf.writeBoolean(isEnslaved);
// Write optional master UUID
buf.writeBoolean(masterUUID != null);
if (masterUUID != null) {
buf.writeUUID(masterUUID);
}
// Write optional transport UUID
buf.writeBoolean(transportUUID != null);
if (transportUUID != null) {
buf.writeUUID(transportUUID);
}
}
/**
* Decode the packet from a byte buffer.
*/
public static PacketSyncEnslavement decode(FriendlyByteBuf buf) {
UUID targetUUID = decodeUUID(buf);
boolean isEnslaved = buf.readBoolean();
UUID masterUUID = null;
if (buf.readBoolean()) {
masterUUID = buf.readUUID();
}
UUID transportUUID = null;
if (buf.readBoolean()) {
transportUUID = buf.readUUID();
}
return new PacketSyncEnslavement(
targetUUID,
masterUUID,
transportUUID,
isEnslaved
);
}
/**
* Apply the enslavement sync to a player.
*
* Note: This is primarily for CLIENT-SIDE visual/state updates.
* The actual enslavement logic is handled server-side.
*/
@Override
@OnlyIn(Dist.CLIENT)
protected void applySync(Player player) {
// Client-side state awareness (for UI indicators, animations)
// The client doesn't directly manipulate enslavement state
// It receives updates from entity syncs
}
@Override
@OnlyIn(Dist.CLIENT)
protected void queueForRetry() {
SyncManager.queueEnslavementSync(
playerUUID,
masterUUID,
transportUUID,
isEnslaved
);
}
// Getters
public UUID getTargetUUID() {
return playerUUID;
}
public UUID getMasterUUID() {
return masterUUID;
}
public UUID getTransportUUID() {
return transportUUID;
}
public boolean isEnslaved() {
return isEnslaved;
}
}

View File

@@ -0,0 +1,85 @@
package com.tiedup.remake.network.sync;
import com.tiedup.remake.network.base.AbstractClientPacket;
import java.util.UUID;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
/**
* Packet sent from server to client to sync leash proxy information.
* Tells the client which proxy entity follows which player.
*
* This allows the client to position the proxy locally each frame,
* rather than waiting for server position updates (much smoother).
*
* Uses UUID for player identification (persistent across restarts)
* and entity ID for proxy (runtime entity only).
*/
public class PacketSyncLeashProxy extends AbstractClientPacket {
/** The player UUID that the proxy follows (persistent) */
private final UUID targetPlayerUUID;
/** The proxy entity ID (runtime only, recreated on restart) */
private final int proxyId;
/** True = attach proxy to player, False = detach */
private final boolean attach;
/**
* Create an attach packet.
*/
public static PacketSyncLeashProxy attach(
UUID targetPlayerUUID,
int proxyId
) {
return new PacketSyncLeashProxy(targetPlayerUUID, proxyId, true);
}
/**
* Create a detach packet.
*/
public static PacketSyncLeashProxy detach(UUID targetPlayerUUID) {
return new PacketSyncLeashProxy(targetPlayerUUID, -1, false);
}
private PacketSyncLeashProxy(
UUID targetPlayerUUID,
int proxyId,
boolean attach
) {
this.targetPlayerUUID = targetPlayerUUID;
this.proxyId = proxyId;
this.attach = attach;
}
/**
* Encode packet to buffer.
*/
public void encode(FriendlyByteBuf buf) {
buf.writeUUID(targetPlayerUUID);
buf.writeInt(proxyId);
buf.writeBoolean(attach);
}
/**
* Decode packet from buffer.
*/
public static PacketSyncLeashProxy decode(FriendlyByteBuf buf) {
UUID targetPlayerUUID = buf.readUUID();
int proxyId = buf.readInt();
boolean attach = buf.readBoolean();
return new PacketSyncLeashProxy(targetPlayerUUID, proxyId, attach);
}
@Override
@OnlyIn(Dist.CLIENT)
protected void handleClientImpl() {
com.tiedup.remake.client.events.LeashProxyClientHandler.handleSyncPacket(
targetPlayerUUID,
proxyId,
attach
);
}
}

View File

@@ -0,0 +1,99 @@
package com.tiedup.remake.network.sync;
import com.tiedup.remake.network.base.AbstractPlayerSyncPacket;
import com.tiedup.remake.v2.bondage.movement.MovementStyle;
import java.util.UUID;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.entity.Pose;
import net.minecraft.world.entity.player.Player;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import org.jetbrains.annotations.Nullable;
/**
* Server-to-client packet that syncs the active movement style for a player.
* Sent when the active style changes (including clearing to null).
*
* <p>Extends {@link AbstractPlayerSyncPacket} for UUID-based player lookup
* with retry support for login race conditions.</p>
*
* <p>Distribution: {@code ModNetwork.sendToAllTrackingAndSelf()} so all
* nearby clients can render the correct animation for remote players.</p>
*/
public class PacketSyncMovementStyle extends AbstractPlayerSyncPacket {
/** Marker byte for "no style" (null). */
private static final byte NO_STYLE = -1;
/** The active style ordinal, or -1 for null. */
private final byte styleOrdinal;
/**
* Construct a sync packet.
*
* @param playerUUID the affected player's UUID
* @param style the active style, or null to clear
*/
public PacketSyncMovementStyle(UUID playerUUID, @Nullable MovementStyle style) {
super(playerUUID);
this.styleOrdinal = style == null ? NO_STYLE : (byte) style.ordinal();
}
private PacketSyncMovementStyle(UUID playerUUID, byte styleOrdinal) {
super(playerUUID);
this.styleOrdinal = styleOrdinal;
}
public void encode(FriendlyByteBuf buf) {
encodeUUID(buf);
buf.writeByte(styleOrdinal);
}
public static PacketSyncMovementStyle decode(FriendlyByteBuf buf) {
UUID uuid = decodeUUID(buf);
byte ordinal = buf.readByte();
return new PacketSyncMovementStyle(uuid, ordinal);
}
@Override
@OnlyIn(Dist.CLIENT)
protected void applySync(Player player) {
ClientHandler.handle(this, player);
}
@Override
@OnlyIn(Dist.CLIENT)
protected void queueForRetry() {
// Movement style will be re-sent on next change or re-resolved on tick.
// Non-critical: skip retry to avoid complexity.
}
@OnlyIn(Dist.CLIENT)
private static class ClientHandler {
private static void handle(PacketSyncMovementStyle pkt, Player player) {
MovementStyle style = null;
if (pkt.styleOrdinal >= 0 && pkt.styleOrdinal < MovementStyle.values().length) {
style = MovementStyle.values()[pkt.styleOrdinal];
}
if (style != null) {
com.tiedup.remake.client.state.MovementStyleClientState.set(player.getUUID(), style);
} else {
com.tiedup.remake.client.state.MovementStyleClientState.clear(player.getUUID());
}
// Crawl pose management: server sets forced pose for hitbox,
// client must also set it for correct rendering
if (style == MovementStyle.CRAWL) {
player.setForcedPose(Pose.SWIMMING);
player.refreshDimensions();
} else {
// Clear forced pose if it was previously set by crawl
if (player.getForcedPose() == Pose.SWIMMING) {
player.setForcedPose(null);
player.refreshDimensions();
}
}
}
}
}

View File

@@ -0,0 +1,74 @@
package com.tiedup.remake.network.sync;
import com.tiedup.remake.network.base.AbstractPlayerSyncPacket;
import com.tiedup.remake.v2.blocks.PetBedBlock;
import java.util.UUID;
import net.minecraft.core.BlockPos;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
/**
* Sync packet for pet bed state (SIT/SLEEP/CLEAR).
* Direction: Server → Client (S2C)
*/
public class PacketSyncPetBedState extends AbstractPlayerSyncPacket {
/** 0=CLEAR, 1=SIT, 2=SLEEP */
private final byte mode;
private final BlockPos pos;
public PacketSyncPetBedState(UUID playerUUID, byte mode, BlockPos pos) {
super(playerUUID);
this.mode = mode;
this.pos = pos;
}
public void encode(FriendlyByteBuf buf) {
encodeUUID(buf);
buf.writeByte(mode);
buf.writeBlockPos(pos);
}
public static PacketSyncPetBedState decode(FriendlyByteBuf buf) {
UUID uuid = decodeUUID(buf);
byte mode = buf.readByte();
BlockPos pos = buf.readBlockPos();
return new PacketSyncPetBedState(uuid, mode, pos);
}
@Override
@OnlyIn(Dist.CLIENT)
protected void applySync(Player player) {
ClientHandler.handle(this, player);
}
@OnlyIn(Dist.CLIENT)
private static class ClientHandler {
private static void handle(PacketSyncPetBedState pkt, Player player) {
if (pkt.mode == 0) {
// CLEAR — only stop animation if no bondage animation should be playing
// (AnimationTickHandler will re-apply bondage anim on next tick if needed)
com.tiedup.remake.client.animation.BondageAnimationManager.stopAnimation(player);
com.tiedup.remake.client.state.PetBedClientState.clear(player.getUUID());
} else {
// Compute bed facing angle from block state
float facingYRot = 0f;
BlockState state = player.level().getBlockState(pkt.pos);
if (state.hasProperty(PetBedBlock.FACING)) {
facingYRot = state.getValue(PetBedBlock.FACING).toYRot();
}
com.tiedup.remake.client.state.PetBedClientState.set(player.getUUID(), pkt.mode, facingYRot);
if (pkt.mode == 1) {
com.tiedup.remake.client.animation.BondageAnimationManager.playAnimation(player, "pet_bed_sit");
} else if (pkt.mode == 2) {
com.tiedup.remake.client.animation.BondageAnimationManager.playAnimation(player, "pet_bed_sleep");
}
}
}
}
}

View File

@@ -0,0 +1,93 @@
package com.tiedup.remake.network.sync;
import com.tiedup.remake.network.base.AbstractPlayerSyncPacket;
import com.tiedup.remake.state.PlayerBindState;
import java.util.UUID;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.entity.player.Player;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
/**
* Packet to synchronize struggle animation state.
* Lightweight packet (17 bytes): UUID (16 bytes) + boolean (1 byte).
*
* Direction: Server → Client (S2C)
*
* Sent when:
* - Player starts struggle animation (R key pressed)
* - Struggle animation auto-stops after 80 ticks
*/
public class PacketSyncStruggleState extends AbstractPlayerSyncPacket {
private final boolean isStruggling;
/**
* Create a packet to sync struggle animation state.
*
* @param playerUUID The player's UUID
* @param isStruggling True if player is struggling, false otherwise
*/
private PacketSyncStruggleState(UUID playerUUID, boolean isStruggling) {
super(playerUUID);
this.isStruggling = isStruggling;
}
/**
* Create a packet from a player's current struggle state.
*
* @param player The player
* @return The packet, or null if player has no bind state
*/
public static PacketSyncStruggleState fromPlayer(Player player) {
PlayerBindState state = PlayerBindState.getInstance(player);
if (state == null) {
return null;
}
return new PacketSyncStruggleState(
player.getUUID(),
state.isStruggling()
);
}
/**
* Encode the packet to a byte buffer.
*/
public void encode(FriendlyByteBuf buf) {
encodeUUID(buf);
buf.writeBoolean(isStruggling);
}
/**
* Decode the packet from a byte buffer.
*/
public static PacketSyncStruggleState decode(FriendlyByteBuf buf) {
UUID playerUUID = decodeUUID(buf);
boolean isStruggling = buf.readBoolean();
return new PacketSyncStruggleState(playerUUID, isStruggling);
}
/**
* Apply the struggle state sync to a player.
*/
@Override
@OnlyIn(Dist.CLIENT)
protected void applySync(Player player) {
PlayerBindState state = PlayerBindState.getInstance(player);
if (state == null) {
return;
}
// Update struggle state (client-side only - server manages timer)
state.setStrugglingClient(isStruggling);
}
// queueForRetry() not needed - struggle state is non-critical and will be re-sent
// Getters
public boolean isStruggling() {
return isStruggling;
}
}

View File

@@ -0,0 +1,133 @@
package com.tiedup.remake.network.sync;
import java.util.UUID;
/**
* Represents a pending synchronization entry for a player that wasn't loaded yet.
* Stores the data needed to retry the sync later.
*/
public class PendingSyncEntry {
private final SyncType type;
private final UUID targetPlayerUUID;
private final long creationTick;
private int retryCount;
// Data for BIND_STATE sync
private final byte stateFlags;
// Data for ENSLAVEMENT sync
private final UUID masterUUID;
private final UUID transportUUID;
private final boolean isEnslaved;
/**
* Create a pending bind state sync entry.
*/
public static PendingSyncEntry forBindState(
UUID targetUUID,
byte flags,
long currentTick
) {
return new PendingSyncEntry(
SyncType.BIND_STATE,
targetUUID,
currentTick,
flags,
null,
null,
false
);
}
/**
* Create a pending enslavement sync entry.
*/
public static PendingSyncEntry forEnslavement(
UUID targetUUID,
UUID masterUUID,
UUID transportUUID,
boolean isEnslaved,
long currentTick
) {
return new PendingSyncEntry(
SyncType.ENSLAVEMENT,
targetUUID,
currentTick,
(byte) 0,
masterUUID,
transportUUID,
isEnslaved
);
}
private PendingSyncEntry(
SyncType type,
UUID targetUUID,
long creationTick,
byte stateFlags,
UUID masterUUID,
UUID transportUUID,
boolean isEnslaved
) {
this.type = type;
this.targetPlayerUUID = targetUUID;
this.creationTick = creationTick;
this.retryCount = 0;
this.stateFlags = stateFlags;
this.masterUUID = masterUUID;
this.transportUUID = transportUUID;
this.isEnslaved = isEnslaved;
}
public SyncType getType() {
return type;
}
public UUID getTargetPlayerUUID() {
return targetPlayerUUID;
}
public long getCreationTick() {
return creationTick;
}
public int getRetryCount() {
return retryCount;
}
public void incrementRetryCount() {
retryCount++;
}
/**
* Check if this entry has expired (too many retries or too old).
* @param currentTick The current game tick
* @param maxTicks Maximum age in ticks before expiring
* @param maxRetries Maximum retry attempts
* @return true if expired and should be discarded
*/
public boolean isExpired(long currentTick, int maxTicks, int maxRetries) {
return (
(currentTick - creationTick) > maxTicks || retryCount >= maxRetries
);
}
// Getters for sync data
public byte getStateFlags() {
return stateFlags;
}
public UUID getMasterUUID() {
return masterUUID;
}
public UUID getTransportUUID() {
return transportUUID;
}
public boolean isEnslaved() {
return isEnslaved;
}
}

View File

@@ -0,0 +1,262 @@
package com.tiedup.remake.network.sync;
import com.mojang.logging.LogUtils;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.util.ValidationHelper;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import java.util.*;
import java.util.function.Function;
import org.jetbrains.annotations.Nullable;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import org.slf4j.Logger;
/**
* Centralized synchronization manager for TiedUp mod.
*
* This manager handles all synchronization between server and client:
* - Provides unified methods for syncing inventory, state, and enslavement
* - Server-side: Sends packets to tracking clients
* - Client-side: Delegates to ClientSyncHandler for queue management
*
* IMPORTANT: This class is loaded on both server and client.
* All client-specific code (Minecraft.getInstance(), queue processing)
* has been moved to ClientSyncHandler to avoid NoClassDefFoundError
* on dedicated servers.
*
* @see ClientSyncHandler for client-side queue handling
*/
public class SyncManager {
private static final Logger LOGGER = LogUtils.getLogger();
// ==================== Server-side sync methods ====================
/**
* Generic sync method that handles the common pattern:
* 1. Check if server-side
* 2. Create packet from player
* 3. Send to all tracking and self
*
* Phase 2 Refactoring: Eliminates code duplication in sync methods.
*
* @param player The player to sync (must be ServerPlayer)
* @param packetFactory Function to create packet from player
* @param <T> The packet type
*/
private static <T> void sendSync(
@Nullable Player player,
Function<Player, T> packetFactory
) {
ValidationHelper.asServerPlayer(player).ifPresent(serverPlayer -> {
T packet = packetFactory.apply(player);
if (packet != null) {
ModNetwork.sendToAllTrackingAndSelf(packet, serverPlayer);
}
});
}
/**
* Sync a player's bondage inventory to all tracking clients and themselves.
* This is the main method to call when equipment changes.
*
* <p><b>Epic 5A:</b> Delegates to {@link V2EquipmentHelper#sync} which sends
* {@code PacketSyncV2Equipment}.
*
* @param player The player whose inventory changed (must be ServerPlayer)
*/
public static void syncInventory(Player player) {
// V2: Use unified V2 equipment sync
ValidationHelper.asServerPlayer(player).ifPresent(serverPlayer -> {
V2EquipmentHelper.sync(serverPlayer);
});
}
/**
* Sync a player's bind state flags to all tracking clients and themselves.
*
* @param player The player whose state changed (must be ServerPlayer)
*/
public static void syncBindState(Player player) {
sendSync(player, PacketSyncBindState::fromPlayer);
}
/**
* Sync a player's enslavement state to all tracking clients and themselves.
*
* @param player The player whose enslavement state changed (must be ServerPlayer)
*/
public static void syncEnslavement(Player player) {
sendSync(player, PacketSyncEnslavement::fromPlayer);
}
/**
* Sync a player's struggle animation state to all tracking clients and themselves.
* Used when struggle animation starts or stops.
*
* @param player The player whose struggle state changed (must be ServerPlayer)
*/
public static void syncStruggleState(Player player) {
sendSync(player, PacketSyncStruggleState::fromPlayer);
}
/**
* Sync a player's clothes configuration to all tracking clients and themselves.
* Used when clothes are equipped, unequipped, or their settings are changed.
*
* @param player The player whose clothes state changed (must be ServerPlayer)
*/
public static void syncClothesConfig(Player player) {
ValidationHelper.asServerPlayer(player).ifPresent(serverPlayer -> {
ItemStack clothes = V2EquipmentHelper.getInRegion(
player, BodyRegionV2.TORSO
);
PacketSyncClothesConfig packet = new PacketSyncClothesConfig(
player.getUUID(),
clothes
);
ModNetwork.sendToAllTrackingAndSelf(packet, serverPlayer);
});
}
/**
* Sync all data (inventory + state + enslavement + struggle + clothes) for a player.
*
* @param player The player to sync
*/
public static void syncAll(Player player) {
syncInventory(player);
syncBindState(player);
syncEnslavement(player);
syncStruggleState(player);
syncClothesConfig(player);
}
/**
* Sync all online players' data TO a specific player.
* Called when a player joins to ensure they see everyone's state.
*
* <p><b>Epic 5A:</b> Equipment sync uses V2 {@link V2EquipmentHelper#syncTo}.
*
* @param target The player who just joined and needs to receive all data
*/
public static void syncAllPlayersTo(ServerPlayer target) {
if (target.level().isClientSide) return;
var server = target.getServer();
if (server == null) return;
int syncedCount = 0;
for (ServerPlayer otherPlayer : server.getPlayerList().getPlayers()) {
if (otherPlayer == target) continue; // Skip self
// V2: Send other player's V2 equipment to target
V2EquipmentHelper.syncTo(otherPlayer, target);
// Send other player's state to target
PacketSyncBindState statePacket = PacketSyncBindState.fromPlayer(
otherPlayer
);
if (statePacket != null) {
ModNetwork.sendToPlayer(statePacket, target);
}
// Send other player's enslavement state to target
PacketSyncEnslavement enslavementPacket =
PacketSyncEnslavement.fromPlayer(otherPlayer);
if (enslavementPacket != null) {
ModNetwork.sendToPlayer(enslavementPacket, target);
}
// Send other player's struggle state to target
PacketSyncStruggleState strugglePacket =
PacketSyncStruggleState.fromPlayer(otherPlayer);
if (strugglePacket != null) {
ModNetwork.sendToPlayer(strugglePacket, target);
}
// Send other player's clothes config to target
ItemStack clothes = V2EquipmentHelper.getInRegion(
otherPlayer, BodyRegionV2.TORSO
);
if (!clothes.isEmpty()) {
PacketSyncClothesConfig clothesPacket =
new PacketSyncClothesConfig(otherPlayer.getUUID(), clothes);
ModNetwork.sendToPlayer(clothesPacket, target);
}
syncedCount++;
}
if (syncedCount > 0) {
LOGGER.debug(
"[SyncManager] Synced {} players' data to {}",
syncedCount,
target.getName().getString()
);
}
}
// ==================== Client-side queue delegation ====================
// These methods delegate to ClientSyncHandler on client side.
// They are safe to call from packet handlers because they check the side.
/**
* Queue a bind state sync for later processing.
* Called from PacketSyncBindState.handle() when target player is null.
*
* @param targetUUID The UUID of the player to sync
* @param stateFlags The state flags to apply
*/
@OnlyIn(Dist.CLIENT)
public static void queueBindStateSync(UUID targetUUID, byte stateFlags) {
ClientSyncHandler.queueBindStateSync(targetUUID, stateFlags);
}
/**
* Queue an enslavement sync for later processing.
* Called from PacketSyncEnslavement.handle() when target player is null.
*
* @param targetUUID The UUID of the player to sync
* @param masterUUID The master's UUID (null if free)
* @param transportUUID The transport entity UUID (null if no transport)
* @param isEnslaved Whether the player is enslaved
*/
@OnlyIn(Dist.CLIENT)
public static void queueEnslavementSync(
UUID targetUUID,
UUID masterUUID,
UUID transportUUID,
boolean isEnslaved
) {
ClientSyncHandler.queueEnslavementSync(
targetUUID,
masterUUID,
transportUUID,
isEnslaved
);
}
/**
* Clear all pending syncs for a player (e.g., when they disconnect).
*
* @param playerUUID The player's UUID
*/
@OnlyIn(Dist.CLIENT)
public static void clearPendingForPlayer(UUID playerUUID) {
ClientSyncHandler.clearPendingForPlayer(playerUUID);
}
/**
* Clear the entire pending queue (e.g., when disconnecting from server).
*/
@OnlyIn(Dist.CLIENT)
public static void clearAllPending() {
ClientSyncHandler.clearAllPending();
}
}

View File

@@ -0,0 +1,16 @@
package com.tiedup.remake.network.sync;
/**
* Types of synchronization packets.
* Used by SyncManager to categorize and process pending syncs.
*/
public enum SyncType {
/** Bind state flags sync (PacketSyncBindState) */
BIND_STATE,
/** Enslavement state sync (PacketSyncEnslavement) */
ENSLAVEMENT,
/** Full sync (all of the above) */
FULL,
}

View File

@@ -0,0 +1,342 @@
package com.tiedup.remake.network.trader;
import com.tiedup.remake.cells.CampOwnership;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.cells.CellDataV2;
import com.tiedup.remake.cells.CellRegistryV2;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.EntitySlaveTrader;
import com.tiedup.remake.state.IRestrainable;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.util.tasks.ItemTask;
import java.util.List;
import java.util.UUID;
import java.util.function.Supplier;
import net.minecraft.ChatFormatting;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.network.NetworkEvent;
/**
* Packet for buying a captive from a SlaveTrader.
*
* Client -> Server
*/
public class PacketBuyCaptive {
private final int traderEntityId;
private final UUID captiveId;
public PacketBuyCaptive(int traderEntityId, UUID captiveId) {
this.traderEntityId = traderEntityId;
this.captiveId = captiveId;
}
public void encode(FriendlyByteBuf buf) {
buf.writeInt(traderEntityId);
buf.writeUUID(captiveId);
}
public static PacketBuyCaptive decode(FriendlyByteBuf buf) {
int traderEntityId = buf.readInt();
UUID captiveId = buf.readUUID();
return new PacketBuyCaptive(traderEntityId, captiveId);
}
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx
.get()
.enqueueWork(() -> {
ServerPlayer player = ctx.get().getSender();
if (player == null) return;
// CRITICAL FIX: Add rate limiting to prevent DoS via packet spam
if (
!com.tiedup.remake.network.PacketRateLimiter.allowPacket(
player,
"action"
)
) {
return;
}
handleServer(player);
});
ctx.get().setPacketHandled(true);
}
private void handleServer(ServerPlayer buyer) {
ServerLevel level = buyer.serverLevel();
// Find trader entity
Entity traderEntity = level.getEntity(traderEntityId);
if (!(traderEntity instanceof EntitySlaveTrader trader)) {
buyer.sendSystemMessage(
Component.literal("Trader not found.").withStyle(
ChatFormatting.RED
)
);
return;
}
// Validate distance to trader
if (buyer.distanceTo(trader) > 10.0) {
return;
}
// Verify player has token
if (!EntityKidnapper.hasTokenInInventory(buyer)) {
buyer.sendSystemMessage(
Component.literal("You need a token to trade.").withStyle(
ChatFormatting.RED
)
);
return;
}
// Find captive in trader's camp cells
UUID campId = trader.getCampUUID();
if (campId == null) {
buyer.sendSystemMessage(
Component.literal("This trader has no camp.").withStyle(
ChatFormatting.RED
)
);
return;
}
CampOwnership ownership = CampOwnership.get(level);
CampOwnership.CampData campData = ownership.getCamp(campId);
if (campData == null) {
buyer.sendSystemMessage(
Component.literal("Camp not found.").withStyle(
ChatFormatting.RED
)
);
return;
}
CellRegistryV2 cellRegistry = CellRegistryV2.get(level);
List<CellDataV2> cells = cellRegistry.findCellsNear(
campData.getCenter(),
50.0
);
CellDataV2 targetCell = null;
for (CellDataV2 cell : cells) {
if (cell.hasPrisoner(captiveId)) {
targetCell = cell;
break;
}
}
if (targetCell == null) {
buyer.sendSystemMessage(
Component.literal("Captive not found in camp.").withStyle(
ChatFormatting.RED
)
);
return;
}
// Find captive - try Player first, then Entity (Damsel)
net.minecraft.world.entity.LivingEntity captiveEntity = null;
IRestrainable kidnappedState = null;
ServerPlayer captivePlayer = level
.getServer()
.getPlayerList()
.getPlayer(captiveId);
if (captivePlayer != null) {
captiveEntity = captivePlayer;
kidnappedState = KidnappedHelper.getKidnappedState(captivePlayer);
} else {
// Try as Entity (Damsel)
net.minecraft.world.entity.Entity entity = level.getEntity(
captiveId
);
if (
entity instanceof net.minecraft.world.entity.LivingEntity living
) {
captiveEntity = living;
kidnappedState = KidnappedHelper.getKidnappedState(living);
}
}
if (kidnappedState == null || !kidnappedState.isForSell()) {
buyer.sendSystemMessage(
Component.literal("This captive is not for sale.").withStyle(
ChatFormatting.RED
)
);
return;
}
// Get price
ItemTask price = kidnappedState.getSalePrice();
if (price == null) {
buyer.sendSystemMessage(
Component.literal("Price not set for this captive.").withStyle(
ChatFormatting.RED
)
);
return;
}
// Check if buyer has enough items
int required = price.getAmount();
int available = countItemInInventory(buyer, price);
if (available < required) {
buyer.sendSystemMessage(
Component.literal(
"You need " +
required +
"x " +
price.getItem().getDescription().getString() +
" (have " +
available +
")"
).withStyle(ChatFormatting.RED)
);
return;
}
// Take payment
removeItemFromInventory(buyer, price, required);
// Mark captive as sold immediately (prevents re-purchase)
kidnappedState.cancelSale();
// Order maid to fetch and deliver the captive
com.tiedup.remake.entities.EntityMaid maid = trader.getMaid();
if (maid != null && !maid.getMaidState().isBusy()) {
// Maid will fetch from cell, release, and deliver to buyer
maid.startFetchAndDeliver(captiveId, buyer.getUUID());
TiedUpMod.LOGGER.info(
"[PacketBuyCaptive] {} bought captive {} from trader {} for {} {} - maid delivering",
buyer.getName().getString(),
captiveId.toString().substring(0, 8),
trader.getNpcName(),
required,
price.getItem().getDescription().getString()
);
buyer.sendSystemMessage(
Component.literal(
"Purchase complete! The maid will deliver your captive."
).withStyle(ChatFormatting.GREEN)
);
} else {
// No maid available - release directly (fallback)
// Cancel sale state (already called above but defensive)
kidnappedState.cancelSale();
// Transfer collar ownership to buyer
net.minecraft.world.item.ItemStack collar =
kidnappedState.getEquipment(BodyRegionV2.NECK);
if (
!collar.isEmpty() &&
collar.getItem() instanceof
com.tiedup.remake.items.base.ItemCollar collarItem
) {
// Remove all existing owners from collar NBT
for (UUID ownerId : new java.util.ArrayList<>(
collarItem.getOwners(collar)
)) {
collarItem.removeOwner(collar, ownerId);
}
// Add buyer as new owner
collarItem.addOwner(
collar,
buyer.getUUID(),
buyer.getName().getString()
);
collarItem.setLocked(collar, false);
// Re-apply modified collar to persist NBT changes
kidnappedState.equip(BodyRegionV2.NECK, collar);
// Update CollarRegistry for SlaveManagementScreen
com.tiedup.remake.state.CollarRegistry collarRegistry =
com.tiedup.remake.state.CollarRegistry.get(level);
if (collarRegistry != null) {
// Remove all previous owners from registry
collarRegistry.unregisterWearer(captiveId);
// Register buyer as new owner
collarRegistry.registerCollar(captiveId, buyer.getUUID());
}
// Sync collar changes to client
if (captivePlayer != null) {
com.tiedup.remake.network.sync.SyncManager.syncBindState(
captivePlayer
);
}
}
// Release via PrisonerService (state transition + cell cleanup + restraint removal)
com.tiedup.remake.prison.service.PrisonerService.get().release(
level,
captiveId,
6000L // 5 minutes grace period
);
TiedUpMod.LOGGER.info(
"[PacketBuyCaptive] {} bought captive {} from trader {} for {} {} - direct release (no maid)",
buyer.getName().getString(),
captiveId.toString().substring(0, 8),
trader.getNpcName(),
required,
price.getItem().getDescription().getString()
);
buyer.sendSystemMessage(
Component.translatable(
"tiedup.trader.purchase_success"
).withStyle(ChatFormatting.GREEN)
);
}
}
private int countItemInInventory(ServerPlayer player, ItemTask task) {
if (task.getItem() == null) return 0;
int count = 0;
for (int i = 0; i < player.getInventory().getContainerSize(); i++) {
ItemStack stack = player.getInventory().getItem(i);
if (!stack.isEmpty() && stack.getItem() == task.getItem()) {
count += stack.getCount();
}
}
return count;
}
private void removeItemFromInventory(
ServerPlayer player,
ItemTask task,
int amount
) {
if (task.getItem() == null) return;
int remaining = amount;
for (
int i = 0;
i < player.getInventory().getContainerSize() && remaining > 0;
i++
) {
ItemStack stack = player.getInventory().getItem(i);
if (!stack.isEmpty() && stack.getItem() == task.getItem()) {
int toRemove = Math.min(stack.getCount(), remaining);
stack.shrink(toRemove);
remaining -= toRemove;
}
}
}
}

View File

@@ -0,0 +1,136 @@
package com.tiedup.remake.network.trader;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.network.base.AbstractClientPacket;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
/**
* Packet to open the SlaveTrader trading screen on the client.
*
* Server -> Client
*/
public class PacketOpenTraderScreen extends AbstractClientPacket {
private final int traderEntityId;
private final String traderName;
private final List<CaptiveOfferData> offers;
/**
* Network-serializable captive offer data.
*/
public static class CaptiveOfferData {
public final UUID captiveId;
public final String captiveName;
public final String priceDescription;
public final int priceAmount;
public final String priceItemId;
public CaptiveOfferData(
UUID captiveId,
String captiveName,
String priceDescription,
int priceAmount,
String priceItemId
) {
this.captiveId = captiveId;
this.captiveName = captiveName;
this.priceDescription = priceDescription;
this.priceAmount = priceAmount;
this.priceItemId = priceItemId;
}
}
public PacketOpenTraderScreen(
int traderEntityId,
String traderName,
List<CaptiveOfferData> offers
) {
this.traderEntityId = traderEntityId;
this.traderName = traderName;
this.offers = offers != null ? offers : new ArrayList<>();
}
public void encode(FriendlyByteBuf buf) {
buf.writeInt(traderEntityId);
buf.writeUtf(traderName, 64);
buf.writeInt(offers.size());
for (CaptiveOfferData offer : offers) {
buf.writeUUID(offer.captiveId);
buf.writeUtf(offer.captiveName, 64);
buf.writeUtf(offer.priceDescription, 128);
buf.writeInt(offer.priceAmount);
buf.writeUtf(offer.priceItemId, 128);
}
}
public static PacketOpenTraderScreen decode(FriendlyByteBuf buf) {
int traderEntityId = buf.readInt();
String traderName = buf.readUtf(64);
int offerCount = buf.readInt();
List<CaptiveOfferData> offers = new ArrayList<>();
for (int i = 0; i < offerCount; i++) {
UUID captiveId = buf.readUUID();
String captiveName = buf.readUtf(64);
String priceDescription = buf.readUtf(128);
int priceAmount = buf.readInt();
String priceItemId = buf.readUtf(128);
offers.add(
new CaptiveOfferData(
captiveId,
captiveName,
priceDescription,
priceAmount,
priceItemId
)
);
}
return new PacketOpenTraderScreen(traderEntityId, traderName, offers);
}
@Override
@OnlyIn(Dist.CLIENT)
protected void handleClientImpl() {
ClientHandler.handle(this);
}
@OnlyIn(Dist.CLIENT)
private static class ClientHandler {
private static void handle(PacketOpenTraderScreen pkt) {
net.minecraft.client.Minecraft mc = net.minecraft.client.Minecraft.getInstance();
if (mc.player == null) return;
// Convert network data to screen data
List<com.tiedup.remake.client.gui.screens.SlaveTraderScreen.CaptiveOffer> screenOffers = new ArrayList<>();
for (CaptiveOfferData data : pkt.offers) {
screenOffers.add(
new com.tiedup.remake.client.gui.screens.SlaveTraderScreen.CaptiveOffer(
data.captiveId,
data.captiveName,
data.priceDescription,
data.priceAmount,
data.priceItemId
)
);
}
TiedUpMod.LOGGER.info(
"[PacketOpenTraderScreen] Opening trader screen with {} offers",
screenOffers.size()
);
mc.setScreen(
new com.tiedup.remake.client.gui.screens.SlaveTraderScreen(pkt.traderEntityId, pkt.traderName, screenOffers)
);
}
}
}