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:
280
src/main/java/com/tiedup/remake/network/ModNetwork.java
Normal file
280
src/main/java/com/tiedup/remake/network/ModNetwork.java
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
444
src/main/java/com/tiedup/remake/network/NetworkEventHandler.java
Normal file
444
src/main/java/com/tiedup/remake/network/NetworkEventHandler.java
Normal 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;
|
||||
}
|
||||
}
|
||||
244
src/main/java/com/tiedup/remake/network/PacketRateLimiter.java
Normal file
244
src/main/java/com/tiedup/remake/network/PacketRateLimiter.java
Normal 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) {}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
262
src/main/java/com/tiedup/remake/network/sync/SyncManager.java
Normal file
262
src/main/java/com/tiedup/remake/network/sync/SyncManager.java
Normal 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();
|
||||
}
|
||||
}
|
||||
16
src/main/java/com/tiedup/remake/network/sync/SyncType.java
Normal file
16
src/main/java/com/tiedup/remake/network/sync/SyncType.java
Normal 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,
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user