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:
@@ -0,0 +1,133 @@
|
||||
package com.tiedup.remake.events.camp;
|
||||
|
||||
import com.tiedup.remake.blocks.BlockMarker;
|
||||
import com.tiedup.remake.blocks.entity.MarkerBlockEntity;
|
||||
import com.tiedup.remake.cells.CampOwnership;
|
||||
import com.tiedup.remake.cells.CampOwnership.CampData;
|
||||
import com.tiedup.remake.cells.MarkerType;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.block.ChestBlock;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
/**
|
||||
* Event handler for camp chest access control.
|
||||
*
|
||||
* Chests with a LOOT marker above them are locked while the camp trader is alive.
|
||||
* Only camp members (trader, maid, kidnappers) can access them.
|
||||
*/
|
||||
@Mod.EventBusSubscriber(
|
||||
modid = TiedUpMod.MOD_ID,
|
||||
bus = Mod.EventBusSubscriber.Bus.FORGE
|
||||
)
|
||||
public class CampChestHandler {
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onChestOpen(PlayerInteractEvent.RightClickBlock event) {
|
||||
if (event.getLevel().isClientSide()) {
|
||||
return;
|
||||
}
|
||||
|
||||
BlockPos pos = event.getPos();
|
||||
ServerLevel level = (ServerLevel) event.getLevel();
|
||||
|
||||
// Only check if clicking a chest
|
||||
if (!(level.getBlockState(pos).getBlock() instanceof ChestBlock)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if there's a LOOT marker above this chest
|
||||
BlockPos markerPos = pos.above();
|
||||
if (
|
||||
!(level.getBlockState(markerPos).getBlock() instanceof BlockMarker)
|
||||
) {
|
||||
return; // No marker above, allow normal access
|
||||
}
|
||||
|
||||
BlockEntity be = level.getBlockEntity(markerPos);
|
||||
if (!(be instanceof MarkerBlockEntity marker)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if it's a LOOT marker
|
||||
if (marker.getMarkerType() != MarkerType.LOOT) {
|
||||
return; // Not a LOOT marker, allow normal access
|
||||
}
|
||||
|
||||
// Find the nearest camp
|
||||
CampOwnership registry = CampOwnership.get(level);
|
||||
CampData camp = registry.findNearestAliveCamp(pos, 100);
|
||||
|
||||
if (camp == null) {
|
||||
return; // No camp nearby, allow access
|
||||
}
|
||||
|
||||
// Camp dead = chest accessible
|
||||
if (!camp.isAlive()) {
|
||||
return; // Trader dead, chest unlocked
|
||||
}
|
||||
|
||||
// Check if the entity has camp access
|
||||
Entity entity = event.getEntity();
|
||||
if (hasCampAccess(camp, entity)) {
|
||||
return; // Camp member, allow access
|
||||
}
|
||||
|
||||
// Deny access
|
||||
event.setCanceled(true);
|
||||
event.setCancellationResult(InteractionResult.FAIL);
|
||||
|
||||
if (entity instanceof Player player) {
|
||||
player.displayClientMessage(
|
||||
Component.literal(
|
||||
"This chest is locked by the camp!"
|
||||
).withStyle(ChatFormatting.RED),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[CampChestHandler] Denied access to LOOT chest at {} for {}",
|
||||
pos.toShortString(),
|
||||
entity.getName().getString()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an entity has access to the camp's resources.
|
||||
*
|
||||
* @param camp The camp data
|
||||
* @param entity The entity trying to access
|
||||
* @return true if the entity is allowed access
|
||||
*/
|
||||
private static boolean hasCampAccess(CampData camp, Entity entity) {
|
||||
UUID entityId = entity.getUUID();
|
||||
|
||||
// Trader has access
|
||||
if (entityId.equals(camp.getTraderUUID())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Maid has access
|
||||
if (entityId.equals(camp.getMaidUUID())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Kidnapper linked to this camp has access
|
||||
if (camp.hasKidnapper(entityId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
package com.tiedup.remake.events.camp;
|
||||
|
||||
import com.tiedup.remake.cells.CampMaidManager;
|
||||
import com.tiedup.remake.cells.CampOwnership;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.entities.EntityMaid;
|
||||
import com.tiedup.remake.entities.EntitySlaveTrader;
|
||||
import com.tiedup.remake.entities.ModEntities;
|
||||
// Prison system v2
|
||||
import com.tiedup.remake.prison.PrisonerManager;
|
||||
import com.tiedup.remake.prison.service.PrisonerService;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.core.BlockPos;
|
||||
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.minecraftforge.event.TickEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
/**
|
||||
* Handles camp management tasks including maid respawn.
|
||||
*
|
||||
* When a maid dies:
|
||||
* - Camp remains alive (trader still exists)
|
||||
* - Prisoners are paused (no new tasks)
|
||||
* - After 5 minutes (6000 ticks), a new maid spawns
|
||||
* - Prisoners are notified and labor resumes
|
||||
*/
|
||||
@Mod.EventBusSubscriber(
|
||||
modid = TiedUpMod.MOD_ID,
|
||||
bus = Mod.EventBusSubscriber.Bus.FORGE
|
||||
)
|
||||
public class CampManagementHandler {
|
||||
|
||||
// Check every 100 ticks (5 seconds) - optimized for performance
|
||||
private static final int CHECK_INTERVAL_TICKS = 100;
|
||||
|
||||
private static int tickCounter = 0;
|
||||
|
||||
/**
|
||||
* Periodically check for camps needing maid respawn.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onServerTick(TickEvent.ServerTickEvent event) {
|
||||
if (event.phase != TickEvent.Phase.END) return;
|
||||
|
||||
tickCounter++;
|
||||
if (tickCounter < CHECK_INTERVAL_TICKS) return;
|
||||
tickCounter = 0;
|
||||
|
||||
// Process all worlds
|
||||
for (ServerLevel level : event.getServer().getAllLevels()) {
|
||||
processCampManagement(level);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process camp management for a specific level.
|
||||
*/
|
||||
private static void processCampManagement(ServerLevel level) {
|
||||
// IMPORTANT: Always use server-level registry to avoid dimension fragmentation
|
||||
CampOwnership ownership = CampOwnership.get(level.getServer());
|
||||
long currentTime = level.getGameTime();
|
||||
|
||||
// Get camps that need maid respawn
|
||||
List<UUID> campsNeedingMaid = CampMaidManager.getCampsNeedingMaidRespawn(
|
||||
currentTime, level
|
||||
);
|
||||
|
||||
for (UUID campId : campsNeedingMaid) {
|
||||
spawnNewMaidForCamp(level, ownership, campId);
|
||||
}
|
||||
|
||||
// Prison system v2 - tick escape service (handles escape detection)
|
||||
PrisonerService.get().tick(level.getServer(), currentTime);
|
||||
|
||||
// Prison system v2 - tick protection expiry
|
||||
PrisonerManager.get(level).tickProtectionExpiry(currentTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn a new maid for a camp that needs one.
|
||||
*
|
||||
* @param level The server level
|
||||
* @param ownership The camp ownership data
|
||||
* @param campId The camp UUID
|
||||
*/
|
||||
private static void spawnNewMaidForCamp(
|
||||
ServerLevel level,
|
||||
CampOwnership ownership,
|
||||
UUID campId
|
||||
) {
|
||||
CampOwnership.CampData campData = ownership.getCamp(campId);
|
||||
if (campData == null || !campData.isAlive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
BlockPos center = campData.getCenter();
|
||||
if (center == null) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[CampManagementHandler] Cannot spawn maid - camp {} has no center position",
|
||||
campId.toString().substring(0, 8)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
UUID traderUUID = campData.getTraderUUID();
|
||||
if (traderUUID == null) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[CampManagementHandler] Cannot spawn maid - camp {} has no trader",
|
||||
campId.toString().substring(0, 8)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the trader entity
|
||||
Entity traderEntity = level.getEntity(traderUUID);
|
||||
EntitySlaveTrader trader = null;
|
||||
if (traderEntity instanceof EntitySlaveTrader t) {
|
||||
trader = t;
|
||||
} else {
|
||||
// Trader not loaded - search near camp center
|
||||
trader = findTraderNearPosition(level, center, 50);
|
||||
}
|
||||
|
||||
if (trader == null) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[CampManagementHandler] Cannot spawn maid - trader not found for camp {}",
|
||||
campId.toString().substring(0, 8)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new maid
|
||||
EntityMaid maid = ModEntities.MAID.get().create(level);
|
||||
if (maid == null) {
|
||||
TiedUpMod.LOGGER.error(
|
||||
"[CampManagementHandler] Failed to create maid entity for camp {}",
|
||||
campId.toString().substring(0, 8)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find spawn position near trader (slightly offset)
|
||||
BlockPos spawnPos = findSafeSpawnPosition(
|
||||
level,
|
||||
trader.blockPosition()
|
||||
);
|
||||
|
||||
// Position the maid
|
||||
maid.moveTo(
|
||||
spawnPos.getX() + 0.5,
|
||||
spawnPos.getY(),
|
||||
spawnPos.getZ() + 0.5,
|
||||
trader.getYRot() + 180, // Face opposite direction from trader
|
||||
0
|
||||
);
|
||||
|
||||
// Link maid to trader
|
||||
maid.setMasterTraderUUID(trader.getUUID());
|
||||
|
||||
// Add to world
|
||||
level.addFreshEntity(maid);
|
||||
|
||||
// Update camp ownership with new maid
|
||||
CampMaidManager.assignNewMaid(campId, maid.getUUID(), level);
|
||||
|
||||
// Update trader's maid reference
|
||||
trader.setMaidUUID(maid.getUUID());
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[CampManagementHandler] Spawned replacement maid {} for camp {} at {}",
|
||||
maid.getNpcName(),
|
||||
campId.toString().substring(0, 8),
|
||||
spawnPos.toShortString()
|
||||
);
|
||||
|
||||
// Notify prisoners
|
||||
notifyPrisonersOfNewMaid(level, campId, maid.getNpcName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a trader near the given position.
|
||||
*/
|
||||
private static EntitySlaveTrader findTraderNearPosition(
|
||||
ServerLevel level,
|
||||
BlockPos pos,
|
||||
int radius
|
||||
) {
|
||||
List<EntitySlaveTrader> traders = level.getEntitiesOfClass(
|
||||
EntitySlaveTrader.class,
|
||||
new net.minecraft.world.phys.AABB(pos).inflate(radius)
|
||||
);
|
||||
return traders.isEmpty() ? null : traders.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a safe position to spawn the maid.
|
||||
* Tries positions around the target, prioritizing nearby valid spots.
|
||||
*/
|
||||
private static BlockPos findSafeSpawnPosition(
|
||||
ServerLevel level,
|
||||
BlockPos targetPos
|
||||
) {
|
||||
// Try offsets in a spiral pattern
|
||||
int[][] offsets = {
|
||||
{ 1, 0 },
|
||||
{ 0, 1 },
|
||||
{ -1, 0 },
|
||||
{ 0, -1 },
|
||||
{ 1, 1 },
|
||||
{ -1, 1 },
|
||||
{ -1, -1 },
|
||||
{ 1, -1 },
|
||||
{ 2, 0 },
|
||||
{ 0, 2 },
|
||||
{ -2, 0 },
|
||||
{ 0, -2 },
|
||||
};
|
||||
|
||||
for (int[] offset : offsets) {
|
||||
BlockPos testPos = targetPos.offset(offset[0], 0, offset[1]);
|
||||
|
||||
// Check if position is safe (solid ground, air above)
|
||||
if (isValidSpawnPosition(level, testPos)) {
|
||||
return testPos;
|
||||
}
|
||||
|
||||
// Try one block down
|
||||
BlockPos downPos = testPos.below();
|
||||
if (isValidSpawnPosition(level, downPos)) {
|
||||
return downPos;
|
||||
}
|
||||
|
||||
// Try one block up
|
||||
BlockPos upPos = testPos.above();
|
||||
if (isValidSpawnPosition(level, upPos)) {
|
||||
return upPos;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: use target position
|
||||
return targetPos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a position is valid for spawning.
|
||||
*/
|
||||
private static boolean isValidSpawnPosition(
|
||||
ServerLevel level,
|
||||
BlockPos pos
|
||||
) {
|
||||
// Ground must be solid
|
||||
if (!level.getBlockState(pos.below()).isSolid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Position and above must be passable (air or similar)
|
||||
if (!level.getBlockState(pos).isAir()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!level.getBlockState(pos.above()).isAir()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify all prisoners of this camp that a new maid has arrived.
|
||||
*/
|
||||
private static void notifyPrisonersOfNewMaid(
|
||||
ServerLevel level,
|
||||
UUID campId,
|
||||
String maidName
|
||||
) {
|
||||
PrisonerManager manager = PrisonerManager.get(level);
|
||||
for (UUID prisonerId : manager.getPrisonersInCamp(campId)) {
|
||||
ServerPlayer player = level
|
||||
.getServer()
|
||||
.getPlayerList()
|
||||
.getPlayer(prisonerId);
|
||||
if (player != null) {
|
||||
player.sendSystemMessage(
|
||||
Component.literal(
|
||||
"A new maid, " +
|
||||
maidName +
|
||||
", has arrived. Work resumes."
|
||||
).withStyle(ChatFormatting.GOLD)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.tiedup.remake.events.camp;
|
||||
|
||||
import com.tiedup.remake.cells.CampLifecycleManager;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.entities.EntityMaid;
|
||||
import com.tiedup.remake.entities.EntitySlaveTrader;
|
||||
import com.tiedup.remake.items.base.ItemBind;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
|
||||
import net.minecraftforge.eventbus.api.EventPriority;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
/**
|
||||
* Protects camp NPCs (Trader and Maid) from being captured by players.
|
||||
* When a player attempts to restrain a camp NPC:
|
||||
* 1. The attempt is cancelled
|
||||
* 2. The entire camp (trader, maid, all kidnappers) is alerted to attack
|
||||
* 3. The player receives a warning message
|
||||
*/
|
||||
@Mod.EventBusSubscriber(modid = TiedUpMod.MOD_ID)
|
||||
public class CampNpcProtectionHandler {
|
||||
|
||||
@SubscribeEvent(priority = EventPriority.HIGHEST)
|
||||
public static void onPlayerInteractEntity(
|
||||
PlayerInteractEvent.EntityInteract event
|
||||
) {
|
||||
Player player = event.getEntity();
|
||||
Entity target = event.getTarget();
|
||||
|
||||
// Server-side only
|
||||
if (player.level().isClientSide) return;
|
||||
if (!(player.level() instanceof ServerLevel serverLevel)) return;
|
||||
|
||||
// Check if player is holding restraint item
|
||||
ItemStack heldItem = player.getItemInHand(event.getHand());
|
||||
if (!(heldItem.getItem() instanceof ItemBind)) return;
|
||||
|
||||
// Check if target is trader or maid with active camp
|
||||
UUID campId = null;
|
||||
boolean isTrader = false;
|
||||
|
||||
if (target instanceof EntitySlaveTrader trader) {
|
||||
if (trader.isTiedUp()) return; // Already captured
|
||||
campId = trader.getCampUUID();
|
||||
isTrader = true;
|
||||
} else if (target instanceof EntityMaid maid) {
|
||||
if (maid.isTiedUp() || maid.isFreed()) return; // Already captured or freed
|
||||
campId = maid.getCampUUID();
|
||||
isTrader = false;
|
||||
} else {
|
||||
return; // Not a protected NPC
|
||||
}
|
||||
|
||||
if (campId == null) return; // No active camp
|
||||
|
||||
// CANCEL THE ATTEMPT
|
||||
event.setCanceled(true);
|
||||
|
||||
// ALERT THE CAMP
|
||||
CampLifecycleManager.alertCampToDefend(campId, player, serverLevel);
|
||||
|
||||
// Log the attempt
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[CampNpcProtection] {} attempted to restrain {} - camp alerted!",
|
||||
player.getName().getString(),
|
||||
isTrader ? "trader" : "maid"
|
||||
);
|
||||
|
||||
// Send warning message to player
|
||||
player.sendSystemMessage(
|
||||
Component.literal(
|
||||
isTrader
|
||||
? "The camp defends their leader! You are now under attack!"
|
||||
: "The camp defends their servant! You are now under attack!"
|
||||
).withStyle(ChatFormatting.RED, ChatFormatting.BOLD)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.tiedup.remake.events.captivity;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.prison.PrisonerManager;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraftforge.event.TickEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
/**
|
||||
* Handles periodic captivity system updates.
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Expire protection periods for released prisoners
|
||||
* - Periodic validation (optional, for debugging)
|
||||
*/
|
||||
@Mod.EventBusSubscriber(
|
||||
modid = TiedUpMod.MOD_ID,
|
||||
bus = Mod.EventBusSubscriber.Bus.FORGE
|
||||
)
|
||||
public class CaptivityTickHandler {
|
||||
|
||||
private static final int EXPIRY_CHECK_INTERVAL = 100; // Check every 5 seconds (100 ticks)
|
||||
private static int tickCounter = 0;
|
||||
|
||||
/**
|
||||
* Handle server ticks for captivity system updates.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onServerTick(TickEvent.ServerTickEvent event) {
|
||||
// Only run at end of tick to avoid interfering with game logic
|
||||
if (event.phase != TickEvent.Phase.END) {
|
||||
return;
|
||||
}
|
||||
|
||||
tickCounter++;
|
||||
|
||||
// Run expiry check every 5 seconds
|
||||
if (tickCounter >= EXPIRY_CHECK_INTERVAL) {
|
||||
tickCounter = 0;
|
||||
|
||||
// Check all loaded worlds
|
||||
for (ServerLevel level : event.getServer().getAllLevels()) {
|
||||
try {
|
||||
PrisonerManager manager = PrisonerManager.get(level);
|
||||
long currentTime = level.getGameTime();
|
||||
|
||||
// Expire protection for players whose grace period has ended
|
||||
manager.tickProtectionExpiry(currentTime);
|
||||
} catch (Exception e) {
|
||||
TiedUpMod.LOGGER.error(
|
||||
"[CaptivityTickHandler] Error processing captivity expiry in {}: {}",
|
||||
level.dimension().location(),
|
||||
e.getMessage(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
package com.tiedup.remake.events.captivity;
|
||||
|
||||
import com.tiedup.remake.core.SystemMessageManager;
|
||||
import com.tiedup.remake.core.SystemMessageManager.MessageCategory;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.entities.LeashProxyEntity;
|
||||
import com.tiedup.remake.state.IBondageState;
|
||||
import com.tiedup.remake.state.PlayerBindState;
|
||||
import com.tiedup.remake.state.PlayerCaptorManager;
|
||||
import com.tiedup.remake.util.KidnappedHelper;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraftforge.event.entity.EntityMountEvent;
|
||||
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
|
||||
import net.minecraftforge.eventbus.api.EventPriority;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
/**
|
||||
* Handles forced seating for captives.
|
||||
*
|
||||
* Flow:
|
||||
* 1. [ALT] + right-click empty vehicle → free captive + mount on vehicle
|
||||
* 2. [ALT] + right-click vehicle with tied player → dismount + re-capture
|
||||
* 3. Owner enters vehicle with tied player → swap (owner = driver)
|
||||
* 4. Tied player Shift → blocked
|
||||
* 5. Vehicle destroyed → tied player dismounts, stays free
|
||||
*
|
||||
* Note: In the vehicle, the captive has NO leash. The "tied up" status
|
||||
* (isTiedUp) is preserved, which blocks vehicle control and dismount.
|
||||
*/
|
||||
@Mod.EventBusSubscriber(
|
||||
modid = TiedUpMod.MOD_ID,
|
||||
bus = Mod.EventBusSubscriber.Bus.FORGE
|
||||
)
|
||||
public class ForcedSeatingHandler {
|
||||
|
||||
/** Track which players have the Force Seat keybind pressed (server-side) */
|
||||
private static final Map<UUID, Boolean> forceSeatPressed =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
/** Track forced dismounts in progress to avoid blocking our own operations */
|
||||
private static final Map<UUID, Boolean> forcedDismountInProgress =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Update the Force Seat keybind state for a player.
|
||||
* Called from PacketForceSeatModifier.
|
||||
*/
|
||||
public static void setForceSeatPressed(UUID playerId, boolean pressed) {
|
||||
if (pressed) {
|
||||
forceSeatPressed.put(playerId, true);
|
||||
} else {
|
||||
forceSeatPressed.remove(playerId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a player has the Force Seat keybind pressed.
|
||||
*/
|
||||
public static boolean isForceSeatPressed(Player player) {
|
||||
return forceSeatPressed.getOrDefault(player.getUUID(), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up keybind state when player disconnects.
|
||||
*/
|
||||
public static void clearPlayer(UUID playerId) {
|
||||
forceSeatPressed.remove(playerId);
|
||||
forcedDismountInProgress.remove(playerId);
|
||||
}
|
||||
|
||||
// ========== ALT + CLICK ON ENTITY ==========
|
||||
|
||||
/**
|
||||
* Handle ALT + right-click on entity.
|
||||
* - If vehicle has tied player → dismount + re-capture
|
||||
* - If vehicle is empty → free captive + mount
|
||||
*/
|
||||
@SubscribeEvent(priority = EventPriority.HIGHEST)
|
||||
public static void onPlayerInteractEntity(
|
||||
PlayerInteractEvent.EntityInteract event
|
||||
) {
|
||||
if (event.getLevel().isClientSide) return;
|
||||
|
||||
Player owner = event.getEntity();
|
||||
Entity target = event.getTarget();
|
||||
|
||||
// Furniture handles its own seating — skip forced seating logic
|
||||
if (target instanceof com.tiedup.remake.v2.furniture.ISeatProvider) return;
|
||||
|
||||
// Only process if Force Seat keybind is pressed
|
||||
if (!isForceSeatPressed(owner)) return;
|
||||
|
||||
// Skip proxies and players
|
||||
if (target instanceof LeashProxyEntity) return;
|
||||
if (target instanceof Player) return;
|
||||
|
||||
// Get owner's state
|
||||
PlayerBindState ownerState = PlayerBindState.getInstance(owner);
|
||||
if (ownerState == null) return;
|
||||
|
||||
PlayerCaptorManager captorManager = ownerState.getCaptorManager();
|
||||
|
||||
// === CASE 1: Vehicle with tied player → dismount and re-capture ===
|
||||
for (Entity passenger : target.getPassengers()) {
|
||||
if (passenger instanceof Player tiedPlayer && tiedPlayer != owner) {
|
||||
IBondageState tiedState = KidnappedHelper.getKidnappedState(
|
||||
tiedPlayer
|
||||
);
|
||||
|
||||
if (tiedState != null && tiedState.isTiedUp()) {
|
||||
// Cancel the event first
|
||||
event.setCanceled(true);
|
||||
|
||||
// Mark forced dismount in progress
|
||||
forcedDismountInProgress.put(tiedPlayer.getUUID(), true);
|
||||
|
||||
// Schedule dismount for next tick
|
||||
target
|
||||
.getServer()
|
||||
.execute(() -> {
|
||||
try {
|
||||
// Dismount the tied player
|
||||
tiedPlayer.stopRiding();
|
||||
|
||||
// Teleport slightly away from vehicle
|
||||
double offsetX =
|
||||
(target.level().random.nextDouble() - 0.5) *
|
||||
2;
|
||||
double offsetZ =
|
||||
(target.level().random.nextDouble() - 0.5) *
|
||||
2;
|
||||
tiedPlayer.teleportTo(
|
||||
target.getX() + offsetX,
|
||||
target.getY() + 0.5,
|
||||
target.getZ() + offsetZ
|
||||
);
|
||||
|
||||
// Re-capture (creates proxy + leash)
|
||||
if (captorManager != null) {
|
||||
tiedState.getCapturedBy(captorManager);
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[ForcedSeating] {} dismounted {} from vehicle",
|
||||
owner.getName().getString(),
|
||||
tiedPlayer.getName().getString()
|
||||
);
|
||||
} finally {
|
||||
forcedDismountInProgress.remove(
|
||||
tiedPlayer.getUUID()
|
||||
);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Need captives for Case 2
|
||||
if (captorManager == null || !captorManager.hasCaptives()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// === CASE 2: Empty vehicle → free captive and mount ===
|
||||
for (IBondageState captive : captorManager.getCaptives()) {
|
||||
LivingEntity captiveEntity = captive.asLivingEntity();
|
||||
|
||||
// Skip if captive is the target itself
|
||||
if (captiveEntity == target) continue;
|
||||
|
||||
// Skip if captive already riding something
|
||||
if (captiveEntity.isPassenger()) continue;
|
||||
|
||||
// Skip if captive not tied
|
||||
if (!captive.isTiedUp()) continue;
|
||||
|
||||
// Free the captive (detaches leash, removes relationship)
|
||||
captive.free(true);
|
||||
|
||||
// Mount captive on vehicle (still tied up, can't control)
|
||||
boolean success = captiveEntity.startRiding(target, true);
|
||||
|
||||
if (success) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[ForcedSeating] {} mounted {} on vehicle",
|
||||
owner.getName().getString(),
|
||||
captive.getKidnappedName()
|
||||
);
|
||||
}
|
||||
|
||||
event.setCanceled(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== MOUNT/DISMOUNT EVENTS ==========
|
||||
|
||||
/**
|
||||
* Handle mounting/dismounting events.
|
||||
* - Block dismount for tied players (unless vehicle is dead)
|
||||
* - Swap when owner enters vehicle with tied player
|
||||
*/
|
||||
@SubscribeEvent(priority = EventPriority.HIGHEST)
|
||||
public static void onEntityMount(EntityMountEvent event) {
|
||||
if (event.getLevel().isClientSide) return;
|
||||
|
||||
Entity rider = event.getEntityMounting();
|
||||
Entity vehicle = event.getEntityBeingMounted();
|
||||
|
||||
// === DISMOUNTING ===
|
||||
if (event.isDismounting()) {
|
||||
if (rider instanceof Player player) {
|
||||
// Skip if this is a forced dismount (ALT+click or swap)
|
||||
if (
|
||||
forcedDismountInProgress.getOrDefault(
|
||||
player.getUUID(),
|
||||
false
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Furniture manages its own dismount via removePassenger()
|
||||
if (vehicle instanceof com.tiedup.remake.v2.furniture.ISeatProvider) {
|
||||
return;
|
||||
}
|
||||
|
||||
IBondageState state = KidnappedHelper.getKidnappedState(player);
|
||||
if (state != null && state.isTiedUp()) {
|
||||
if (vehicle.isAlive()) {
|
||||
// Block voluntary dismount (Shift key)
|
||||
event.setCanceled(true);
|
||||
SystemMessageManager.sendRestriction(
|
||||
player,
|
||||
MessageCategory.CANT_MOVE
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Vehicle destroyed → captive dismounts, stays free
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// === MOUNTING: Swap if tied player already in vehicle ===
|
||||
if (rider instanceof Player newDriver) {
|
||||
// Skip if swap already in progress for this driver
|
||||
if (
|
||||
forcedDismountInProgress.getOrDefault(
|
||||
newDriver.getUUID(),
|
||||
false
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Entity passenger : vehicle.getPassengers()) {
|
||||
if (
|
||||
passenger instanceof Player tiedPlayer &&
|
||||
tiedPlayer != newDriver
|
||||
) {
|
||||
IBondageState state = KidnappedHelper.getKidnappedState(
|
||||
tiedPlayer
|
||||
);
|
||||
if (state != null && state.isTiedUp()) {
|
||||
// Mark swap in progress for both players
|
||||
forcedDismountInProgress.put(newDriver.getUUID(), true);
|
||||
forcedDismountInProgress.put(
|
||||
tiedPlayer.getUUID(),
|
||||
true
|
||||
);
|
||||
|
||||
// Cancel this mount event - we'll do it manually
|
||||
event.setCanceled(true);
|
||||
|
||||
// Schedule the swap for next tick
|
||||
vehicle
|
||||
.getServer()
|
||||
.execute(() -> {
|
||||
try {
|
||||
// 1. Dismount tied player
|
||||
tiedPlayer.stopRiding();
|
||||
|
||||
// 2. Mount new driver (becomes controller)
|
||||
newDriver.startRiding(vehicle, true);
|
||||
|
||||
// 3. Remount tied player (becomes passenger)
|
||||
tiedPlayer.startRiding(vehicle, true);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[ForcedSeating] Swapped: {} driving, {} passenger",
|
||||
newDriver.getName().getString(),
|
||||
tiedPlayer.getName().getString()
|
||||
);
|
||||
} finally {
|
||||
forcedDismountInProgress.remove(
|
||||
newDriver.getUUID()
|
||||
);
|
||||
forcedDismountInProgress.remove(
|
||||
tiedPlayer.getUUID()
|
||||
);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.tiedup.remake.events.captivity;
|
||||
|
||||
import com.tiedup.remake.state.IPlayerLeashAccess;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraftforge.event.TickEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
/**
|
||||
* Handles leash ticking for players via Forge events.
|
||||
* This replaces the mixin @Inject approach which had refmap issues.
|
||||
*
|
||||
* Security fix: Uses per-player tick count instead of global counter
|
||||
* to prevent erratic physics behavior with multiple players.
|
||||
*/
|
||||
@Mod.EventBusSubscriber(
|
||||
modid = "tiedup",
|
||||
bus = Mod.EventBusSubscriber.Bus.FORGE
|
||||
)
|
||||
public class LeashTickHandler {
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onPlayerTick(TickEvent.PlayerTickEvent event) {
|
||||
// Only on server side, at end of tick
|
||||
if (event.phase != TickEvent.Phase.END) return;
|
||||
if (!(event.player instanceof ServerPlayer serverPlayer)) return;
|
||||
|
||||
// FIX: Removed throttling (was: tickCount % 2 != 0)
|
||||
// Throttling caused rubber-banding and jerky movement because:
|
||||
// 1. Client predicts movement between ticks
|
||||
// 2. Server sends velocity corrections every 2 ticks
|
||||
// 3. This creates visible "snapping" effect
|
||||
// Now physics runs every tick (20 Hz) for smooth movement.
|
||||
|
||||
// Call the leash tick method via the mixin interface
|
||||
if (serverPlayer instanceof IPlayerLeashAccess access) {
|
||||
access.tiedup$tickLeash();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
package com.tiedup.remake.events.captivity;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.entities.LeashProxyEntity;
|
||||
import com.tiedup.remake.items.base.ItemCollar;
|
||||
import com.tiedup.remake.state.IPlayerLeashAccess;
|
||||
import com.tiedup.remake.state.IBondageState;
|
||||
import com.tiedup.remake.state.PlayerBindState;
|
||||
import com.tiedup.remake.util.KidnappedHelper;
|
||||
import com.tiedup.remake.core.SettingsAccessor;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraftforge.event.TickEvent;
|
||||
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
/**
|
||||
* Event handler for master-slave (enslavement) interactions.
|
||||
*
|
||||
* <h2>Key Mechanisms</h2>
|
||||
* <ul>
|
||||
* <li><b>Enslavement:</b> Right-click with Lead on tied player → enslaves them</li>
|
||||
* <li><b>Freeing:</b> Right-click with empty hand on your slave → frees them</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Leash System</h2>
|
||||
* Uses a proxy-based leash system where a {@link LeashProxyEntity} follows the player
|
||||
* and holds the leash. The player does NOT mount anything - traction is applied via push().
|
||||
*
|
||||
* @see PlayerBindState#free()
|
||||
* @see LeashProxyEntity
|
||||
* @see IPlayerLeashAccess
|
||||
*/
|
||||
@Mod.EventBusSubscriber(modid = TiedUpMod.MOD_ID)
|
||||
public class PlayerEnslavementHandler {
|
||||
|
||||
/**
|
||||
* Prevent enslaved players from attacking/breaking their constraints.
|
||||
* Handles Left-Click (Attack) events.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onPlayerAttackEntity(
|
||||
net.minecraftforge.event.entity.player.AttackEntityEvent event
|
||||
) {
|
||||
Player player = event.getEntity();
|
||||
Entity target = event.getTarget();
|
||||
|
||||
// Check if player is leashed (enslaved) via proxy system
|
||||
if (
|
||||
player instanceof IPlayerLeashAccess access &&
|
||||
access.tiedup$isLeashed()
|
||||
) {
|
||||
// Prevent breaking the LeashKnot or the LeashProxy itself
|
||||
if (
|
||||
target instanceof
|
||||
net.minecraft.world.entity.decoration.LeashFenceKnotEntity ||
|
||||
target instanceof LeashProxyEntity
|
||||
) {
|
||||
event.setCanceled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle player interactions with other players for enslavement/freeing.
|
||||
*
|
||||
* Uses IBondageState for condition checks (isEnslavable)
|
||||
* Note: Enslavement system (getEnslavedBy, free) remains PlayerBindState-specific
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onPlayerInteractEntity(
|
||||
PlayerInteractEvent.EntityInteract event
|
||||
) {
|
||||
Player master = event.getEntity();
|
||||
Entity target = event.getTarget();
|
||||
|
||||
// 1. Prevention Logic: Slaves cannot interact with leash knots or proxies
|
||||
if (
|
||||
master instanceof IPlayerLeashAccess access &&
|
||||
access.tiedup$isLeashed()
|
||||
) {
|
||||
if (
|
||||
target instanceof
|
||||
net.minecraft.world.entity.decoration.LeashFenceKnotEntity ||
|
||||
target instanceof LeashProxyEntity
|
||||
) {
|
||||
event.setCanceled(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Only handle player-to-player interactions for enslavement logic
|
||||
if (!(target instanceof Player)) {
|
||||
return;
|
||||
}
|
||||
Player slave = (Player) target;
|
||||
|
||||
// Server-side only
|
||||
if (master.level().isClientSide) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only handle MAIN_HAND to avoid double processing
|
||||
if (event.getHand() != net.minecraft.world.InteractionHand.MAIN_HAND) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if enslavement is enabled
|
||||
if (!SettingsAccessor.isEnslavementEnabled(master.level().getGameRules())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get IBondageState for condition checks
|
||||
IBondageState slaveKidnappedState = KidnappedHelper.getKidnappedState(
|
||||
slave
|
||||
);
|
||||
if (slaveKidnappedState == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get PlayerBindState for enslavement operations (Player-only system)
|
||||
PlayerBindState masterState = PlayerBindState.getInstance(master);
|
||||
PlayerBindState slaveState = PlayerBindState.getInstance(slave);
|
||||
|
||||
if (masterState == null || slaveState == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ItemStack heldItem = master.getItemInHand(event.getHand());
|
||||
|
||||
// ========================================
|
||||
// Scenario 1: Enslavement with Lead
|
||||
// ========================================
|
||||
if (heldItem.is(Items.LEAD)) {
|
||||
// Check if target is enslavable (using IBondageState)
|
||||
if (!slaveKidnappedState.isEnslavable()) {
|
||||
// Exception: collar owner can capture even if not tied
|
||||
boolean canCapture = false;
|
||||
if (slaveKidnappedState.hasCollar()) {
|
||||
ItemStack collar = slaveKidnappedState.getEquipment(BodyRegionV2.NECK);
|
||||
if (collar.getItem() instanceof ItemCollar collarItem) {
|
||||
if (
|
||||
collarItem
|
||||
.getOwners(collar)
|
||||
.contains(master.getUUID())
|
||||
) {
|
||||
canCapture = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!canCapture) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PlayerEnslavementHandler] {} cannot be enslaved - not tied and not collar owner",
|
||||
slave.getName().getString()
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 17: Check if not already captured (Player-specific check)
|
||||
if (slaveState.isCaptive()) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PlayerEnslavementHandler] {} is already captured",
|
||||
slave.getName().getString()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Attempt capture
|
||||
boolean success = slaveState.getCapturedBy(
|
||||
masterState.getCaptorManager()
|
||||
);
|
||||
|
||||
if (success) {
|
||||
heldItem.shrink(1);
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[PlayerEnslavementHandler] {} enslaved {} (lead consumed)",
|
||||
master.getName().getString(),
|
||||
slave.getName().getString()
|
||||
);
|
||||
event.setCanceled(true);
|
||||
} else {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[PlayerEnslavementHandler] Failed to enslave {} by {}",
|
||||
slave.getName().getString(),
|
||||
master.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
// ========================================
|
||||
// Scenario 2: Freeing with Empty Hand
|
||||
// ========================================
|
||||
else if (heldItem.isEmpty()) {
|
||||
// Phase 17: isSlave → isCaptive
|
||||
if (!slaveState.isCaptive()) return;
|
||||
|
||||
// Phase 17: getMaster → getCaptor, getSlaveHolderManager → getCaptorManager
|
||||
if (slaveState.getCaptor() != masterState.getCaptorManager()) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PlayerEnslavementHandler] {} tried to free {} but is not the captor",
|
||||
master.getName().getString(),
|
||||
slave.getName().getString()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
slaveState.free();
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[PlayerEnslavementHandler] {} freed {}",
|
||||
master.getName().getString(),
|
||||
slave.getName().getString()
|
||||
);
|
||||
event.setCanceled(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Periodic check for enslaved players.
|
||||
*
|
||||
* Phase 14.1.5: Remains PlayerBindState-specific (enslavement system is Player-only)
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onPlayerTick(TickEvent.PlayerTickEvent event) {
|
||||
if (event.phase != TickEvent.Phase.END) return;
|
||||
Player player = event.player;
|
||||
if (player.level().isClientSide) return;
|
||||
if (player.tickCount % 20 != 0) return;
|
||||
|
||||
PlayerBindState state = PlayerBindState.getInstance(player);
|
||||
if (state == null) return;
|
||||
|
||||
// Phase 17: isSlave → isCaptive, checkStillSlave → checkStillCaptive
|
||||
if (state.isCaptive()) {
|
||||
state.checkStillCaptive();
|
||||
}
|
||||
// Phase 17: getSlaveHolderManager → getCaptorManager, cleanupInvalidSlaves → cleanupInvalidCaptives
|
||||
state.getCaptorManager().cleanupInvalidCaptives();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.tiedup.remake.events.combat;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.entities.EntityKidnapper;
|
||||
import com.tiedup.remake.prison.PrisonerManager;
|
||||
import com.tiedup.remake.prison.PrisonerRecord;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraftforge.event.entity.player.AttackEntityEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
/**
|
||||
* Event handler for grace period management.
|
||||
*
|
||||
* Revokes grace period when a player attacks a kidnapper.
|
||||
*/
|
||||
@Mod.EventBusSubscriber(
|
||||
modid = TiedUpMod.MOD_ID,
|
||||
bus = Mod.EventBusSubscriber.Bus.FORGE
|
||||
)
|
||||
public class GraceEventHandler {
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onAttackEntity(AttackEntityEvent event) {
|
||||
if (event.getEntity().level().isClientSide()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Entity attacker = event.getEntity();
|
||||
Entity target = event.getTarget();
|
||||
|
||||
// Check if a player is attacking a kidnapper
|
||||
if (
|
||||
attacker instanceof Player player &&
|
||||
target instanceof EntityKidnapper
|
||||
) {
|
||||
ServerLevel level = (ServerLevel) player.level();
|
||||
PrisonerManager manager = PrisonerManager.get(level);
|
||||
PrisonerRecord record = manager.getRecord(player.getUUID());
|
||||
|
||||
// Check if player has grace
|
||||
if (record.isProtected(level.getGameTime())) {
|
||||
// Revoke grace by clearing protection expiry
|
||||
record.setProtectionExpiry(0);
|
||||
|
||||
player.displayClientMessage(
|
||||
Component.literal(
|
||||
"You attacked a kidnapper - protection lost!"
|
||||
).withStyle(ChatFormatting.RED),
|
||||
true
|
||||
);
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[GraceEventHandler] Player {} lost grace period by attacking kidnapper {}",
|
||||
player.getName().getString(),
|
||||
target.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
package com.tiedup.remake.events.combat;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.entities.EntityKidnapper;
|
||||
import com.tiedup.remake.entities.EntityMaid;
|
||||
import com.tiedup.remake.entities.EntitySlaveTrader;
|
||||
import com.tiedup.remake.items.base.ItemBind;
|
||||
import com.tiedup.remake.prison.LaborRecord;
|
||||
import com.tiedup.remake.prison.PrisonerManager;
|
||||
import com.tiedup.remake.prison.PrisonerRecord;
|
||||
import com.tiedup.remake.prison.PrisonerState;
|
||||
import com.tiedup.remake.state.IRestrainable;
|
||||
import com.tiedup.remake.util.KidnappedHelper;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraftforge.event.entity.player.AttackEntityEvent;
|
||||
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
/**
|
||||
* Punishes prisoners who attack or attempt to restrain kidnappers, traders, or maids while working.
|
||||
*
|
||||
* Triggers:
|
||||
* - Attacking a protected entity (kidnappers, traders, maids)
|
||||
* - Using rope/restraint items on a protected entity
|
||||
*
|
||||
* Punishments (applied immediately):
|
||||
* - Shock (damage + message)
|
||||
* - Debt increase (+25 emeralds)
|
||||
* - Task marked as failed (no payment)
|
||||
* - Equipment reclaimed
|
||||
* - Immediate teleportation back to cell
|
||||
* - State changed to IMPRISONED (resting)
|
||||
* - Attack/interaction cancelled (no damage to target)
|
||||
*
|
||||
* This prevents prisoners from attacking or restraining their captors during labor tasks.
|
||||
*/
|
||||
@Mod.EventBusSubscriber(modid = TiedUpMod.MOD_ID)
|
||||
public class LaborAttackPunishmentHandler {
|
||||
|
||||
/** Debt increase per attack */
|
||||
private static final int DEBT_INCREASE_PER_ATTACK = 25;
|
||||
|
||||
/** Base shock damage */
|
||||
private static final float BASE_SHOCK_DAMAGE = 3.0f;
|
||||
|
||||
/**
|
||||
* Detect when a prisoner attacks a kidnapper/trader/maid during labor.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onPlayerAttackEntity(AttackEntityEvent event) {
|
||||
Player player = event.getEntity();
|
||||
Entity target = event.getTarget();
|
||||
|
||||
// Server-side only
|
||||
if (player.level().isClientSide) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only handle ServerPlayer
|
||||
if (!(player instanceof ServerPlayer serverPlayer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only handle ServerLevel
|
||||
if (!(player.level() instanceof ServerLevel serverLevel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if target is a kidnapper, trader, or maid
|
||||
boolean isProtectedEntity =
|
||||
target instanceof EntityKidnapper ||
|
||||
target instanceof EntitySlaveTrader ||
|
||||
target instanceof EntityMaid;
|
||||
|
||||
if (!isProtectedEntity) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if player is a prisoner in WORKING state
|
||||
PrisonerManager manager = PrisonerManager.get(serverLevel);
|
||||
PrisonerRecord record = manager.getRecord(serverPlayer.getUUID());
|
||||
|
||||
if (record.getState() != PrisonerState.WORKING) {
|
||||
return; // Not working, no punishment
|
||||
}
|
||||
|
||||
// PUNISH THE PRISONER
|
||||
punishPrisonerForAttack(serverPlayer, serverLevel, record, target);
|
||||
|
||||
// Cancel the attack
|
||||
event.setCanceled(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect when a prisoner tries to use rope/restraint items on kidnapper/trader/maid during labor.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onPlayerInteractEntity(
|
||||
PlayerInteractEvent.EntityInteract event
|
||||
) {
|
||||
Player player = event.getEntity();
|
||||
Entity target = event.getTarget();
|
||||
InteractionHand hand = event.getHand();
|
||||
|
||||
// Server-side only
|
||||
if (player.level().isClientSide) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only handle ServerPlayer
|
||||
if (!(player instanceof ServerPlayer serverPlayer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only handle ServerLevel
|
||||
if (!(player.level() instanceof ServerLevel serverLevel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if target is a kidnapper, trader, or maid
|
||||
boolean isProtectedEntity =
|
||||
target instanceof EntityKidnapper ||
|
||||
target instanceof EntitySlaveTrader ||
|
||||
target instanceof EntityMaid;
|
||||
|
||||
if (!isProtectedEntity) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if player is a prisoner in WORKING state
|
||||
PrisonerManager manager = PrisonerManager.get(serverLevel);
|
||||
PrisonerRecord record = manager.getRecord(serverPlayer.getUUID());
|
||||
|
||||
if (record.getState() != PrisonerState.WORKING) {
|
||||
return; // Not working, no punishment
|
||||
}
|
||||
|
||||
// Check if player is holding a restraint item (rope, chain, etc.)
|
||||
ItemStack heldItem = serverPlayer.getItemInHand(hand);
|
||||
if (!(heldItem.getItem() instanceof ItemBind)) {
|
||||
return; // Not a restraint item
|
||||
}
|
||||
|
||||
// PUNISH THE PRISONER
|
||||
punishPrisonerForAttack(serverPlayer, serverLevel, record, target);
|
||||
|
||||
// Cancel the interaction
|
||||
event.setCanceled(true);
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[LaborAttackPunishmentHandler] Prisoner {} attempted to restrain {} during labor - punished",
|
||||
serverPlayer.getName().getString(),
|
||||
target instanceof EntityKidnapper kidnapper
|
||||
? kidnapper.getNpcName()
|
||||
: target instanceof EntitySlaveTrader trader
|
||||
? trader.getNpcName()
|
||||
: target instanceof EntityMaid maid
|
||||
? maid.getNpcName()
|
||||
: "captor"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply punishment to prisoner for attacking protected entity.
|
||||
*/
|
||||
private static void punishPrisonerForAttack(
|
||||
ServerPlayer prisoner,
|
||||
ServerLevel level,
|
||||
PrisonerRecord record,
|
||||
Entity target
|
||||
) {
|
||||
IRestrainable kidnappedState = KidnappedHelper.getKidnappedState(
|
||||
prisoner
|
||||
);
|
||||
if (kidnappedState == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Shock the prisoner
|
||||
String targetName = "your captor";
|
||||
if (target instanceof EntityKidnapper kidnapper) {
|
||||
targetName = kidnapper.getNpcName();
|
||||
} else if (target instanceof EntitySlaveTrader trader) {
|
||||
targetName = trader.getNpcName();
|
||||
} else if (target instanceof EntityMaid maid) {
|
||||
targetName = maid.getNpcName();
|
||||
}
|
||||
|
||||
String shockMessage = String.format(
|
||||
"You DARE attack %s? You'll pay for this insolence!",
|
||||
targetName
|
||||
);
|
||||
|
||||
kidnappedState.shockKidnapped(shockMessage, BASE_SHOCK_DAMAGE);
|
||||
|
||||
// 2. Increase debt
|
||||
PrisonerManager manager = PrisonerManager.get(level);
|
||||
com.tiedup.remake.prison.RansomRecord ransomRecord =
|
||||
manager.getRansomRecord(prisoner.getUUID());
|
||||
if (ransomRecord != null) {
|
||||
manager.increaseDebt(prisoner.getUUID(), DEBT_INCREASE_PER_ATTACK);
|
||||
|
||||
prisoner.sendSystemMessage(
|
||||
Component.literal(
|
||||
String.format(
|
||||
"Your debt has increased by %d emeralds for attacking %s!",
|
||||
DEBT_INCREASE_PER_ATTACK,
|
||||
targetName
|
||||
)
|
||||
).withStyle(ChatFormatting.RED, ChatFormatting.BOLD)
|
||||
);
|
||||
}
|
||||
|
||||
// 3. CRITICAL FIX: Collect task items, reclaim equipment, and confiscate everything
|
||||
LaborRecord laborRecord = manager.getLaborRecord(prisoner.getUUID());
|
||||
com.tiedup.remake.labor.LaborTask task = laborRecord.getTask();
|
||||
|
||||
// Find cell via PrisonerRecord.getCellId() — CellRegistryV2 won't have the prisoner
|
||||
// during WORKING state because extractFromCell() removes them from the registry.
|
||||
com.tiedup.remake.cells.CellRegistryV2 cellRegistry =
|
||||
com.tiedup.remake.cells.CellRegistryV2.get(level);
|
||||
java.util.UUID cellId = record.getCellId();
|
||||
com.tiedup.remake.cells.CellDataV2 cell =
|
||||
cellId != null ? cellRegistry.getCell(cellId) : null;
|
||||
|
||||
if (task != null) {
|
||||
// Collect task items (prevent player keeping valuable items like diamonds)
|
||||
java.util.List<net.minecraft.world.item.ItemStack> collectedItems =
|
||||
task.collectItems(prisoner);
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[LaborAttackPunishmentHandler] Collected {} task items from {} before punishment",
|
||||
collectedItems.size(),
|
||||
prisoner.getName().getString()
|
||||
);
|
||||
|
||||
// Reclaim labor tools
|
||||
java.util.List<net.minecraft.world.item.ItemStack> reclaimedTools =
|
||||
task.reclaimEquipment(prisoner);
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[LaborAttackPunishmentHandler] Reclaimed {} tools from {} before punishment",
|
||||
reclaimedTools.size(),
|
||||
prisoner.getName().getString()
|
||||
);
|
||||
|
||||
if (cell != null) {
|
||||
java.util.List<net.minecraft.world.item.ItemStack> allItems =
|
||||
new java.util.ArrayList<>();
|
||||
allItems.addAll(collectedItems);
|
||||
allItems.addAll(reclaimedTools);
|
||||
|
||||
if (!allItems.isEmpty()) {
|
||||
depositPunishmentItems(level, cell, allItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Confiscate remaining contraband (anything picked up during labor)
|
||||
if (cell != null && !prisoner.getInventory().isEmpty()) {
|
||||
com.tiedup.remake.cells.ConfiscatedInventoryRegistry registry =
|
||||
com.tiedup.remake.cells.ConfiscatedInventoryRegistry.get(level);
|
||||
|
||||
java.util.List<net.minecraft.core.BlockPos> campChests =
|
||||
findCampLootChests(level, cell);
|
||||
if (!campChests.isEmpty()) {
|
||||
registry.dumpInventoryToChest(prisoner, campChests.get(0));
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[LaborAttackPunishmentHandler] Confiscated remaining contraband from {}",
|
||||
prisoner.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
laborRecord.setTaskFailed(true);
|
||||
|
||||
// 4. Warning message
|
||||
prisoner.sendSystemMessage(
|
||||
Component.literal(
|
||||
"Your task has been marked as failed. You will not be paid for your work."
|
||||
).withStyle(ChatFormatting.DARK_RED)
|
||||
);
|
||||
|
||||
// 5. Return prisoner to cell immediately (punishment)
|
||||
|
||||
if (cell != null) {
|
||||
// Teleport to cell
|
||||
net.minecraft.core.BlockPos spawnPoint = cell
|
||||
.getSpawnPoint()
|
||||
.above();
|
||||
com.tiedup.remake.util.teleport.Position teleportTarget =
|
||||
new com.tiedup.remake.util.teleport.Position(
|
||||
spawnPoint,
|
||||
level.dimension()
|
||||
);
|
||||
com.tiedup.remake.util.teleport.TeleportHelper.teleportEntity(
|
||||
prisoner,
|
||||
teleportTarget
|
||||
);
|
||||
|
||||
// Reassign prisoner to cell registry (was removed during extract)
|
||||
cellRegistry.assignPrisoner(cell.getId(), prisoner.getUUID());
|
||||
|
||||
// Transition to IMPRISONED state and start rest
|
||||
long currentTime = level.getGameTime();
|
||||
record.setState(PrisonerState.IMPRISONED, currentTime);
|
||||
laborRecord.setTask(null); // CRITICAL: Clear task to prevent reuse after punishment
|
||||
laborRecord.startRest(currentTime);
|
||||
laborRecord.setEscortMaidId(null);
|
||||
|
||||
prisoner.sendSystemMessage(
|
||||
Component.literal(
|
||||
"You have been returned to your cell for your insolence!"
|
||||
).withStyle(ChatFormatting.RED, ChatFormatting.BOLD)
|
||||
);
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[LaborAttackPunishmentHandler] Prisoner {} attacked {} during labor - punished (shocked, +{} debt, task failed, items confiscated, returned to cell)",
|
||||
prisoner.getName().getString(),
|
||||
targetName,
|
||||
DEBT_INCREASE_PER_ATTACK
|
||||
);
|
||||
} else {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[LaborAttackPunishmentHandler] Could not return {} to cell - no cell found! (cellId={}, record={})",
|
||||
prisoner.getName().getString(),
|
||||
cellId != null ? cellId.toString().substring(0, 8) : "null",
|
||||
record
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deposit punishment items (task items + tools) in camp chests.
|
||||
* Items are confiscated as punishment and stored in camp.
|
||||
*/
|
||||
private static void depositPunishmentItems(
|
||||
ServerLevel level,
|
||||
com.tiedup.remake.cells.CellDataV2 cell,
|
||||
java.util.List<net.minecraft.world.item.ItemStack> items
|
||||
) {
|
||||
if (items.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
java.util.UUID campId = cell.getOwnerId();
|
||||
if (campId == null) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[LaborAttackPunishmentHandler] Cell has no camp owner - cannot deposit punishment items"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
java.util.List<net.minecraft.core.BlockPos> campChests =
|
||||
findCampLootChests(level, cell);
|
||||
|
||||
if (campChests.isEmpty()) {
|
||||
// Fallback: drop at camp center
|
||||
com.tiedup.remake.cells.CampOwnership campOwnership =
|
||||
com.tiedup.remake.cells.CampOwnership.get(level);
|
||||
com.tiedup.remake.cells.CampOwnership.CampData camp =
|
||||
campOwnership.getCamp(campId);
|
||||
if (camp != null && camp.getCenter() != null) {
|
||||
net.minecraft.core.BlockPos center = camp.getCenter();
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[LaborAttackPunishmentHandler] No LOOT chests found - dropping punishment items at camp center {}",
|
||||
center.toShortString()
|
||||
);
|
||||
|
||||
for (net.minecraft.world.item.ItemStack stack : items) {
|
||||
if (stack.isEmpty()) continue;
|
||||
net.minecraft.world.entity.item.ItemEntity itemEntity =
|
||||
new net.minecraft.world.entity.item.ItemEntity(
|
||||
level,
|
||||
center.getX() + 0.5,
|
||||
center.getY() + 1.0,
|
||||
center.getZ() + 0.5,
|
||||
stack
|
||||
);
|
||||
level.addFreshEntity(itemEntity);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Use unified inventory system for smart chest rotation
|
||||
com.tiedup.remake.cells.ConfiscatedInventoryRegistry registry =
|
||||
com.tiedup.remake.cells.ConfiscatedInventoryRegistry.get(level);
|
||||
|
||||
int deposited = registry.depositItemsInChests(items, campChests, level);
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[LaborAttackPunishmentHandler] Deposited {} punishment items in {} camp LOOT chests",
|
||||
deposited,
|
||||
campChests.size()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all LOOT chests for a camp.
|
||||
* Delegates to ItemService which handles fast-path + lazy discovery.
|
||||
*/
|
||||
private static java.util.List<
|
||||
net.minecraft.core.BlockPos
|
||||
> findCampLootChests(
|
||||
ServerLevel level,
|
||||
com.tiedup.remake.cells.CellDataV2 cell
|
||||
) {
|
||||
java.util.UUID campId = cell.getOwnerId();
|
||||
if (campId == null) return java.util.List.of();
|
||||
|
||||
return com.tiedup.remake.prison.service.ItemService.get().findAllCampChests(
|
||||
level,
|
||||
campId
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.tiedup.remake.events.combat;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.state.PlayerBindState;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.monster.Monster;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraftforge.event.entity.living.LivingChangeTargetEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
/**
|
||||
* Event handler to prevent monsters from targeting tied-up players.
|
||||
*
|
||||
* When a player is bound (has bind item), monsters will ignore them.
|
||||
* This makes sense because:
|
||||
* 1. Tied-up players are helpless and can't fight back
|
||||
* 2. Camp kidnappers protect their captives from monsters
|
||||
* 3. It reduces frustration of being killed while unable to defend
|
||||
*
|
||||
* The kidnapper's HuntMonstersGoal will handle killing nearby monsters instead.
|
||||
*/
|
||||
@Mod.EventBusSubscriber(
|
||||
modid = TiedUpMod.MOD_ID,
|
||||
bus = Mod.EventBusSubscriber.Bus.FORGE
|
||||
)
|
||||
public class MonsterTargetingHandler {
|
||||
|
||||
/**
|
||||
* Called when any living entity changes its target.
|
||||
* Cancel the event if a monster tries to target a tied-up player.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onLivingChangeTarget(LivingChangeTargetEvent event) {
|
||||
// Only care about monsters
|
||||
if (!(event.getEntity() instanceof Monster monster)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if new target is a player
|
||||
LivingEntity newTarget = event.getNewTarget();
|
||||
if (!(newTarget instanceof Player player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Server-side only for ServerPlayer state check
|
||||
if (!(player instanceof ServerPlayer serverPlayer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if player is tied up
|
||||
PlayerBindState state = PlayerBindState.getInstance(serverPlayer);
|
||||
if (state != null && state.isTiedUp()) {
|
||||
// Cancel targeting - monster ignores tied-up player
|
||||
event.setCanceled(true);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MonsterTargetingHandler] {} ignored tied-up player {}",
|
||||
monster.getName().getString(),
|
||||
player.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package com.tiedup.remake.events.interaction;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.dialogue.conversation.ConversationManager;
|
||||
import com.tiedup.remake.network.ModNetwork;
|
||||
import com.tiedup.remake.network.conversation.PacketEndConversationS2C;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraftforge.event.TickEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
/**
|
||||
* Server tick handler for dialogue and conversation system maintenance.
|
||||
*
|
||||
* Phase 14: Conversation System
|
||||
*
|
||||
* Handles:
|
||||
* - Cleanup of stale/abandoned conversations
|
||||
* - Distance checks for active conversations
|
||||
* - Entity validity checks (NPC died, etc.)
|
||||
*/
|
||||
@Mod.EventBusSubscriber(
|
||||
modid = TiedUpMod.MOD_ID,
|
||||
bus = Mod.EventBusSubscriber.Bus.FORGE
|
||||
)
|
||||
public class DialogueTickHandler {
|
||||
|
||||
/**
|
||||
* How often to run cleanup (in ticks).
|
||||
* 1200 ticks = 1 minute
|
||||
*/
|
||||
private static final int CLEANUP_INTERVAL = 1200;
|
||||
|
||||
/**
|
||||
* How often to check active conversations (in ticks).
|
||||
* 20 ticks = 1 second
|
||||
*/
|
||||
private static final int CHECK_INTERVAL = 20;
|
||||
|
||||
/**
|
||||
* Maximum distance for conversation before auto-ending.
|
||||
*/
|
||||
private static final double MAX_CONVERSATION_DISTANCE = 8.0;
|
||||
|
||||
/**
|
||||
* Handle server tick events for dialogue system maintenance.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onServerTick(TickEvent.ServerTickEvent event) {
|
||||
if (event.phase != TickEvent.Phase.END) return;
|
||||
|
||||
long currentTick = event.getServer().getTickCount();
|
||||
|
||||
// Check active conversations every second
|
||||
if (currentTick % CHECK_INTERVAL == 0) {
|
||||
checkActiveConversations(event.getServer());
|
||||
}
|
||||
|
||||
// Cleanup stale conversations every minute
|
||||
if (currentTick % CLEANUP_INTERVAL == 0) {
|
||||
ConversationManager.cleanupStaleConversations();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check all active conversations for validity.
|
||||
* Ends conversations where player/NPC is too far, dead, or disconnected.
|
||||
*/
|
||||
private static void checkActiveConversations(
|
||||
net.minecraft.server.MinecraftServer server
|
||||
) {
|
||||
List<ServerPlayer> playersToEnd = new ArrayList<>();
|
||||
|
||||
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
|
||||
ConversationManager.ConversationState state =
|
||||
ConversationManager.getConversationState(player);
|
||||
if (state == null) continue;
|
||||
|
||||
UUID speakerId = state.getSpeakerEntityId();
|
||||
boolean shouldEnd = false;
|
||||
int entityId = -1;
|
||||
|
||||
// O(1) lookup: get the specific ServerLevel, then getEntity by UUID
|
||||
ServerLevel speakerLevel = server.getLevel(
|
||||
state.getSpeakerDimension()
|
||||
);
|
||||
Entity speakerEntity =
|
||||
speakerLevel != null ? speakerLevel.getEntity(speakerId) : null;
|
||||
|
||||
if (speakerEntity == null) {
|
||||
// Entity no longer exists
|
||||
shouldEnd = true;
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[DialogueTickHandler] Ending conversation: NPC no longer exists"
|
||||
);
|
||||
} else if (!speakerEntity.isAlive()) {
|
||||
// Entity died
|
||||
shouldEnd = true;
|
||||
entityId = speakerEntity.getId();
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[DialogueTickHandler] Ending conversation: NPC died"
|
||||
);
|
||||
} else if (
|
||||
speakerEntity.distanceTo(player) > MAX_CONVERSATION_DISTANCE
|
||||
) {
|
||||
// Player moved too far
|
||||
shouldEnd = true;
|
||||
entityId = speakerEntity.getId();
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[DialogueTickHandler] Ending conversation: Player too far ({})",
|
||||
speakerEntity.distanceTo(player)
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldEnd) {
|
||||
playersToEnd.add(player);
|
||||
// Send packet to close client GUI
|
||||
if (entityId != -1) {
|
||||
ModNetwork.sendToPlayer(
|
||||
new PacketEndConversationS2C(entityId),
|
||||
player
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// End conversations outside the iteration
|
||||
for (ServerPlayer player : playersToEnd) {
|
||||
ConversationManager.endConversation(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package com.tiedup.remake.events.interaction;
|
||||
|
||||
import com.tiedup.remake.core.SystemMessageManager;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory;
|
||||
import com.tiedup.remake.entities.EntityKidnapper;
|
||||
import com.tiedup.remake.state.IRestrainable;
|
||||
import com.tiedup.remake.state.ICaptor;
|
||||
import com.tiedup.remake.state.PlayerBindState;
|
||||
import com.tiedup.remake.util.tasks.ItemTask;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
/**
|
||||
* Event handler for player interactions with EntityKidnapper.
|
||||
*
|
||||
* Phase 14.3.5: Sale system interactions
|
||||
*
|
||||
* Handles:
|
||||
* - Right-click on kidnapper selling slave to buy
|
||||
* - Payment validation
|
||||
* - Slave transfer to buyer
|
||||
*/
|
||||
@Mod.EventBusSubscriber(modid = TiedUpMod.MOD_ID)
|
||||
public class KidnapperInteractionEventHandler {
|
||||
|
||||
/**
|
||||
* Handle right-click interaction with EntityKidnapper.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onEntityInteract(
|
||||
PlayerInteractEvent.EntityInteract event
|
||||
) {
|
||||
// Server-side only
|
||||
if (event.getLevel().isClientSide()) return;
|
||||
|
||||
// Must be interacting with a kidnapper
|
||||
if (!(event.getTarget() instanceof EntityKidnapper kidnapper)) return;
|
||||
|
||||
// Must use main hand
|
||||
if (event.getHand() != InteractionHand.MAIN_HAND) return;
|
||||
|
||||
Player player = event.getEntity();
|
||||
|
||||
// Skip enslaved kidnappers - let item/entity interactions handle them
|
||||
if (kidnapper.isTiedUp()) return;
|
||||
|
||||
// Check if kidnapper is selling a captive
|
||||
if (kidnapper.isSellingCaptive()) {
|
||||
handleSalePurchase(player, kidnapper, event);
|
||||
return;
|
||||
}
|
||||
|
||||
// Other interactions can be added here later
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a player attempting to purchase a captive from a kidnapper.
|
||||
*/
|
||||
private static void handleSalePurchase(
|
||||
Player player,
|
||||
EntityKidnapper kidnapper,
|
||||
PlayerInteractEvent.EntityInteract event
|
||||
) {
|
||||
IRestrainable captive = kidnapper.getCaptive();
|
||||
if (captive == null) return;
|
||||
|
||||
ItemTask price = captive.getSalePrice();
|
||||
if (price == null) return;
|
||||
|
||||
ItemStack heldItem = player.getMainHandItem();
|
||||
|
||||
// Check if player is holding the required payment
|
||||
if (!price.matchesItem(heldItem)) {
|
||||
// Wrong item - show what's needed
|
||||
SystemMessageManager.sendToPlayer(
|
||||
player,
|
||||
SystemMessageManager.MessageCategory.ERROR,
|
||||
Component.translatable(
|
||||
"tiedup.sale.wrong_item",
|
||||
price.toDisplayString()
|
||||
).getString()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!price.isCompletedBy(heldItem)) {
|
||||
// Not enough items
|
||||
int have = heldItem.getCount();
|
||||
int need = price.getAmount();
|
||||
SystemMessageManager.sendToPlayer(
|
||||
player,
|
||||
SystemMessageManager.MessageCategory.ERROR,
|
||||
Component.translatable(
|
||||
"tiedup.sale.not_enough",
|
||||
have,
|
||||
need,
|
||||
price.getDisplayName()
|
||||
).getString()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Player has the required payment!
|
||||
// Consume payment
|
||||
price.consumeFrom(heldItem);
|
||||
|
||||
// Get player's kidnapper interface for slave transfer
|
||||
PlayerBindState bindState = PlayerBindState.getInstance(player);
|
||||
if (bindState == null) {
|
||||
TiedUpMod.LOGGER.error(
|
||||
"[KidnapperInteractionEventHandler] Player {} has no bind state!",
|
||||
player.getName().getString()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Phase 17: getSlaveHolderManager → getCaptorManager
|
||||
ICaptor buyerKidnapper = bindState.getCaptorManager();
|
||||
if (buyerKidnapper == null) {
|
||||
TiedUpMod.LOGGER.error(
|
||||
"[KidnapperInteractionEventHandler] Player {} has no kidnapper interface!",
|
||||
player.getName().getString()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Complete the sale
|
||||
if (kidnapper.completeSale(buyerKidnapper)) {
|
||||
// Success!
|
||||
SystemMessageManager.sendChatToPlayer(
|
||||
player,
|
||||
Component.translatable(
|
||||
"tiedup.sale.success",
|
||||
captive.getKidnappedName()
|
||||
).getString(),
|
||||
ChatFormatting.GREEN
|
||||
);
|
||||
|
||||
// Kidnapper talks about completed sale
|
||||
kidnapper.talkTo(player, DialogueCategory.SALE_COMPLETE);
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[KidnapperInteractionEventHandler] {} purchased {} from {} for {}",
|
||||
player.getName().getString(),
|
||||
captive.getKidnappedName(),
|
||||
kidnapper.getNpcName(),
|
||||
price.toDisplayString()
|
||||
);
|
||||
|
||||
event.setCanceled(true);
|
||||
} else {
|
||||
// Sale failed for some reason
|
||||
SystemMessageManager.sendToPlayer(
|
||||
player,
|
||||
SystemMessageManager.MessageCategory.ERROR,
|
||||
Component.translatable("tiedup.sale.failed").getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.tiedup.remake.events.interaction;
|
||||
|
||||
import com.tiedup.remake.core.SystemMessageManager;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.entities.EntityKidnapperMerchant;
|
||||
import com.tiedup.remake.network.ModNetwork;
|
||||
import com.tiedup.remake.network.merchant.PacketOpenMerchantScreen;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
/**
|
||||
* Event handler for player interactions with EntityKidnapperMerchant.
|
||||
*
|
||||
* Handles:
|
||||
* - Right-click on merchant to open trading screen
|
||||
* - Validation checks (merchant mode, no captives)
|
||||
*/
|
||||
@Mod.EventBusSubscriber(modid = TiedUpMod.MOD_ID)
|
||||
public class MerchantInteractionEventHandler {
|
||||
|
||||
/**
|
||||
* Handle right-click interaction with EntityKidnapperMerchant.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onEntityInteract(
|
||||
PlayerInteractEvent.EntityInteract event
|
||||
) {
|
||||
// Server-side only
|
||||
if (event.getLevel().isClientSide()) return;
|
||||
|
||||
// Must be interacting with a merchant
|
||||
if (
|
||||
!(event.getTarget() instanceof EntityKidnapperMerchant merchant)
|
||||
) return;
|
||||
|
||||
// Must use main hand
|
||||
if (event.getHand() != InteractionHand.MAIN_HAND) return;
|
||||
|
||||
// Skip enslaved merchants - allow normal NPC interactions (command wand, conversation)
|
||||
if (merchant.isTiedUp()) return;
|
||||
|
||||
Player player = event.getEntity();
|
||||
|
||||
// Validation 1: Merchant must be in MERCHANT mode (not hostile)
|
||||
if (merchant.isHostile()) {
|
||||
SystemMessageManager.sendToPlayer(
|
||||
player,
|
||||
SystemMessageManager.MessageCategory.ERROR,
|
||||
"The merchant is too angry to trade!"
|
||||
);
|
||||
event.setCanceled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validation 2: Merchant must not have captives
|
||||
if (merchant.hasCaptives()) {
|
||||
SystemMessageManager.sendToPlayer(
|
||||
player,
|
||||
SystemMessageManager.MessageCategory.ERROR,
|
||||
"The merchant is busy with a captive!"
|
||||
);
|
||||
event.setCanceled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Open trading screen
|
||||
if (player instanceof ServerPlayer serverPlayer) {
|
||||
// Mark merchant as trading with this player
|
||||
merchant.startTrading(player.getUUID());
|
||||
|
||||
ModNetwork.sendToPlayer(
|
||||
new PacketOpenMerchantScreen(
|
||||
merchant.getUUID(),
|
||||
merchant.getTrades()
|
||||
),
|
||||
serverPlayer
|
||||
);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MerchantInteractionEventHandler] {} opened merchant screen for {}",
|
||||
player.getName().getString(),
|
||||
merchant.getNpcName()
|
||||
);
|
||||
}
|
||||
|
||||
event.setCanceled(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package com.tiedup.remake.events.lifecycle;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.v2.bondage.IV2BondageItem;
|
||||
import com.tiedup.remake.v2.bondage.capability.V2BondageEquipmentProvider;
|
||||
import java.util.Map;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.GameRules;
|
||||
import net.minecraftforge.event.AttachCapabilitiesEvent;
|
||||
import net.minecraftforge.event.entity.player.PlayerEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
/**
|
||||
* Event handler for capability attachment and persistence.
|
||||
* Handles attaching V2 bondage equipment to players and preserving it across death/respawn.
|
||||
*/
|
||||
@Mod.EventBusSubscriber(
|
||||
modid = TiedUpMod.MOD_ID,
|
||||
bus = Mod.EventBusSubscriber.Bus.FORGE
|
||||
)
|
||||
public class CapabilityEventHandler {
|
||||
|
||||
private static final ResourceLocation V2_BONDAGE_EQUIPMENT_ID =
|
||||
ResourceLocation.fromNamespaceAndPath(
|
||||
TiedUpMod.MOD_ID,
|
||||
"v2_bondage_equipment"
|
||||
);
|
||||
|
||||
/**
|
||||
* Attach V2 bondage equipment capability to all players.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onAttachCapabilities(
|
||||
AttachCapabilitiesEvent<Entity> event
|
||||
) {
|
||||
if (!(event.getObject() instanceof Player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// V2 bondage equipment capability
|
||||
V2BondageEquipmentProvider v2Provider = new V2BondageEquipmentProvider();
|
||||
event.addCapability(V2_BONDAGE_EQUIPMENT_ID, v2Provider);
|
||||
event.addListener(v2Provider::invalidate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle bondage equipment on player clone (death/respawn or dimension change).
|
||||
*
|
||||
* <p>Behavior:
|
||||
* <ul>
|
||||
* <li>Death without keepInventory: Fire onUnequipped for all items, don't copy</li>
|
||||
* <li>Death with keepInventory: Preserve via serialize/deserialize round-trip</li>
|
||||
* <li>Dimension change (End portal): Preserve via serialize/deserialize round-trip</li>
|
||||
* </ul>
|
||||
*
|
||||
* IMPORTANT: Must call reviveCaps() on original player to access capabilities
|
||||
* after the entity has been marked as removed.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onPlayerClone(PlayerEvent.Clone event) {
|
||||
// Revive capabilities on old player (required after entity removal)
|
||||
event.getOriginal().reviveCaps();
|
||||
|
||||
Player oldPlayer = event.getOriginal();
|
||||
Player newPlayer = event.getEntity();
|
||||
|
||||
if (event.isWasDeath()) {
|
||||
boolean keepInventory = oldPlayer.level().getGameRules()
|
||||
.getBoolean(GameRules.RULE_KEEPINVENTORY);
|
||||
|
||||
// === V2 Death Handling ===
|
||||
oldPlayer
|
||||
.getCapability(V2BondageEquipmentProvider.V2_BONDAGE_EQUIPMENT)
|
||||
.ifPresent(oldV2 -> {
|
||||
if (keepInventory) {
|
||||
// keepInventory: serialize/deserialize round-trip
|
||||
newPlayer
|
||||
.getCapability(V2BondageEquipmentProvider.V2_BONDAGE_EQUIPMENT)
|
||||
.ifPresent(newV2 -> {
|
||||
CompoundTag saved = oldV2.serializeNBT();
|
||||
newV2.deserializeNBT(saved);
|
||||
// Fire onEquipped for each item on the new player
|
||||
for (Map.Entry<BodyRegionV2, ItemStack> entry
|
||||
: newV2.getAllEquipped().entrySet()) {
|
||||
ItemStack stack = entry.getValue();
|
||||
if (stack.getItem() instanceof IV2BondageItem v2Item) {
|
||||
v2Item.onEquipped(stack, newPlayer);
|
||||
}
|
||||
}
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[CapabilityEventHandler] V2 preserved on death (keepInventory)"
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// No keepInventory: fire onUnequipped for each V2 item
|
||||
for (Map.Entry<BodyRegionV2, ItemStack> entry
|
||||
: oldV2.getAllEquipped().entrySet()) {
|
||||
ItemStack stack = entry.getValue();
|
||||
if (stack.getItem() instanceof IV2BondageItem v2Item) {
|
||||
v2Item.onUnequipped(stack, oldPlayer);
|
||||
}
|
||||
}
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[CapabilityEventHandler] V2 cleared on death (no keepInventory)"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Invalidate old player caps to prevent memory leak
|
||||
event.getOriginal().invalidateCaps();
|
||||
return;
|
||||
}
|
||||
|
||||
// DIMENSION CHANGE (End portal, etc.): Preserve all equipment
|
||||
|
||||
// === V2 Dimension Change ===
|
||||
oldPlayer
|
||||
.getCapability(V2BondageEquipmentProvider.V2_BONDAGE_EQUIPMENT)
|
||||
.ifPresent(oldV2 -> {
|
||||
newPlayer
|
||||
.getCapability(V2BondageEquipmentProvider.V2_BONDAGE_EQUIPMENT)
|
||||
.ifPresent(newV2 -> {
|
||||
CompoundTag saved = oldV2.serializeNBT();
|
||||
newV2.deserializeNBT(saved);
|
||||
// Fire onEquipped for each item on the new player
|
||||
for (Map.Entry<BodyRegionV2, ItemStack> entry
|
||||
: newV2.getAllEquipped().entrySet()) {
|
||||
ItemStack stack = entry.getValue();
|
||||
if (stack.getItem() instanceof IV2BondageItem v2Item) {
|
||||
v2Item.onEquipped(stack, newPlayer);
|
||||
}
|
||||
}
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[CapabilityEventHandler] Preserved V2 equipment on dimension change"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Invalidate old player caps to prevent memory leak
|
||||
event.getOriginal().invalidateCaps();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package com.tiedup.remake.events.lifecycle;
|
||||
|
||||
import com.tiedup.remake.core.SettingsAccessor;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.entities.EntityDamsel;
|
||||
import com.tiedup.remake.entities.EntityKidnapper;
|
||||
import com.tiedup.remake.entities.EntityKidnapperArcher;
|
||||
import com.tiedup.remake.entities.EntityKidnapperElite;
|
||||
import com.tiedup.remake.entities.EntityKidnapperMerchant;
|
||||
import com.tiedup.remake.entities.EntityMaster;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.level.GameRules;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraftforge.event.entity.living.MobSpawnEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
/**
|
||||
* Event handler for controlling entity spawning via gamerules.
|
||||
*
|
||||
* Controls both on/off spawning and spawn rates:
|
||||
* - damselsSpawn + damselSpawnRate: Controls EntityDamsel
|
||||
* - kidnappersSpawn + variant rates: Controls EntityKidnapper variants
|
||||
* - masterSpawnRate: Controls EntityMaster
|
||||
*
|
||||
* Spawn rates are 0-100 percentage. For example, 50 = 50% chance to allow spawn.
|
||||
* Note: Command spawns (/tiedup npc spawn) are not affected.
|
||||
*/
|
||||
@Mod.EventBusSubscriber(
|
||||
modid = TiedUpMod.MOD_ID,
|
||||
bus = Mod.EventBusSubscriber.Bus.FORGE
|
||||
)
|
||||
public class EntitySpawnHandler {
|
||||
|
||||
/**
|
||||
* Check if entity spawn should be allowed based on gamerules.
|
||||
*
|
||||
* @param event The spawn check event
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onCheckSpawn(MobSpawnEvent.FinalizeSpawn event) {
|
||||
LivingEntity entity = event.getEntity();
|
||||
Level level = entity.level();
|
||||
GameRules gameRules = level.getGameRules();
|
||||
RandomSource random = level.getRandom();
|
||||
|
||||
// Check Master spawn (separate entity, not related to Damsel)
|
||||
if (entity instanceof EntityMaster) {
|
||||
int spawnRate = SettingsAccessor.getMasterSpawnRate(gameRules);
|
||||
if (!checkSpawnRate(random, spawnRate)) {
|
||||
event.setSpawnCancelled(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Check Kidnapper spawn (includes Elite, Archer, Merchant)
|
||||
else if (entity instanceof EntityKidnapper kidnapper) {
|
||||
if (!SettingsAccessor.doKidnappersSpawn(gameRules)) {
|
||||
event.setSpawnCancelled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check variant-specific spawn rate
|
||||
int spawnRate = getKidnapperVariantSpawnRate(kidnapper, gameRules);
|
||||
if (!checkSpawnRate(random, spawnRate)) {
|
||||
event.setSpawnCancelled(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Check Damsel spawn
|
||||
else if (entity instanceof EntityDamsel) {
|
||||
if (!SettingsAccessor.doDamselsSpawn(gameRules)) {
|
||||
event.setSpawnCancelled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check damsel spawn rate
|
||||
int spawnRate = SettingsAccessor.getDamselSpawnRate(gameRules);
|
||||
if (!checkSpawnRate(random, spawnRate)) {
|
||||
event.setSpawnCancelled(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if spawn should be allowed based on rate (0-100).
|
||||
*
|
||||
* @param random Random source
|
||||
* @param rate Spawn rate percentage (0-100)
|
||||
* @return true if spawn should be allowed
|
||||
*/
|
||||
private static boolean checkSpawnRate(RandomSource random, int rate) {
|
||||
if (rate <= 0) return false;
|
||||
if (rate >= 100) return true;
|
||||
return random.nextInt(100) < rate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get spawn rate for a specific kidnapper variant.
|
||||
*/
|
||||
private static int getKidnapperVariantSpawnRate(
|
||||
EntityKidnapper kidnapper,
|
||||
GameRules gameRules
|
||||
) {
|
||||
// Check class type to determine spawn rate
|
||||
if (kidnapper instanceof EntityKidnapperArcher) {
|
||||
return SettingsAccessor.getKidnapperArcherSpawnRate(gameRules);
|
||||
} else if (kidnapper instanceof EntityKidnapperElite) {
|
||||
return SettingsAccessor.getKidnapperEliteSpawnRate(gameRules);
|
||||
} else if (kidnapper instanceof EntityKidnapperMerchant) {
|
||||
return SettingsAccessor.getKidnapperMerchantSpawnRate(gameRules);
|
||||
} else {
|
||||
// Base kidnapper
|
||||
return SettingsAccessor.getKidnapperSpawnRate(gameRules);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package com.tiedup.remake.events.lifecycle;
|
||||
|
||||
import com.mojang.logging.LogUtils;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.events.captivity.ForcedSeatingHandler;
|
||||
import com.tiedup.remake.events.restriction.LaborToolProtectionHandler;
|
||||
import com.tiedup.remake.events.restriction.PetPlayRestrictionHandler;
|
||||
import com.tiedup.remake.network.PacketRateLimiter;
|
||||
import net.minecraftforge.event.entity.player.PlayerEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
/**
|
||||
* Handler for player disconnect events to clean up server-side resources.
|
||||
*
|
||||
* <p>This handler ensures that resources associated with disconnected players
|
||||
* are properly cleaned up to prevent memory leaks and unbounded map growth.
|
||||
*
|
||||
* <p>Phase: Server Resource Management
|
||||
*
|
||||
* <p>Cleanup includes:
|
||||
* <ul>
|
||||
* <li>Rate limiter token buckets ({@link PacketRateLimiter})</li>
|
||||
* <li>Future: Session data, pending tasks, etc.</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Mod.EventBusSubscriber(
|
||||
modid = TiedUpMod.MOD_ID,
|
||||
bus = Mod.EventBusSubscriber.Bus.FORGE
|
||||
)
|
||||
public class PlayerDisconnectHandler {
|
||||
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
|
||||
/**
|
||||
* Clean up resources when a player logs out.
|
||||
*
|
||||
* <p>This event fires when a player disconnects from the server,
|
||||
* either by logging out normally or being kicked/timing out.
|
||||
*
|
||||
* @param event The player logout event
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onPlayerLogout(PlayerEvent.PlayerLoggedOutEvent event) {
|
||||
java.util.UUID playerId = event.getEntity().getUUID();
|
||||
|
||||
// NOTE: PlayerBindState.removeInstance() is called by PlayerLifecycleHandler (EventPriority.HIGH)
|
||||
// We don't duplicate it here to avoid redundant cleanup
|
||||
|
||||
// Clean up rate limiter state
|
||||
PacketRateLimiter.cleanup(playerId);
|
||||
|
||||
// Clean up static cooldown maps to prevent memory leaks
|
||||
com.tiedup.remake.commands.SocialCommand.cleanupPlayer(playerId);
|
||||
LaborToolProtectionHandler.cleanupPlayer(playerId);
|
||||
|
||||
// BUG FIX: Memory leak cleanup for event handlers
|
||||
// Clean up ForcedSeatingHandler maps
|
||||
ForcedSeatingHandler.clearPlayer(playerId);
|
||||
|
||||
// Clean up PetPlayRestrictionHandler timestamp map
|
||||
PetPlayRestrictionHandler.clearPlayer(playerId);
|
||||
|
||||
// Clean up minigame sessions
|
||||
com.tiedup.remake.minigame.MiniGameSessionManager.getInstance().cleanupPlayer(
|
||||
playerId
|
||||
);
|
||||
|
||||
// Clean up pet cage state
|
||||
com.tiedup.remake.v2.blocks.PetCageManager.onPlayerDisconnect(playerId);
|
||||
|
||||
// Clean up pet bed state (has onPlayerDisconnect but was never wired)
|
||||
com.tiedup.remake.v2.blocks.PetBedManager.onPlayerDisconnect(playerId);
|
||||
|
||||
// Clean up active conversations
|
||||
com.tiedup.remake.dialogue.conversation.ConversationManager.cleanupPlayer(playerId);
|
||||
|
||||
// Clean up cell selection mode
|
||||
com.tiedup.remake.cells.CellSelectionManager.cleanup(playerId);
|
||||
|
||||
// BUG FIX: Security - Remove labor tools from disconnecting player
|
||||
// This prevents players from keeping unbreakable tools by disconnecting
|
||||
if (
|
||||
event.getEntity() instanceof
|
||||
net.minecraft.server.level.ServerPlayer player
|
||||
) {
|
||||
removeLaborTools(player);
|
||||
}
|
||||
|
||||
// BUG FIX: Memory leak cleanup for entities
|
||||
// Clean up EntityKidnapperMerchant tradingPlayers set (O(1) reverse-lookup)
|
||||
if (
|
||||
event.getEntity().level() instanceof
|
||||
net.minecraft.server.level.ServerLevel serverLevel
|
||||
) {
|
||||
java.util.UUID merchantUUID =
|
||||
com.tiedup.remake.entities.EntityKidnapperMerchant.getMerchantForPlayer(
|
||||
playerId
|
||||
);
|
||||
if (merchantUUID != null) {
|
||||
net.minecraft.world.entity.Entity merchantEntity =
|
||||
serverLevel.getEntity(merchantUUID);
|
||||
if (
|
||||
merchantEntity instanceof
|
||||
com.tiedup.remake.entities.EntityKidnapperMerchant merchant
|
||||
) {
|
||||
merchant.cleanupTradingPlayer(playerId);
|
||||
}
|
||||
}
|
||||
|
||||
// Kidnapper robbery immunity: cheap per-entity Map.remove(), disconnect-only — acceptable scan
|
||||
// Uses getAllEntities since there's no UUID index for this reverse lookup
|
||||
for (net.minecraft.world.entity.Entity entity : serverLevel.getAllEntities()) {
|
||||
if (
|
||||
entity instanceof
|
||||
com.tiedup.remake.entities.EntityKidnapper kidnapper
|
||||
) {
|
||||
kidnapper.clearRobbedImmunity(playerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.debug(
|
||||
"Cleaned up server resources for player: {} ({})",
|
||||
event.getEntity().getName().getString(),
|
||||
playerId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* SECURITY: Remove all labor tools from player inventory on disconnect.
|
||||
* Prevents exploit where players disconnect to keep unbreakable tools.
|
||||
*/
|
||||
private static void removeLaborTools(
|
||||
net.minecraft.server.level.ServerPlayer player
|
||||
) {
|
||||
var inventory = player.getInventory();
|
||||
int removedCount = 0;
|
||||
|
||||
for (int i = 0; i < inventory.getContainerSize(); i++) {
|
||||
net.minecraft.world.item.ItemStack stack = inventory.getItem(i);
|
||||
if (!stack.isEmpty() && stack.hasTag()) {
|
||||
net.minecraft.nbt.CompoundTag tag = stack.getTag();
|
||||
if (tag != null && tag.getBoolean("LaborTool")) {
|
||||
inventory.setItem(
|
||||
i,
|
||||
net.minecraft.world.item.ItemStack.EMPTY
|
||||
);
|
||||
removedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (removedCount > 0) {
|
||||
LOGGER.info(
|
||||
"[PlayerDisconnectHandler] Removed {} labor tools from {} on disconnect",
|
||||
removedCount,
|
||||
player.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
package com.tiedup.remake.events.lifecycle;
|
||||
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.v2.bondage.capability.V2BondageEquipmentProvider;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.entities.EntityMaster;
|
||||
import com.tiedup.remake.entities.ModEntities;
|
||||
import com.tiedup.remake.state.IPlayerLeashAccess;
|
||||
import com.tiedup.remake.state.PlayerBindState;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.decoration.LeashFenceKnotEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraftforge.event.entity.player.PlayerEvent;
|
||||
import net.minecraftforge.eventbus.api.EventPriority;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
/**
|
||||
* Handles clean player lifecycle transitions.
|
||||
* - Master/Slave links are cleaned up on disconnect.
|
||||
* - Pole leash positions are saved for restoration on reconnect.
|
||||
*/
|
||||
@Mod.EventBusSubscriber(modid = TiedUpMod.MOD_ID)
|
||||
public class PlayerLifecycleHandler {
|
||||
|
||||
/**
|
||||
* Triggered when a player leaves the server.
|
||||
* Cleans up enslavement links and saves pole leash state.
|
||||
*
|
||||
* Priority HIGH ensures this runs BEFORE other logout handlers that may read PlayerBindState.
|
||||
*/
|
||||
@SubscribeEvent(priority = EventPriority.HIGH)
|
||||
public static void onPlayerLoggedOut(
|
||||
PlayerEvent.PlayerLoggedOutEvent event
|
||||
) {
|
||||
Player player = event.getEntity();
|
||||
if (player.level().isClientSide) return;
|
||||
|
||||
PlayerBindState state = PlayerBindState.getInstance(player);
|
||||
if (state == null) return;
|
||||
|
||||
// Phase 17: CASE 1: This player is a Captor - free all their captives
|
||||
if (state.getCaptorManager().hasCaptives()) {
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[Lifecycle] Captor {} disconnected. Freeing all captives.",
|
||||
player.getName().getString()
|
||||
);
|
||||
state.getCaptorManager().freeAllCaptives(true);
|
||||
}
|
||||
|
||||
// CASE 2: This player is leashed
|
||||
if (
|
||||
player instanceof IPlayerLeashAccess access &&
|
||||
access.tiedup$isLeashed()
|
||||
) {
|
||||
Entity holder = access.tiedup$getLeashHolder();
|
||||
|
||||
// CASE 2a: Leashed to a pole - SAVE for restoration
|
||||
if (holder instanceof LeashFenceKnotEntity fenceKnot) {
|
||||
BlockPos polePos = fenceKnot.getPos();
|
||||
player
|
||||
.getCapability(V2BondageEquipmentProvider.V2_BONDAGE_EQUIPMENT)
|
||||
.ifPresent(cap -> {
|
||||
cap.savePoleLeash(polePos, player.level().dimension());
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[Lifecycle] {} disconnected while leashed to pole at {}. Saved for restoration.",
|
||||
player.getName().getString(),
|
||||
polePos
|
||||
);
|
||||
});
|
||||
// Detach without dropping (will restore on reconnect)
|
||||
access.tiedup$detachLeash();
|
||||
}
|
||||
// MEDIUM FIX: CASE 2b: Leashed to a captor (player or NPC) - SAVE for restoration
|
||||
else if (state.isCaptive() && state.getCaptor() != null) {
|
||||
java.util.UUID captorUUID = state
|
||||
.getCaptor()
|
||||
.getEntity()
|
||||
.getUUID();
|
||||
player
|
||||
.getCapability(V2BondageEquipmentProvider.V2_BONDAGE_EQUIPMENT)
|
||||
.ifPresent(cap -> {
|
||||
cap.saveCaptorUUID(captorUUID);
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[Lifecycle] {} disconnected while captive to {}. Saved for restoration.",
|
||||
player.getName().getString(),
|
||||
captorUUID.toString().substring(0, 8)
|
||||
);
|
||||
});
|
||||
|
||||
// Remove from captor's tracking list (will be re-added on reconnect)
|
||||
state.getCaptor().removeCaptive(state, false);
|
||||
|
||||
// Detach leash without dropping (will restore on reconnect)
|
||||
access.tiedup$detachLeash();
|
||||
|
||||
// Clear captor reference (will be restored on reconnect)
|
||||
state.setCaptor(null);
|
||||
}
|
||||
}
|
||||
|
||||
// CASE 3: Pet play - handle Master entity
|
||||
handleMasterOnDisconnect(player);
|
||||
|
||||
// Canonical removal point — this is the ONLY place removeInstance should be called for server-side logout
|
||||
PlayerBindState.removeInstance(player.getUUID(), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when a player joins the server.
|
||||
* Restores Master entity if the player was a pet.
|
||||
*/
|
||||
@SubscribeEvent(priority = EventPriority.LOW)
|
||||
public static void onPlayerLoggedIn(PlayerEvent.PlayerLoggedInEvent event) {
|
||||
Player player = event.getEntity();
|
||||
if (player.level().isClientSide) return;
|
||||
|
||||
if (player instanceof ServerPlayer serverPlayer) {
|
||||
// Clean up any leftover pet bed/cage speed modifiers from previous session
|
||||
com.tiedup.remake.v2.blocks.PetBedManager.onPlayerLogin(
|
||||
serverPlayer
|
||||
);
|
||||
com.tiedup.remake.v2.blocks.PetCageManager.onPlayerLogin(
|
||||
serverPlayer
|
||||
);
|
||||
|
||||
// Delay to ensure player is fully loaded and in world
|
||||
serverPlayer
|
||||
.getServer()
|
||||
.execute(() -> {
|
||||
// Extra tick delay to ensure world is ready
|
||||
serverPlayer
|
||||
.level()
|
||||
.getServer()
|
||||
.execute(() -> {
|
||||
handleMasterOnReconnect(serverPlayer);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// MASTER PERSISTENCE CONSTANTS
|
||||
// ========================================
|
||||
|
||||
private static final String NBT_MASTER_DATA = "TiedUp_MasterPersistence";
|
||||
private static final String NBT_MASTER_HAS_MASTER = "HasMaster";
|
||||
private static final String NBT_MASTER_VARIANT_ID = "MasterVariantId";
|
||||
private static final String NBT_MASTER_STATE = "MasterState";
|
||||
private static final String NBT_MASTER_NAME = "MasterName";
|
||||
|
||||
// ========================================
|
||||
// MASTER DISCONNECT HANDLING
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Handle Master entity when pet player disconnects.
|
||||
* Saves Master data to player and removes Master entity.
|
||||
*
|
||||
* If Master entity is not found (in unloaded chunk), falls back to reading
|
||||
* data from the collar NBT which is continuously synchronized.
|
||||
*/
|
||||
private static void handleMasterOnDisconnect(Player player) {
|
||||
// Check if player has a Master (pet play mode)
|
||||
UUID masterUUID = EntityMaster.getMasterUUID(player);
|
||||
if (masterUUID == null) return;
|
||||
|
||||
if (!(player.level() instanceof ServerLevel serverLevel)) return;
|
||||
|
||||
// O(1) lookup by UUID instead of scanning all entities
|
||||
EntityMaster master = null;
|
||||
Entity masterEntity = serverLevel.getEntity(masterUUID);
|
||||
if (masterEntity instanceof EntityMaster m) {
|
||||
master = m;
|
||||
}
|
||||
|
||||
CompoundTag masterData = new CompoundTag();
|
||||
|
||||
if (master != null) {
|
||||
// Master found - save from entity (most up-to-date)
|
||||
masterData.putBoolean(NBT_MASTER_HAS_MASTER, true);
|
||||
masterData.putString(
|
||||
NBT_MASTER_VARIANT_ID,
|
||||
master.getKidnapperVariantId()
|
||||
);
|
||||
masterData.putString(
|
||||
NBT_MASTER_STATE,
|
||||
master.getStateManager().serializeState()
|
||||
);
|
||||
masterData.putString(NBT_MASTER_NAME, master.getNpcName());
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[Lifecycle] {} disconnected - saving Master {} (entity found)",
|
||||
player.getName().getString(),
|
||||
master.getNpcName()
|
||||
);
|
||||
|
||||
// Despawn the Master entity
|
||||
master.discard();
|
||||
} else {
|
||||
// Master not found - fallback to collar data
|
||||
ItemStack collar =
|
||||
com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper.getInRegion(
|
||||
player,
|
||||
com.tiedup.remake.v2.BodyRegionV2.NECK
|
||||
);
|
||||
|
||||
if (!collar.isEmpty() && collar.hasTag()) {
|
||||
CompoundTag collarTag = collar.getTag();
|
||||
if (collarTag.getBoolean("masterDataValid")) {
|
||||
masterData.putBoolean(NBT_MASTER_HAS_MASTER, true);
|
||||
masterData.putString(
|
||||
NBT_MASTER_VARIANT_ID,
|
||||
collarTag.getString("masterVariantId")
|
||||
);
|
||||
masterData.putString(
|
||||
NBT_MASTER_STATE,
|
||||
collarTag.getString("masterState")
|
||||
);
|
||||
masterData.putString(
|
||||
NBT_MASTER_NAME,
|
||||
collarTag.getString("masterName")
|
||||
);
|
||||
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[Lifecycle] {} disconnected - Master entity not found in loaded chunks, " +
|
||||
"using collar data. Master may remain in unloaded chunk.",
|
||||
player.getName().getString()
|
||||
);
|
||||
} else {
|
||||
TiedUpMod.LOGGER.error(
|
||||
"[Lifecycle] {} has Master UUID but no valid collar data!",
|
||||
player.getName().getString()
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
TiedUpMod.LOGGER.error(
|
||||
"[Lifecycle] {} has Master UUID but no collar found!",
|
||||
player.getName().getString()
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Store in player's persistent data
|
||||
CompoundTag persistentData = player.getPersistentData();
|
||||
persistentData.put(NBT_MASTER_DATA, masterData);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// MASTER RECONNECT HANDLING
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Handle Master restoration when pet player reconnects.
|
||||
* Spawns a new Master entity with saved data.
|
||||
*
|
||||
* First cleans up any orphaned Master entities that may remain from previous sessions
|
||||
* (e.g., if Master was in unloaded chunk during disconnect).
|
||||
*/
|
||||
private static void handleMasterOnReconnect(ServerPlayer player) {
|
||||
// Check if player has saved Master data
|
||||
CompoundTag persistentData = player.getPersistentData();
|
||||
if (!persistentData.contains(NBT_MASTER_DATA)) return;
|
||||
|
||||
CompoundTag masterData = persistentData.getCompound(NBT_MASTER_DATA);
|
||||
if (!masterData.getBoolean(NBT_MASTER_HAS_MASTER)) return;
|
||||
|
||||
// Check if player still has pet collar
|
||||
if (!EntityMaster.hasPetCollar(player)) {
|
||||
// Player no longer has collar - clear saved data
|
||||
persistentData.remove(NBT_MASTER_DATA);
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[Lifecycle] {} reconnected but no pet collar - clearing Master data",
|
||||
player.getName().getString()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
ServerLevel level = player.serverLevel();
|
||||
|
||||
// CLEANUP: Remove orphaned Master entity that may remain from previous session
|
||||
// (e.g., if Master was in unloaded chunk during disconnect)
|
||||
// O(1) lookup by UUID from collar instead of scanning all entities
|
||||
UUID existingMasterUUID = EntityMaster.getMasterUUID(player);
|
||||
if (existingMasterUUID != null) {
|
||||
Entity orphanEntity = level.getEntity(existingMasterUUID);
|
||||
if (
|
||||
orphanEntity instanceof EntityMaster m && m.isPetPlayer(player)
|
||||
) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[Lifecycle] Found orphaned Master {} claiming {}, despawning before restoration",
|
||||
m.getNpcName(),
|
||||
player.getName().getString()
|
||||
);
|
||||
m.discard();
|
||||
}
|
||||
}
|
||||
|
||||
// Spawn new Master entity near player
|
||||
EntityMaster master = ModEntities.MASTER.get().create(level);
|
||||
if (master == null) {
|
||||
TiedUpMod.LOGGER.error(
|
||||
"[Lifecycle] Failed to create Master entity for {}",
|
||||
player.getName().getString()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore Master data
|
||||
String variantId = masterData.getString(NBT_MASTER_VARIANT_ID);
|
||||
String stateSerialized = masterData.getString(NBT_MASTER_STATE);
|
||||
String masterName = masterData.getString(NBT_MASTER_NAME);
|
||||
|
||||
if (!variantId.isEmpty()) {
|
||||
// Lookup and set the variant
|
||||
com.tiedup.remake.entities.KidnapperVariant variant =
|
||||
master.lookupVariantById(variantId);
|
||||
if (variant != null) {
|
||||
master.setKidnapperVariant(variant);
|
||||
}
|
||||
}
|
||||
|
||||
// Position Master near player
|
||||
double angle = master.getRandom().nextDouble() * Math.PI * 2;
|
||||
double dist = 2.0 + master.getRandom().nextDouble() * 2;
|
||||
double x = player.getX() + Math.cos(angle) * dist;
|
||||
double z = player.getZ() + Math.sin(angle) * dist;
|
||||
|
||||
master.setPos(x, player.getY(), z);
|
||||
|
||||
// Set the pet player
|
||||
master.setPetPlayer(player);
|
||||
|
||||
// Restore state if available
|
||||
if (!stateSerialized.isEmpty()) {
|
||||
master.getStateManager().deserializeState(stateSerialized);
|
||||
}
|
||||
|
||||
// Spawn the Master
|
||||
level.addFreshEntity(master);
|
||||
|
||||
// Update the player's collar with new Master UUID
|
||||
updateCollarMasterUUID(player, master.getUUID());
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[Lifecycle] {} reconnected - restored Master {} at ({}, {}, {})",
|
||||
player.getName().getString(),
|
||||
master.getNpcName(),
|
||||
(int) x,
|
||||
(int) player.getY(),
|
||||
(int) z
|
||||
);
|
||||
|
||||
// Clear saved Master data
|
||||
persistentData.remove(NBT_MASTER_DATA);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's collar with the new Master UUID.
|
||||
*/
|
||||
private static void updateCollarMasterUUID(
|
||||
ServerPlayer player,
|
||||
UUID masterUUID
|
||||
) {
|
||||
net.minecraft.world.item.ItemStack collarStack =
|
||||
com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper.getInRegion(
|
||||
player,
|
||||
com.tiedup.remake.v2.BodyRegionV2.NECK
|
||||
);
|
||||
|
||||
if (!collarStack.isEmpty()) {
|
||||
CompoundTag tag = collarStack.getOrCreateTag();
|
||||
tag.putUUID("masterUUID", masterUUID);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
package com.tiedup.remake.events.lifecycle;
|
||||
|
||||
import com.tiedup.remake.events.restriction.BondageItemRestrictionHandler;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.v2.bondage.capability.V2BondageEquipmentProvider;
|
||||
import com.tiedup.remake.cells.CampOwnership;
|
||||
import com.tiedup.remake.cells.CellRegistryV2;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.labor.LaborTask;
|
||||
import com.tiedup.remake.network.ModNetwork;
|
||||
import com.tiedup.remake.network.labor.PacketSyncLaborProgress;
|
||||
import com.tiedup.remake.network.sync.PacketSyncBindState;
|
||||
import com.tiedup.remake.prison.LaborRecord;
|
||||
import com.tiedup.remake.prison.PrisonerManager;
|
||||
import com.tiedup.remake.prison.PrisonerRecord;
|
||||
import com.tiedup.remake.prison.PrisonerState;
|
||||
import com.tiedup.remake.state.IPlayerLeashAccess;
|
||||
import com.tiedup.remake.state.PlayerBindState;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.decoration.LeashFenceKnotEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraftforge.event.entity.living.LivingDeathEvent;
|
||||
import net.minecraftforge.event.entity.player.PlayerEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
/**
|
||||
* Event handler for PlayerBindState lifecycle management.
|
||||
* Handles player connection, disconnection, death, and respawn.
|
||||
*/
|
||||
@Mod.EventBusSubscriber(
|
||||
modid = TiedUpMod.MOD_ID,
|
||||
bus = Mod.EventBusSubscriber.Bus.FORGE
|
||||
)
|
||||
public class PlayerStateEventHandler {
|
||||
|
||||
/** UUIDs of players who died while imprisoned — message sent on respawn */
|
||||
private static final Set<UUID> pendingDeathEscapeMessage =
|
||||
ConcurrentHashMap.newKeySet();
|
||||
|
||||
/**
|
||||
* Called when a player logs in to the server.
|
||||
* Initialize or reset their PlayerBindState, restore pole leash, and sync to client.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onPlayerLoggedIn(PlayerEvent.PlayerLoggedInEvent event) {
|
||||
if (!(event.getEntity() instanceof ServerPlayer player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerBindState state = PlayerBindState.getInstance(player);
|
||||
if (state != null) {
|
||||
state.resetNewConnection(player);
|
||||
}
|
||||
|
||||
// Restore pole leash if player was leashed to one when they disconnected
|
||||
restorePoleLeashIfNeeded(player);
|
||||
|
||||
// MEDIUM FIX: Restore captor if player was captive when they disconnected
|
||||
restoreCaptorIfNeeded(player);
|
||||
|
||||
// Sync V2 equipment + bind state to the logging-in player
|
||||
com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper.sync(player);
|
||||
|
||||
PacketSyncBindState statePacket = PacketSyncBindState.fromPlayer(
|
||||
player
|
||||
);
|
||||
if (statePacket != null) {
|
||||
ModNetwork.sendToPlayer(statePacket, player);
|
||||
}
|
||||
|
||||
// Sync Labor HUD on login
|
||||
syncLaborState(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore leash to pole if player was leashed when they disconnected.
|
||||
*/
|
||||
private static void restorePoleLeashIfNeeded(ServerPlayer player) {
|
||||
player
|
||||
.getCapability(V2BondageEquipmentProvider.V2_BONDAGE_EQUIPMENT)
|
||||
.ifPresent(cap -> {
|
||||
if (!cap.wasLeashedToPole()) {
|
||||
return;
|
||||
}
|
||||
|
||||
BlockPos polePos = cap.getSavedPolePosition();
|
||||
ResourceKey<Level> poleDimension = cap.getSavedPoleDimension();
|
||||
|
||||
if (polePos == null || poleDimension == null) {
|
||||
cap.clearSavedPoleLeash();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if player is in the same dimension
|
||||
if (!player.level().dimension().equals(poleDimension)) {
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[Lifecycle] {} reconnected but is in different dimension. Clearing saved pole leash.",
|
||||
player.getName().getString()
|
||||
);
|
||||
cap.clearSavedPoleLeash();
|
||||
return;
|
||||
}
|
||||
|
||||
ServerLevel level = player.serverLevel();
|
||||
|
||||
// Try to find or create the LeashFenceKnotEntity at that position
|
||||
LeashFenceKnotEntity fenceKnot =
|
||||
LeashFenceKnotEntity.getOrCreateKnot(level, polePos);
|
||||
|
||||
if (fenceKnot == null) {
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[Lifecycle] {} reconnected but pole at {} no longer exists.",
|
||||
player.getName().getString(),
|
||||
polePos
|
||||
);
|
||||
cap.clearSavedPoleLeash();
|
||||
return;
|
||||
}
|
||||
|
||||
// Attach leash to the pole
|
||||
if (player instanceof IPlayerLeashAccess access) {
|
||||
access.tiedup$attachLeash(fenceKnot);
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[Lifecycle] {} reconnected. Restored leash to pole at {}.",
|
||||
player.getName().getString(),
|
||||
polePos
|
||||
);
|
||||
}
|
||||
|
||||
// Clear saved data (no longer needed)
|
||||
cap.clearSavedPoleLeash();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* MEDIUM FIX: Restore captor if player was captive when they disconnected.
|
||||
*/
|
||||
private static void restoreCaptorIfNeeded(ServerPlayer player) {
|
||||
player
|
||||
.getCapability(V2BondageEquipmentProvider.V2_BONDAGE_EQUIPMENT)
|
||||
.ifPresent(cap -> {
|
||||
if (!cap.hasSavedCaptor()) {
|
||||
return;
|
||||
}
|
||||
|
||||
java.util.UUID captorUUID = cap.getSavedCaptorUUID();
|
||||
if (captorUUID == null) {
|
||||
cap.clearSavedCaptor();
|
||||
return;
|
||||
}
|
||||
|
||||
ServerLevel level = player.serverLevel();
|
||||
|
||||
// Try to find the captor entity
|
||||
net.minecraft.world.entity.Entity captorEntity =
|
||||
level.getEntity(captorUUID);
|
||||
|
||||
if (captorEntity == null) {
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[Lifecycle] {} reconnected but captor {} no longer exists. Clearing captivity.",
|
||||
player.getName().getString(),
|
||||
captorUUID.toString().substring(0, 8)
|
||||
);
|
||||
cap.clearSavedCaptor();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if captor is still a valid ICaptor
|
||||
if (
|
||||
!(captorEntity instanceof
|
||||
net.minecraft.world.entity.LivingEntity livingCaptor)
|
||||
) {
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[Lifecycle] {} reconnected but captor is not a LivingEntity. Clearing captivity.",
|
||||
player.getName().getString()
|
||||
);
|
||||
cap.clearSavedCaptor();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the ICaptor interface
|
||||
com.tiedup.remake.state.ICaptor captor = null;
|
||||
if (
|
||||
livingCaptor instanceof
|
||||
com.tiedup.remake.state.ICaptor kidnapper
|
||||
) {
|
||||
captor = kidnapper;
|
||||
} else {
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[Lifecycle] {} reconnected but captor {} does not implement ICaptor. Clearing captivity.",
|
||||
player.getName().getString(),
|
||||
captorUUID.toString().substring(0, 8)
|
||||
);
|
||||
cap.clearSavedCaptor();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if player is still enslavable (still tied up)
|
||||
PlayerBindState state = PlayerBindState.getInstance(player);
|
||||
if (state == null || !state.isTiedUp()) {
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[Lifecycle] {} reconnected but is no longer tied up. Clearing saved captor.",
|
||||
player.getName().getString()
|
||||
);
|
||||
cap.clearSavedCaptor();
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore captivity using the PlayerCaptivity component
|
||||
boolean success = state.getCapturedBy(captor);
|
||||
|
||||
if (success) {
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[Lifecycle] {} reconnected. Restored captivity to {}.",
|
||||
player.getName().getString(),
|
||||
captorEntity.getName().getString()
|
||||
);
|
||||
} else {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[Lifecycle] {} reconnected but failed to restore captivity to {}.",
|
||||
player.getName().getString(),
|
||||
captorEntity.getName().getString()
|
||||
);
|
||||
}
|
||||
|
||||
// Clear saved data (no longer needed)
|
||||
cap.clearSavedCaptor();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a player logs out of the server.
|
||||
* Clean up their PlayerBindState instance.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onPlayerLoggedOut(
|
||||
PlayerEvent.PlayerLoggedOutEvent event
|
||||
) {
|
||||
if (!(event.getEntity() instanceof ServerPlayer player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// NOTE: PlayerBindState.removeInstance() is NOT called here.
|
||||
// The canonical removal point is PlayerLifecycleHandler.onPlayerLoggedOut (EventPriority.HIGH).
|
||||
// Calling it here at NORMAL priority would be a redundant double-removal.
|
||||
|
||||
// Clean up message cooldowns to prevent memory leak
|
||||
BondageItemRestrictionHandler.clearCooldowns(player.getUUID());
|
||||
|
||||
// Clean up pending death escape message flag (player disconnected between death and respawn)
|
||||
pendingDeathEscapeMessage.remove(player.getUUID());
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a player dies.
|
||||
* Handle bondage state cleanup based on game rules.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onPlayerDeath(LivingDeathEvent event) {
|
||||
if (!(event.getEntity() instanceof ServerPlayer player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerBindState state = PlayerBindState.getInstance(player);
|
||||
if (state != null) {
|
||||
state.onDeathKidnapped(player.level());
|
||||
}
|
||||
|
||||
// Clean up captivity state - prisoner can't continue task after death
|
||||
PrisonerManager manager = PrisonerManager.get(player.serverLevel());
|
||||
PrisonerRecord record = manager.getRecord(player.getUUID());
|
||||
LaborRecord laborRecord = manager.getLaborRecord(player.getUUID());
|
||||
|
||||
if (record.isImprisoned() || laborRecord.hasTask()) {
|
||||
// SECURITY: Remove labor tools before death to prevent item duplication
|
||||
// (Tools would otherwise drop on death)
|
||||
removeLaborToolsOnDeath(player);
|
||||
|
||||
// Transition to FREE state via escape (clears all data)
|
||||
long currentTime = player.serverLevel().getGameTime();
|
||||
// Use centralized escape service for complete cleanup
|
||||
com.tiedup.remake.prison.service.PrisonerService.get().escape(
|
||||
player.serverLevel(),
|
||||
player.getUUID(),
|
||||
"death"
|
||||
);
|
||||
|
||||
// Flag for respawn message
|
||||
pendingDeathEscapeMessage.add(player.getUUID());
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PlayerStateEventHandler] Transitioned {} to FREE state on death",
|
||||
player.getName().getString()
|
||||
);
|
||||
} else if (record.isProtected(player.serverLevel().getGameTime())) {
|
||||
// Revoke grace period on death - player can be targeted again after respawn
|
||||
// Death is considered "payment" for any remaining grace
|
||||
record.setProtectionExpiry(0);
|
||||
}
|
||||
|
||||
// Release prisoner from any cells to prevent "ghost prisoner" blocking cells
|
||||
// Without this, the cell still counts this player as prisoner, making it "full"
|
||||
// and preventing new kidnappers from using it
|
||||
CellRegistryV2 cellRegistry = CellRegistryV2.get(player.server);
|
||||
if (cellRegistry != null) {
|
||||
int released = cellRegistry.releasePrisonerFromAllCells(
|
||||
player.getUUID()
|
||||
);
|
||||
if (released > 0) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PlayerStateEventHandler] Released {} from {} cell(s) on death",
|
||||
player.getName().getString(),
|
||||
released
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a player respawns after death.
|
||||
* Handled by PlayerEvent.Clone in CapabilityEventHandler (capability copying)
|
||||
* and PlayerLoggedIn (state reset).
|
||||
*
|
||||
* The respawn process:
|
||||
* 1. PlayerEvent.Clone -> Copy capability data from old player to new player
|
||||
* 2. PlayerLoggedIn -> Reset PlayerBindState with new player entity
|
||||
* 3. Sync to client
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onPlayerRespawn(PlayerEvent.PlayerRespawnEvent event) {
|
||||
if (!(event.getEntity() instanceof ServerPlayer player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get or create state for the respawned player
|
||||
PlayerBindState state = PlayerBindState.getInstance(player);
|
||||
if (state != null) {
|
||||
state.resetNewConnection(player);
|
||||
}
|
||||
|
||||
// Sync V2 equipment + bind state to the respawned player
|
||||
com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper.sync(player);
|
||||
|
||||
PacketSyncBindState statePacket = PacketSyncBindState.fromPlayer(
|
||||
player
|
||||
);
|
||||
if (statePacket != null) {
|
||||
ModNetwork.sendToPlayer(statePacket, player);
|
||||
}
|
||||
|
||||
// Sync Labor HUD on respawn (restores task OR clears it if executed)
|
||||
syncLaborState(player);
|
||||
|
||||
// Send death escape message if applicable
|
||||
if (pendingDeathEscapeMessage.remove(player.getUUID())) {
|
||||
player.sendSystemMessage(
|
||||
net.minecraft.network.chat.Component.literal(
|
||||
"You died and escaped captivity. Your items remain in the camp chest."
|
||||
).withStyle(net.minecraft.ChatFormatting.YELLOW)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to sync labor state to client.
|
||||
* Sends current progress if active, or a clear packet if not.
|
||||
*/
|
||||
private static void syncLaborState(ServerPlayer player) {
|
||||
PrisonerManager manager = PrisonerManager.get(player.serverLevel());
|
||||
PrisonerRecord record = manager.getRecord(player.getUUID());
|
||||
LaborRecord laborRecord = manager.getLaborRecord(player.getUUID());
|
||||
|
||||
if (record.isImprisoned() && laborRecord.hasTask()) {
|
||||
LaborTask task = laborRecord.getTask();
|
||||
ModNetwork.sendToPlayer(
|
||||
new PacketSyncLaborProgress(
|
||||
task.getDescription(),
|
||||
task.getProgress(),
|
||||
task.getQuota(),
|
||||
task.getValue()
|
||||
),
|
||||
player
|
||||
);
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PlayerStateEventHandler] Synced labor HUD for {}: {}/{} {}",
|
||||
player.getName().getString(),
|
||||
task.getProgress(),
|
||||
task.getQuota(),
|
||||
task.getDescription()
|
||||
);
|
||||
} else {
|
||||
// No active captivity state - ensure HUD is cleared (important after execution/death)
|
||||
ModNetwork.sendToPlayer(new PacketSyncLaborProgress(), player);
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PlayerStateEventHandler] Cleared labor HUD for {}",
|
||||
player.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SECURITY: Remove all labor tools from player inventory before death.
|
||||
* Prevents tools from dropping and being recoverable.
|
||||
*/
|
||||
private static void removeLaborToolsOnDeath(ServerPlayer player) {
|
||||
var inventory = player.getInventory();
|
||||
int removedCount = 0;
|
||||
|
||||
for (int i = 0; i < inventory.getContainerSize(); i++) {
|
||||
ItemStack stack = inventory.getItem(i);
|
||||
if (!stack.isEmpty() && stack.hasTag()) {
|
||||
CompoundTag tag = stack.getTag();
|
||||
if (tag != null && tag.getBoolean("LaborTool")) {
|
||||
inventory.setItem(i, ItemStack.EMPTY);
|
||||
removedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (removedCount > 0) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PlayerStateEventHandler] Removed {} labor tools from {} before death",
|
||||
removedCount,
|
||||
player.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,548 @@
|
||||
package com.tiedup.remake.events.restriction;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.entities.EntityKidnapper;
|
||||
import com.tiedup.remake.items.ItemLockpick;
|
||||
import com.tiedup.remake.items.base.IKnife;
|
||||
import com.tiedup.remake.state.IBondageState;
|
||||
import com.tiedup.remake.state.IRestrainable;
|
||||
import com.tiedup.remake.dialogue.EntityDialogueManager;
|
||||
import com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory;
|
||||
import com.tiedup.remake.util.KidnappedHelper;
|
||||
import com.tiedup.remake.core.SystemMessageManager;
|
||||
import com.tiedup.remake.core.SystemMessageManager.MessageCategory;
|
||||
import com.tiedup.remake.util.GameConstants;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.ButtonBlock;
|
||||
import net.minecraft.world.level.block.DoorBlock;
|
||||
import net.minecraft.world.level.block.FenceGateBlock;
|
||||
import net.minecraft.world.level.block.LeverBlock;
|
||||
import net.minecraft.world.level.block.TrapDoorBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import net.minecraftforge.event.TickEvent;
|
||||
import net.minecraftforge.event.entity.living.LivingEvent;
|
||||
import net.minecraftforge.event.entity.living.LivingHurtEvent;
|
||||
import net.minecraftforge.event.entity.living.LivingEntityUseItemEvent;
|
||||
import net.minecraftforge.event.entity.player.AttackEntityEvent;
|
||||
import net.minecraftforge.event.entity.player.PlayerEvent;
|
||||
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
|
||||
import net.minecraftforge.event.level.BlockEvent;
|
||||
import net.minecraftforge.eventbus.api.EventPriority;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
/**
|
||||
* Unified event handler for all bondage item restrictions.
|
||||
*
|
||||
* Phase 14.4+: Centralized restriction system
|
||||
* Leg Binding: Separate arm/leg restrictions
|
||||
*
|
||||
* This handler manages restrictions based on equipped bondage items:
|
||||
*
|
||||
* === LEGS BOUND (hasLegsBound) ===
|
||||
* - No sprinting
|
||||
* - No climbing ladders (can descend)
|
||||
* - No elytra flying
|
||||
* - Reduced swim speed (50%)
|
||||
*
|
||||
* === ARMS BOUND (hasArmsBound) ===
|
||||
* - No block breaking
|
||||
* - No block placing
|
||||
* - No attacking
|
||||
* - No item usage
|
||||
* - No block interaction (except allowed blocks)
|
||||
*
|
||||
* === MITTENS ===
|
||||
* - Additional hand restrictions
|
||||
* - Allowed: buttons, levers, doors, trapdoors, fence gates (when arms bound only)
|
||||
*
|
||||
* === BLINDFOLDED ===
|
||||
* - Vision effects (handled client-side)
|
||||
*
|
||||
* === GAGGED ===
|
||||
* - Chat muffling (handled in ChatEventHandler)
|
||||
*
|
||||
* @see RestraintTaskTickHandler for task tick progression (untying, tying, force feeding)
|
||||
*/
|
||||
@Mod.EventBusSubscriber(
|
||||
modid = TiedUpMod.MOD_ID,
|
||||
bus = Mod.EventBusSubscriber.Bus.FORGE
|
||||
)
|
||||
public class BondageItemRestrictionHandler {
|
||||
|
||||
/** Cooldown between restriction messages (in milliseconds) */
|
||||
private static final long MESSAGE_COOLDOWN_MS = 2000; // 2 seconds
|
||||
|
||||
/** Swim speed multiplier when tied (from config) */
|
||||
|
||||
/** Per-player, per-category message cooldowns */
|
||||
private static final Map<
|
||||
UUID,
|
||||
Map<MessageCategory, Long>
|
||||
> messageCooldowns = new HashMap<>();
|
||||
|
||||
// ========================================
|
||||
// MOVEMENT RESTRICTIONS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Movement restrictions per tick (throttled to every 5 ticks).
|
||||
* - Prevent sprinting when tied
|
||||
* - Prevent ladder climbing when tied (can descend)
|
||||
* - Reduce swim speed when tied
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onPlayerTick(TickEvent.PlayerTickEvent event) {
|
||||
if (event.side.isClient() || event.phase != TickEvent.Phase.END) {
|
||||
return;
|
||||
}
|
||||
|
||||
Player player = event.player;
|
||||
|
||||
// Throttle: only check every 5 ticks (0.25 seconds) - per-player timing
|
||||
if (player.tickCount % 5 != 0) {
|
||||
return;
|
||||
}
|
||||
IRestrainable state = KidnappedHelper.getKidnappedState(player);
|
||||
if (state == null) return;
|
||||
|
||||
// Movement restrictions only apply when LEGS are bound
|
||||
if (!state.hasLegsBound()) return;
|
||||
|
||||
// === SPRINT RESTRICTION ===
|
||||
if (player.isSprinting()) {
|
||||
player.setSprinting(false);
|
||||
}
|
||||
|
||||
// === LADDER RESTRICTION ===
|
||||
// Can descend but not climb
|
||||
if (player.onClimbable()) {
|
||||
Vec3 motion = player.getDeltaMovement();
|
||||
if (motion.y > 0) {
|
||||
player.setDeltaMovement(motion.x, 0, motion.z);
|
||||
}
|
||||
}
|
||||
|
||||
// === SWIM SPEED RESTRICTION ===
|
||||
if (player.isInWater() && player.isSwimming()) {
|
||||
Vec3 motion = player.getDeltaMovement();
|
||||
player.setDeltaMovement(motion.scale(
|
||||
com.tiedup.remake.core.ModConfig.SERVER.tiedSwimSpeedMultiplier.get()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent elytra flying when tied.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onLivingTick(LivingEvent.LivingTickEvent event) {
|
||||
if (!(event.getEntity() instanceof Player player)) return;
|
||||
if (player.level().isClientSide) return;
|
||||
|
||||
// Cheap check FIRST: only proceed if player is flying
|
||||
if (!player.isFallFlying()) return;
|
||||
|
||||
IRestrainable state = KidnappedHelper.getKidnappedState(player);
|
||||
if (state == null || !state.hasLegsBound()) return;
|
||||
|
||||
// Elytra restriction only applies when LEGS are bound
|
||||
player.stopFallFlying();
|
||||
sendRestrictionMessage(player, MessageCategory.NO_ELYTRA);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// INTERACTION RESTRICTIONS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Block breaking restrictions.
|
||||
* - Tied: Cannot break blocks
|
||||
* - Mittens: Cannot break blocks (hands covered)
|
||||
*/
|
||||
@SubscribeEvent(priority = EventPriority.HIGH)
|
||||
public static void onBlockBreak(BlockEvent.BreakEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
if (player == null) return;
|
||||
|
||||
IRestrainable state = KidnappedHelper.getKidnappedState(player);
|
||||
if (state == null) return;
|
||||
|
||||
// Block breaking requires ARMS to be free
|
||||
if (state.hasArmsBound()) {
|
||||
event.setCanceled(true);
|
||||
sendRestrictionMessage(player, MessageCategory.CANT_BREAK_TIED);
|
||||
} else if (state.hasMittens()) {
|
||||
event.setCanceled(true);
|
||||
sendRestrictionMessage(player, MessageCategory.CANT_BREAK_MITTENS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Block placing restrictions.
|
||||
* - Arms bound: Cannot place blocks
|
||||
* - Mittens: Cannot place blocks (hands covered)
|
||||
*/
|
||||
@SubscribeEvent(priority = EventPriority.HIGH)
|
||||
public static void onBlockPlace(BlockEvent.EntityPlaceEvent event) {
|
||||
if (!(event.getEntity() instanceof Player player)) return;
|
||||
|
||||
IRestrainable state = KidnappedHelper.getKidnappedState(player);
|
||||
if (state == null) return;
|
||||
|
||||
// Block placing requires ARMS to be free
|
||||
if (state.hasArmsBound()) {
|
||||
event.setCanceled(true);
|
||||
sendRestrictionMessage(player, MessageCategory.CANT_PLACE_TIED);
|
||||
} else if (state.hasMittens()) {
|
||||
event.setCanceled(true);
|
||||
sendRestrictionMessage(player, MessageCategory.CANT_PLACE_MITTENS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Block interaction restrictions.
|
||||
* - Arms bound: Cannot interact with most blocks
|
||||
* - Mittens: Cannot interact (hands covered)
|
||||
* - Exception: Buttons, levers, doors (can be pressed/opened with body when arms bound, but not with mittens)
|
||||
*/
|
||||
@SubscribeEvent(priority = EventPriority.HIGH)
|
||||
public static void onRightClickBlock(
|
||||
PlayerInteractEvent.RightClickBlock event
|
||||
) {
|
||||
Player player = event.getEntity();
|
||||
|
||||
IRestrainable state = KidnappedHelper.getKidnappedState(player);
|
||||
if (state == null) return;
|
||||
|
||||
boolean hasArmsBound = state.hasArmsBound();
|
||||
boolean hasMittens = state.hasMittens();
|
||||
|
||||
if (!hasArmsBound && !hasMittens) return;
|
||||
|
||||
// Get the block being interacted with
|
||||
BlockState blockState = event.getLevel().getBlockState(event.getPos());
|
||||
|
||||
// Allow specific block interactions (can press with body/head) - only when arms bound WITHOUT mittens
|
||||
// Mittens block ALL interactions since you can't use buttons with covered hands
|
||||
if (
|
||||
hasArmsBound && !hasMittens && isAllowedTiedInteraction(blockState)
|
||||
) {
|
||||
return; // Allow this interaction
|
||||
}
|
||||
|
||||
// Block all other interactions
|
||||
event.setCanceled(true);
|
||||
if (hasArmsBound) {
|
||||
sendRestrictionMessage(player, MessageCategory.CANT_INTERACT_TIED);
|
||||
} else {
|
||||
sendRestrictionMessage(
|
||||
player,
|
||||
MessageCategory.CANT_INTERACT_MITTENS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Item usage restrictions.
|
||||
* - Arms bound: Cannot use items (except knife/lockpick without mittens)
|
||||
* - Mittens: Cannot use items (hands covered)
|
||||
*
|
||||
* v2.5: Allow knife and lockpick usage when tied (but NOT with mittens).
|
||||
* This enables the player to cut/lockpick their binds while restrained.
|
||||
*/
|
||||
@SubscribeEvent(priority = EventPriority.HIGH)
|
||||
public static void onRightClickItem(
|
||||
PlayerInteractEvent.RightClickItem event
|
||||
) {
|
||||
Player player = event.getEntity();
|
||||
ItemStack heldItem = event.getItemStack();
|
||||
|
||||
IRestrainable state = KidnappedHelper.getKidnappedState(player);
|
||||
if (state == null) return;
|
||||
|
||||
// v2.5: Allow knife and lockpick usage when tied (but NOT with mittens)
|
||||
if (state.hasArmsBound() && !state.hasMittens()) {
|
||||
// Allow knives - they have special cutting logic
|
||||
if (heldItem.getItem() instanceof IKnife) {
|
||||
return; // Don't block
|
||||
}
|
||||
// Allow lockpicks - they open the struggle choice screen
|
||||
if (heldItem.getItem() instanceof ItemLockpick) {
|
||||
return; // Don't block
|
||||
}
|
||||
}
|
||||
|
||||
// Item usage requires ARMS to be free (except above exceptions)
|
||||
if (state.hasArmsBound()) {
|
||||
event.setCanceled(true);
|
||||
sendRestrictionMessage(player, MessageCategory.CANT_USE_ITEM_TIED);
|
||||
} else if (state.hasMittens()) {
|
||||
event.setCanceled(true);
|
||||
sendRestrictionMessage(
|
||||
player,
|
||||
MessageCategory.CANT_USE_ITEM_MITTENS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attack restrictions.
|
||||
* - Captive/slave attacking their kidnapper: Cannot attack, gets shocked
|
||||
* - Arms bound: Cannot attack at all
|
||||
* - Mittens only: Can punch but damage is zeroed in onLivingHurt()
|
||||
*/
|
||||
@SubscribeEvent(priority = EventPriority.HIGH)
|
||||
public static void onAttack(AttackEntityEvent event) {
|
||||
Player player = event.getEntity();
|
||||
Entity target = event.getTarget();
|
||||
|
||||
// Check if player is attacking a kidnapper
|
||||
if (target instanceof EntityKidnapper kidnapper) {
|
||||
// Check if the attacker is this kidnapper's captive or job worker
|
||||
boolean isThisCaptive = false;
|
||||
boolean isThisJobWorker = false;
|
||||
|
||||
// Check if player is the current captive
|
||||
IRestrainable captive = kidnapper.getCaptive();
|
||||
if (captive != null) {
|
||||
UUID captiveUUID = captive.getKidnappedUniqueId();
|
||||
if (
|
||||
captiveUUID != null && captiveUUID.equals(player.getUUID())
|
||||
) {
|
||||
isThisCaptive = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if player is the job worker
|
||||
UUID workerUUID = kidnapper.getJobWorkerUUID();
|
||||
if (workerUUID != null && workerUUID.equals(player.getUUID())) {
|
||||
isThisJobWorker = true;
|
||||
}
|
||||
|
||||
// Only block if player is THIS kidnapper's captive or job worker
|
||||
if (isThisCaptive || isThisJobWorker) {
|
||||
event.setCanceled(true);
|
||||
|
||||
// Kidnapper responds with dialogue
|
||||
kidnapper.talkTo(
|
||||
player,
|
||||
DialogueCategory.ATTACK_SLAVE
|
||||
);
|
||||
|
||||
// Shock the captive/worker
|
||||
IRestrainable playerState = KidnappedHelper.getKidnappedState(
|
||||
player
|
||||
);
|
||||
if (playerState != null) {
|
||||
playerState.shockKidnapped(
|
||||
" (You cannot attack your master!)",
|
||||
2.0f
|
||||
);
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[RESTRICTION] {} tried to attack master {} - shocked (captive={}, worker={})",
|
||||
player.getName().getString(),
|
||||
kidnapper.getName().getString(),
|
||||
isThisCaptive,
|
||||
isThisJobWorker
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Other players CAN attack this kidnapper - don't block
|
||||
}
|
||||
|
||||
IRestrainable state = KidnappedHelper.getKidnappedState(player);
|
||||
if (state == null) return;
|
||||
|
||||
// Arms bound: cannot attack at all
|
||||
if (state.hasArmsBound()) {
|
||||
event.setCanceled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Mittens only (not arms bound): allow punch animation
|
||||
// Damage will be zeroed in onLivingHurt()
|
||||
}
|
||||
|
||||
/**
|
||||
* Damage reduction for mittens.
|
||||
* When a player with mittens (but not arms bound) attacks, damage is reduced to 0.
|
||||
* The punch animation still plays, but no damage is dealt.
|
||||
*/
|
||||
@SubscribeEvent(priority = EventPriority.HIGHEST)
|
||||
public static void onLivingHurt(LivingHurtEvent event) {
|
||||
// Check if the attacker is a player with mittens
|
||||
if (!(event.getSource().getEntity() instanceof Player player)) return;
|
||||
|
||||
IRestrainable state = KidnappedHelper.getKidnappedState(player);
|
||||
if (state == null) return;
|
||||
|
||||
// Mittens only (not arms bound): zero damage
|
||||
if (state.hasMittens() && !state.hasArmsBound()) {
|
||||
event.setAmount(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fall damage protection for captive players on a leash.
|
||||
* When being led by a kidnapper/master, the player has no control over movement
|
||||
* and should not take fall damage from terrain the captor drags them through.
|
||||
*/
|
||||
@SubscribeEvent(priority = EventPriority.HIGHEST)
|
||||
public static void onCaptiveFallDamage(LivingHurtEvent event) {
|
||||
if (!(event.getEntity() instanceof Player player)) return;
|
||||
if (
|
||||
!event
|
||||
.getSource()
|
||||
.is(net.minecraft.world.damagesource.DamageTypes.FALL)
|
||||
) return;
|
||||
|
||||
IRestrainable state = KidnappedHelper.getKidnappedState(player);
|
||||
if (state == null) return;
|
||||
|
||||
if (state.isCaptive()) {
|
||||
event.setCanceled(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the Master when their pet takes damage.
|
||||
* This allows the Master to react (e.g., get up from human chair).
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onPetHurt(LivingHurtEvent event) {
|
||||
if (!(event.getEntity() instanceof ServerPlayer pet)) return;
|
||||
if (event.getAmount() <= 0) return;
|
||||
|
||||
// Find if this player has a nearby Master who owns them
|
||||
var masters = pet.level().getEntitiesOfClass(
|
||||
com.tiedup.remake.entities.EntityMaster.class,
|
||||
pet.getBoundingBox().inflate(32.0),
|
||||
m -> m.isAlive() && m.hasPet()
|
||||
&& pet.getUUID().equals(m.getStateManager().getPetPlayerUUID())
|
||||
);
|
||||
|
||||
for (var master : masters) {
|
||||
master.onPetHurt(event.getSource(), event.getAmount());
|
||||
}
|
||||
}
|
||||
|
||||
// ========== BREAK SPEED REDUCTION ==========
|
||||
|
||||
/**
|
||||
* Apply break speed reduction when tied up.
|
||||
* This makes mining extremely slow (10% speed) when restrained.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onCalculateSpeed(PlayerEvent.BreakSpeed event) {
|
||||
Player player = event.getEntity();
|
||||
IBondageState state = KidnappedHelper.getKidnappedState(player);
|
||||
|
||||
if (state != null && state.isTiedUp()) {
|
||||
event.setNewSpeed(
|
||||
event.getNewSpeed() * GameConstants.TIED_BREAK_SPEED_MULTIPLIER
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== BLOCK SELF-EATING WHEN GAGGED ==========
|
||||
|
||||
/**
|
||||
* Block gagged players from eating food themselves.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onGaggedPlayerEat(LivingEntityUseItemEvent.Start event) {
|
||||
if (!(event.getEntity() instanceof Player player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!event.getItem().getItem().isEdible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
IBondageState state = KidnappedHelper.getKidnappedState(player);
|
||||
if (state != null && state.isGagged()) {
|
||||
event.setCanceled(true);
|
||||
player.displayClientMessage(
|
||||
Component.literal("You can't eat with a gag on.").withStyle(
|
||||
ChatFormatting.RED
|
||||
),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// HELPER METHODS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Check if a block interaction should be allowed when tied.
|
||||
* These blocks can be activated with body/head, not hands.
|
||||
*/
|
||||
private static boolean isAllowedTiedInteraction(BlockState blockState) {
|
||||
// Buttons - can press with head/body
|
||||
if (blockState.getBlock() instanceof ButtonBlock) return true;
|
||||
|
||||
// Levers - can push with body
|
||||
if (blockState.getBlock() instanceof LeverBlock) return true;
|
||||
|
||||
// Doors - can push open with body
|
||||
if (blockState.getBlock() instanceof DoorBlock) return true;
|
||||
|
||||
// Trapdoors - debatable, but allow for now
|
||||
if (blockState.getBlock() instanceof TrapDoorBlock) return true;
|
||||
|
||||
// Fence gates - can push open
|
||||
if (blockState.getBlock() instanceof FenceGateBlock) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a restriction message to the player.
|
||||
* Uses per-category cooldown to prevent spam.
|
||||
*/
|
||||
private static void sendRestrictionMessage(
|
||||
Player player,
|
||||
MessageCategory category
|
||||
) {
|
||||
// Only send on server side
|
||||
if (player.level().isClientSide) return;
|
||||
|
||||
UUID playerId = player.getUUID();
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
// Get or create player's cooldown map
|
||||
Map<MessageCategory, Long> playerCooldowns =
|
||||
messageCooldowns.computeIfAbsent(playerId, k -> new HashMap<>());
|
||||
|
||||
// Check cooldown for this category
|
||||
Long lastSent = playerCooldowns.get(category);
|
||||
if (lastSent != null && (now - lastSent) < MESSAGE_COOLDOWN_MS) {
|
||||
return; // Still on cooldown
|
||||
}
|
||||
|
||||
// Update cooldown and send message
|
||||
playerCooldowns.put(category, now);
|
||||
SystemMessageManager.sendRestriction(player, category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up cooldowns for a player (call when they disconnect).
|
||||
*/
|
||||
public static void clearCooldowns(UUID playerId) {
|
||||
messageCooldowns.remove(playerId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package com.tiedup.remake.events.restriction;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.entities.EntityLaborGuard;
|
||||
import com.tiedup.remake.prison.LaborRecord;
|
||||
import com.tiedup.remake.prison.PrisonerManager;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.ChestBlock;
|
||||
import net.minecraft.world.level.block.ShulkerBoxBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraftforge.event.entity.item.ItemTossEvent;
|
||||
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
/**
|
||||
* Event handler for labor tool protection.
|
||||
*
|
||||
* Prevents prisoners from:
|
||||
* - Dropping labor tools
|
||||
* - Storing labor tools in containers
|
||||
*
|
||||
* Note: Death handling removed - if prisoner dies, they're no longer working anyway.
|
||||
*
|
||||
* This is extracted from CampLaborEventHandler after the refactoring to remove event-driven task tracking.
|
||||
* Only the essential protection mechanisms remain.
|
||||
*/
|
||||
@Mod.EventBusSubscriber(modid = TiedUpMod.MOD_ID)
|
||||
public class LaborToolProtectionHandler {
|
||||
|
||||
private static final Map<UUID, Long> lastDropWarningTick = new HashMap<>();
|
||||
private static final long DROP_WARNING_COOLDOWN = 60; // 3 seconds
|
||||
|
||||
/** Remove player entry on disconnect to prevent memory leak. */
|
||||
public static void cleanupPlayer(java.util.UUID playerId) {
|
||||
lastDropWarningTick.remove(playerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent dropping labor tools.
|
||||
* Forge removes the item from inventory before firing ItemTossEvent,
|
||||
* so we must restore it after canceling.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onItemToss(ItemTossEvent event) {
|
||||
ItemStack stack = event.getEntity().getItem();
|
||||
if (!isLaborTool(stack)) return;
|
||||
|
||||
// Cancel + restore (Forge removes from inventory before event fires)
|
||||
event.setCanceled(true);
|
||||
event.getPlayer().getInventory().add(stack);
|
||||
|
||||
Player player = event.getPlayer();
|
||||
if (!(player instanceof ServerPlayer serverPlayer)) return;
|
||||
if (!(player.level() instanceof ServerLevel level)) return;
|
||||
|
||||
// Cooldown
|
||||
long tick = level.getGameTime();
|
||||
Long last = lastDropWarningTick.get(player.getUUID());
|
||||
if (last != null && (tick - last) < DROP_WARNING_COOLDOWN) return;
|
||||
lastDropWarningTick.put(player.getUUID(), tick);
|
||||
|
||||
// Guard reaction
|
||||
EntityLaborGuard guard = findGuardForPlayer(level, player.getUUID());
|
||||
if (guard != null && guard.isAlive()) {
|
||||
guard.getLookControl().setLookAt(player);
|
||||
guard.guardSay(
|
||||
serverPlayer,
|
||||
"guard.labor.drop_tool",
|
||||
"Stupid! Don't drop your tools!"
|
||||
);
|
||||
} else {
|
||||
player.displayClientMessage(
|
||||
Component.literal("You cannot drop labor tools!"),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent storing labor tools in containers.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onRightClickBlock(
|
||||
PlayerInteractEvent.RightClickBlock event
|
||||
) {
|
||||
Player player = event.getEntity();
|
||||
ItemStack held = player.getMainHandItem();
|
||||
if (!isLaborTool(held)) return;
|
||||
|
||||
BlockState state = event.getLevel().getBlockState(event.getPos());
|
||||
if (
|
||||
!(state.getBlock() instanceof ChestBlock) &&
|
||||
!(state.getBlock() instanceof ShulkerBoxBlock)
|
||||
) return;
|
||||
|
||||
event.setCanceled(true);
|
||||
|
||||
if (
|
||||
player instanceof ServerPlayer sp &&
|
||||
player.level() instanceof ServerLevel sl
|
||||
) {
|
||||
// Reuse same cooldown
|
||||
long tick = sl.getGameTime();
|
||||
Long last = lastDropWarningTick.get(player.getUUID());
|
||||
if (last != null && (tick - last) < DROP_WARNING_COOLDOWN) return;
|
||||
lastDropWarningTick.put(player.getUUID(), tick);
|
||||
|
||||
EntityLaborGuard guard = findGuardForPlayer(sl, player.getUUID());
|
||||
if (guard != null && guard.isAlive()) {
|
||||
guard.getLookControl().setLookAt(player);
|
||||
guard.guardSay(
|
||||
sp,
|
||||
"guard.labor.hide_tool",
|
||||
"Don't try to hide your tools!"
|
||||
);
|
||||
} else {
|
||||
player.displayClientMessage(
|
||||
Component.literal("You cannot store labor tools!"),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static EntityLaborGuard findGuardForPlayer(
|
||||
ServerLevel level,
|
||||
UUID playerUUID
|
||||
) {
|
||||
PrisonerManager manager = PrisonerManager.get(level);
|
||||
LaborRecord labor = manager.getLaborRecord(playerUUID);
|
||||
UUID guardId = labor.getGuardId();
|
||||
if (guardId == null) return null;
|
||||
net.minecraft.world.entity.Entity entity = level.getEntity(guardId);
|
||||
if (entity instanceof EntityLaborGuard guard) return guard;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an item is a labor tool (tagged as LaborTool).
|
||||
*/
|
||||
private static boolean isLaborTool(ItemStack stack) {
|
||||
return stack.hasTag() && stack.getTag().getBoolean("LaborTool");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
package com.tiedup.remake.events.restriction;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.entities.EntityMaster;
|
||||
import com.tiedup.remake.items.ItemChokeCollar;
|
||||
import com.tiedup.remake.state.PlayerBindState;
|
||||
import com.tiedup.remake.v2.blocks.PetBedManager;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.Style;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.effect.MobEffectInstance;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.BedBlock;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraftforge.event.TickEvent;
|
||||
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
|
||||
import net.minecraftforge.event.entity.player.PlayerSleepInBedEvent;
|
||||
import net.minecraftforge.eventbus.api.Event;
|
||||
import net.minecraftforge.eventbus.api.EventPriority;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
/**
|
||||
* Event handler for pet play restrictions.
|
||||
*
|
||||
* When a player has a pet play collar (from EntityMaster):
|
||||
* - Cannot eat food from hand (must use Bowl block)
|
||||
* - Cannot sleep in normal beds (must use Pet Bed block)
|
||||
*
|
||||
* Placeholder blocks:
|
||||
* - Bowl: Cauldron (shift+right-click with food to eat)
|
||||
* - Pet Bed: White carpet (shift+right-click to sleep)
|
||||
*/
|
||||
@Mod.EventBusSubscriber(
|
||||
modid = TiedUpMod.MOD_ID,
|
||||
bus = Mod.EventBusSubscriber.Bus.FORGE
|
||||
)
|
||||
public class PetPlayRestrictionHandler {
|
||||
|
||||
/** Message cooldown in milliseconds */
|
||||
private static final long MESSAGE_COOLDOWN_MS = 3000;
|
||||
|
||||
/** Last message time per player */
|
||||
private static final java.util.Map<java.util.UUID, Long> lastMessageTime =
|
||||
new java.util.HashMap<>();
|
||||
|
||||
// ========================================
|
||||
// EATING RESTRICTION
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Prevent pet play players from eating food from hand.
|
||||
* They must use a Bowl block (cauldron placeholder).
|
||||
*/
|
||||
@SubscribeEvent(priority = EventPriority.HIGH)
|
||||
public static void onRightClickItem(
|
||||
PlayerInteractEvent.RightClickItem event
|
||||
) {
|
||||
Player player = event.getEntity();
|
||||
if (player.level().isClientSide) return;
|
||||
|
||||
// Check for pet play collar
|
||||
if (!EntityMaster.hasPetCollar(player)) return;
|
||||
|
||||
ItemStack heldItem = event.getItemStack();
|
||||
|
||||
// Check if trying to eat food
|
||||
if (heldItem.isEdible()) {
|
||||
event.setCanceled(true);
|
||||
event.setCancellationResult(
|
||||
net.minecraft.world.InteractionResult.FAIL
|
||||
);
|
||||
|
||||
sendThrottledMessage(
|
||||
player,
|
||||
"You cannot eat from your hand! Use a bowl."
|
||||
);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PetPlayRestrictionHandler] Blocked {} from eating food directly",
|
||||
player.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow eating from Bowl block (cauldron placeholder).
|
||||
* Shift+right-click with food on cauldron to eat.
|
||||
*/
|
||||
@SubscribeEvent(priority = EventPriority.HIGH)
|
||||
public static void onRightClickBlock(
|
||||
PlayerInteractEvent.RightClickBlock event
|
||||
) {
|
||||
Player player = event.getEntity();
|
||||
if (player.level().isClientSide) return;
|
||||
|
||||
// Check for pet play collar
|
||||
if (!EntityMaster.hasPetCollar(player)) return;
|
||||
|
||||
BlockPos pos = event.getPos();
|
||||
BlockState state = player.level().getBlockState(pos);
|
||||
ItemStack heldItem = event.getItemStack();
|
||||
|
||||
// Bowl interaction (cauldron placeholder)
|
||||
if (
|
||||
state.getBlock() == Blocks.CAULDRON &&
|
||||
player.isShiftKeyDown() &&
|
||||
heldItem.isEdible()
|
||||
) {
|
||||
// Allow eating from bowl
|
||||
// Consume the food
|
||||
if (player instanceof ServerPlayer serverPlayer) {
|
||||
net.minecraft.world.food.FoodProperties food = heldItem
|
||||
.getItem()
|
||||
.getFoodProperties();
|
||||
if (food != null) {
|
||||
serverPlayer
|
||||
.getFoodData()
|
||||
.eat(food.getNutrition(), food.getSaturationModifier());
|
||||
|
||||
// Shrink the item
|
||||
heldItem.shrink(1);
|
||||
|
||||
// Play eating sound
|
||||
player
|
||||
.level()
|
||||
.playSound(
|
||||
null,
|
||||
pos,
|
||||
net.minecraft.sounds.SoundEvents.GENERIC_EAT,
|
||||
net.minecraft.sounds.SoundSource.PLAYERS,
|
||||
1.0f,
|
||||
1.0f
|
||||
);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PetPlayRestrictionHandler] {} ate from bowl",
|
||||
player.getName().getString()
|
||||
);
|
||||
|
||||
// Cancel to prevent normal cauldron interaction
|
||||
event.setCanceled(true);
|
||||
event.setCancellationResult(
|
||||
net.minecraft.world.InteractionResult.SUCCESS
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pet bed interaction (carpet placeholder) - handled in sleep event
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// SLEEPING RESTRICTION
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Prevent pet play players from sleeping in normal beds.
|
||||
* They must use a Pet Bed block (carpet placeholder).
|
||||
*/
|
||||
@SubscribeEvent(priority = EventPriority.HIGH)
|
||||
public static void onSleepInBed(PlayerSleepInBedEvent event) {
|
||||
Player player = event.getEntity();
|
||||
if (player.level().isClientSide) return;
|
||||
|
||||
// Check for pet play collar
|
||||
if (!EntityMaster.hasPetCollar(player)) return;
|
||||
|
||||
// Block sleeping in beds
|
||||
BlockState state = player.level().getBlockState(event.getPos());
|
||||
if (state.getBlock() instanceof BedBlock) {
|
||||
event.setResult(Player.BedSleepingProblem.OTHER_PROBLEM);
|
||||
|
||||
sendThrottledMessage(
|
||||
player,
|
||||
"You cannot sleep in a bed! Use your pet bed."
|
||||
);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PetPlayRestrictionHandler] Blocked {} from sleeping in bed",
|
||||
player.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow sleeping on Pet Bed block (carpet placeholder).
|
||||
* Shift+right-click on white carpet to sleep.
|
||||
*/
|
||||
@SubscribeEvent(priority = EventPriority.NORMAL)
|
||||
public static void onInteractForSleep(
|
||||
PlayerInteractEvent.RightClickBlock event
|
||||
) {
|
||||
Player player = event.getEntity();
|
||||
if (player.level().isClientSide) return;
|
||||
|
||||
// Check for pet play collar
|
||||
if (!EntityMaster.hasPetCollar(player)) return;
|
||||
|
||||
BlockPos pos = event.getPos();
|
||||
BlockState state = player.level().getBlockState(pos);
|
||||
|
||||
// Pet bed interaction (white carpet placeholder)
|
||||
if (
|
||||
state.getBlock() == Blocks.WHITE_CARPET && player.isShiftKeyDown()
|
||||
) {
|
||||
if (player instanceof ServerPlayer serverPlayer) {
|
||||
// Try to sleep
|
||||
Player.BedSleepingProblem problem = canSleepNow(serverPlayer);
|
||||
|
||||
if (problem == null) {
|
||||
// Set spawn point to pet bed location
|
||||
serverPlayer.setRespawnPosition(
|
||||
serverPlayer.level().dimension(),
|
||||
pos,
|
||||
serverPlayer.getYRot(),
|
||||
false,
|
||||
true
|
||||
);
|
||||
|
||||
// Start sleeping (simplified - real implementation would need proper sleep mechanics)
|
||||
serverPlayer.sendSystemMessage(
|
||||
Component.literal(
|
||||
"You curl up in your pet bed..."
|
||||
).withStyle(Style.EMPTY.withColor(0x888888))
|
||||
);
|
||||
|
||||
// Apply sleep effects (heal, skip night handled elsewhere)
|
||||
serverPlayer.heal(2.0f);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PetPlayRestrictionHandler] {} slept in pet bed",
|
||||
player.getName().getString()
|
||||
);
|
||||
|
||||
event.setCanceled(true);
|
||||
event.setCancellationResult(
|
||||
net.minecraft.world.InteractionResult.SUCCESS
|
||||
);
|
||||
} else {
|
||||
sendThrottledMessage(
|
||||
player,
|
||||
"You can only sleep at night or during thunderstorms."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if player can sleep now (time/weather check).
|
||||
*/
|
||||
private static Player.BedSleepingProblem canSleepNow(ServerPlayer player) {
|
||||
// Check time of day
|
||||
if (player.level().isDay() && !player.level().isThundering()) {
|
||||
return Player.BedSleepingProblem.NOT_POSSIBLE_NOW;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// CHOKE COLLAR EFFECT
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Apply choke collar effect on player tick.
|
||||
* When choking is active, rapidly reduces air supply to cause drowning damage.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onPlayerTick(TickEvent.PlayerTickEvent event) {
|
||||
// Only process on server side, at END phase
|
||||
if (event.phase != TickEvent.Phase.END) return;
|
||||
if (event.player.level().isClientSide) return;
|
||||
if (!(event.player instanceof ServerPlayer player)) return;
|
||||
|
||||
// Check pet bed sit cancellation (movement detection)
|
||||
PetBedManager.tickPlayer(player);
|
||||
|
||||
// Check pet cage validity
|
||||
com.tiedup.remake.v2.blocks.PetCageManager.tickPlayer(player);
|
||||
|
||||
// Get player's collar
|
||||
PlayerBindState bindState = PlayerBindState.getInstance(player);
|
||||
if (bindState == null || !bindState.hasCollar()) return;
|
||||
|
||||
ItemStack collar = bindState.getEquipment(BodyRegionV2.NECK);
|
||||
if (collar.getItem() instanceof ItemChokeCollar chokeCollar) {
|
||||
if (chokeCollar.isChoking(collar)) {
|
||||
// Apply ChokeEffect (short duration, re-applied each active tick)
|
||||
if (
|
||||
!player.hasEffect(
|
||||
com.tiedup.remake.core.ModEffects.CHOKE.get()
|
||||
)
|
||||
) {
|
||||
player.addEffect(
|
||||
new MobEffectInstance(
|
||||
com.tiedup.remake.core.ModEffects.CHOKE.get(),
|
||||
40,
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Remove effect if choke is deactivated
|
||||
player.removeEffect(
|
||||
com.tiedup.remake.core.ModEffects.CHOKE.get()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// UTILITY
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* BUG FIX: Clean up player data to prevent memory leak.
|
||||
* Called on player logout.
|
||||
*/
|
||||
public static void clearPlayer(java.util.UUID playerId) {
|
||||
lastMessageTime.remove(playerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message with cooldown to prevent spam.
|
||||
*/
|
||||
private static void sendThrottledMessage(Player player, String message) {
|
||||
long now = System.currentTimeMillis();
|
||||
Long lastTime = lastMessageTime.get(player.getUUID());
|
||||
|
||||
if (lastTime == null || now - lastTime > MESSAGE_COOLDOWN_MS) {
|
||||
lastMessageTime.put(player.getUUID(), now);
|
||||
player.sendSystemMessage(
|
||||
Component.literal(message).withStyle(
|
||||
Style.EMPTY.withColor(EntityMaster.MASTER_NAME_COLOR)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,672 @@
|
||||
package com.tiedup.remake.events.restriction;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.entities.EntityKidnapper;
|
||||
import com.tiedup.remake.items.base.ItemCollar;
|
||||
import com.tiedup.remake.minigame.StruggleSessionManager;
|
||||
import com.tiedup.remake.network.ModNetwork;
|
||||
import com.tiedup.remake.network.personality.PacketSlaveBeingFreed;
|
||||
import com.tiedup.remake.state.IBondageState;
|
||||
import com.tiedup.remake.state.PlayerBindState;
|
||||
import com.tiedup.remake.tasks.ForceFeedingTask;
|
||||
import com.tiedup.remake.tasks.TimedInteractTask;
|
||||
import com.tiedup.remake.tasks.UntyingPlayerTask;
|
||||
import com.tiedup.remake.tasks.UntyingTask;
|
||||
import com.tiedup.remake.util.GameConstants;
|
||||
import com.tiedup.remake.util.KidnappedHelper;
|
||||
import com.tiedup.remake.core.SettingsAccessor;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
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.event.TickEvent;
|
||||
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
/**
|
||||
* Tick handler for restraint-related tasks (untying, tying, force feeding).
|
||||
*
|
||||
* Manages progress-based interaction tasks that span multiple ticks:
|
||||
* - Untying mechanic (empty hand right-click on tied entity)
|
||||
* - Tying mechanic (tick progression)
|
||||
* - Force feeding mechanic (food right-click on gagged entity)
|
||||
* - Auto-shock collar checks
|
||||
* - Struggle auto-stop (legacy QTE fallback)
|
||||
*
|
||||
* @see BondageItemRestrictionHandler for movement, interaction, and eating restrictions
|
||||
*/
|
||||
@Mod.EventBusSubscriber(
|
||||
modid = TiedUpMod.MOD_ID,
|
||||
bus = Mod.EventBusSubscriber.Bus.FORGE
|
||||
)
|
||||
public class RestraintTaskTickHandler {
|
||||
|
||||
// ========== PLAYER-SPECIFIC TICK ==========
|
||||
|
||||
/**
|
||||
* Handle player tick event for player-specific features.
|
||||
* - Auto-shock collar check (throttled to every N ticks)
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onPlayerTick(TickEvent.PlayerTickEvent event) {
|
||||
if (event.side.isClient() || event.phase != TickEvent.Phase.END) {
|
||||
return;
|
||||
}
|
||||
|
||||
Player player = event.player;
|
||||
PlayerBindState playerState = PlayerBindState.getInstance(player);
|
||||
|
||||
// Check if struggle animation should stop
|
||||
// For continuous struggle: animation is managed by MiniGameSessionManager
|
||||
// Only auto-stop if NO active continuous session (legacy QTE fallback)
|
||||
if (playerState != null && playerState.isStruggling()) {
|
||||
// Don't auto-stop if there's an active continuous struggle session
|
||||
StruggleSessionManager mgr = StruggleSessionManager.getInstance();
|
||||
if (mgr.getContinuousStruggleSession(player.getUUID()) == null) {
|
||||
// Legacy behavior: stop after 80 ticks (no active continuous session)
|
||||
if (
|
||||
playerState.shouldStopStruggling(
|
||||
player.level().getGameTime()
|
||||
)
|
||||
) {
|
||||
playerState.setStruggling(false, 0);
|
||||
com.tiedup.remake.network.sync.SyncManager.syncStruggleState(
|
||||
player
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process untying task tick (progress-based system)
|
||||
// tick() increments/decrements progress based on whether update() was called this tick
|
||||
// sendProgressPackets() updates the UI for both players
|
||||
if (playerState != null) {
|
||||
com.tiedup.remake.tasks.UntyingTask currentUntyingTask =
|
||||
playerState.getCurrentUntyingTask();
|
||||
if (currentUntyingTask != null && !currentUntyingTask.isStopped()) {
|
||||
// AUTO-UPDATE: Check if player is still targeting the same entity
|
||||
// This allows "hold click" behavior without needing repeated interactLivingEntity calls
|
||||
if (
|
||||
currentUntyingTask instanceof
|
||||
com.tiedup.remake.tasks.UntyingPlayerTask untyingPlayerTask
|
||||
) {
|
||||
net.minecraft.world.entity.LivingEntity target =
|
||||
untyingPlayerTask.getTargetEntity();
|
||||
if (target != null && target.isAlive()) {
|
||||
// Check if player is looking at target and close enough
|
||||
double distance = player.distanceTo(target);
|
||||
boolean isLookingAtTarget = isPlayerLookingAtEntity(
|
||||
player,
|
||||
target,
|
||||
4.0
|
||||
);
|
||||
|
||||
if (
|
||||
distance <= 4.0 &&
|
||||
isLookingAtTarget &&
|
||||
player.hasLineOfSight(target)
|
||||
) {
|
||||
// Player is still targeting - auto-update the task
|
||||
currentUntyingTask.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process tick (increment if active, decrement if not)
|
||||
currentUntyingTask.tick();
|
||||
// Send progress packets to update UI
|
||||
currentUntyingTask.sendProgressPackets();
|
||||
|
||||
// Check if task stopped (completed or cancelled due to no progress)
|
||||
if (currentUntyingTask.isStopped()) {
|
||||
playerState.setCurrentUntyingTask(null);
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[RESTRAINT] {} untying task ended (tick update)",
|
||||
player.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Process tying task tick (same progress-based system)
|
||||
com.tiedup.remake.tasks.TyingTask currentTyingTask =
|
||||
playerState.getCurrentTyingTask();
|
||||
if (currentTyingTask != null && !currentTyingTask.isStopped()) {
|
||||
// AUTO-UPDATE: Check if player is still targeting the same entity
|
||||
// This allows "hold click" behavior without needing repeated interactLivingEntity calls
|
||||
if (
|
||||
currentTyingTask instanceof
|
||||
com.tiedup.remake.tasks.TyingPlayerTask tyingPlayerTask
|
||||
) {
|
||||
net.minecraft.world.entity.LivingEntity target =
|
||||
tyingPlayerTask.getTargetEntity();
|
||||
boolean isSelfTying =
|
||||
target != null && target.equals(player);
|
||||
|
||||
if (isSelfTying) {
|
||||
// Self-tying: skip look-at/distance checks (player can't raycast to own hitbox)
|
||||
// Progress is driven by continuous PacketSelfBondage packets from client
|
||||
currentTyingTask.update();
|
||||
} else if (target != null && target.isAlive()) {
|
||||
// Tying another player: check distance + line of sight
|
||||
double distance = player.distanceTo(target);
|
||||
boolean isLookingAtTarget = isPlayerLookingAtEntity(
|
||||
player,
|
||||
target,
|
||||
4.0
|
||||
);
|
||||
|
||||
if (
|
||||
distance <= 4.0 &&
|
||||
isLookingAtTarget &&
|
||||
player.hasLineOfSight(target)
|
||||
) {
|
||||
currentTyingTask.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process tick (increment if active, decrement if not)
|
||||
currentTyingTask.tick();
|
||||
// Send progress packets to update UI
|
||||
currentTyingTask.sendProgressPackets();
|
||||
|
||||
// Check if task stopped (completed or cancelled due to no progress)
|
||||
if (currentTyingTask.isStopped()) {
|
||||
playerState.setCurrentTyingTask(null);
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[RESTRAINT] {} tying task ended (tick update)",
|
||||
player.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Process force feeding task tick
|
||||
TimedInteractTask feedingTask = playerState.getCurrentFeedingTask();
|
||||
if (feedingTask != null && !feedingTask.isStopped()) {
|
||||
LivingEntity target = feedingTask.getTargetEntity();
|
||||
if (target != null && target.isAlive()) {
|
||||
double distance = player.distanceTo(target);
|
||||
boolean isLookingAtTarget = isPlayerLookingAtEntity(
|
||||
player,
|
||||
target,
|
||||
4.0
|
||||
);
|
||||
|
||||
if (
|
||||
distance <= 4.0 &&
|
||||
isLookingAtTarget &&
|
||||
player.hasLineOfSight(target)
|
||||
) {
|
||||
feedingTask.update();
|
||||
}
|
||||
}
|
||||
|
||||
feedingTask.tick();
|
||||
feedingTask.sendProgressPackets();
|
||||
|
||||
if (feedingTask.isStopped()) {
|
||||
playerState.setCurrentFeedingTask(null);
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[RESTRAINT] {} feeding task ended (tick update)",
|
||||
player.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Throttle: only check every N ticks (configurable via GameConstants) - per-player timing
|
||||
if (player.tickCount % GameConstants.SHOCK_COLLAR_CHECK_INTERVAL != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Phase 13: Auto-shock collar logic (Player-specific feature)
|
||||
if (playerState != null) {
|
||||
playerState.checkAutoShockCollar();
|
||||
}
|
||||
}
|
||||
|
||||
// ========== UNTYING MECHANIC ==========
|
||||
|
||||
/**
|
||||
* Handle untying a tied entity (right-click with empty hand).
|
||||
*
|
||||
* Based on original PlayerKidnapActionsHandler.onUntyingTarget() (1.12.2)
|
||||
*
|
||||
* When a player right-clicks a tied entity (player or NPC) with an empty hand,
|
||||
* starts or continues an untying task to free them.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onUntyingTarget(
|
||||
PlayerInteractEvent.EntityInteract event
|
||||
) {
|
||||
// Only run on server side
|
||||
if (event.getLevel().isClientSide) {
|
||||
return;
|
||||
}
|
||||
|
||||
Entity target = event.getTarget();
|
||||
Player helper = event.getEntity();
|
||||
|
||||
// Must be targeting a LivingEntity, using main hand, and have empty hand
|
||||
if (
|
||||
!(target instanceof LivingEntity targetEntity) ||
|
||||
event.getHand() != InteractionHand.MAIN_HAND ||
|
||||
!helper.getMainHandItem().isEmpty()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// MCA villagers require Shift+click to untie (prevents conflict with MCA menu)
|
||||
if (
|
||||
com.tiedup.remake.compat.mca.MCACompat.isMCALoaded() &&
|
||||
com.tiedup.remake.compat.mca.MCACompat.isMCAVillager(
|
||||
targetEntity
|
||||
) &&
|
||||
!helper.isShiftKeyDown()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if target is tied using IBondageState interface
|
||||
IBondageState targetState = KidnappedHelper.getKidnappedState(
|
||||
targetEntity
|
||||
);
|
||||
if (targetState == null || !targetState.isTiedUp()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// SECURITY: Distance and line-of-sight validation
|
||||
// ========================================
|
||||
double maxUntieDistance = 4.0; // Max distance to untie (blocks)
|
||||
double distance = helper.distanceTo(targetEntity);
|
||||
if (distance > maxUntieDistance) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[RESTRAINT] {} tried to untie {} from too far away ({} blocks)",
|
||||
helper.getName().getString(),
|
||||
targetEntity.getName().getString(),
|
||||
String.format("%.1f", distance)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check line-of-sight (helper must be able to see target)
|
||||
if (!helper.hasLineOfSight(targetEntity)) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[RESTRAINT] {} tried to untie {} without line of sight",
|
||||
helper.getName().getString(),
|
||||
targetEntity.getName().getString()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for Kidnapper fight back - block untying if Kidnapper is nearby
|
||||
if (targetEntity instanceof com.tiedup.remake.entities.AbstractTiedUpNpc npc) {
|
||||
if (
|
||||
npc.getCaptor() instanceof EntityKidnapper kidnapper &&
|
||||
kidnapper.isAlive()
|
||||
) {
|
||||
double distanceToKidnapper = helper.distanceTo(kidnapper);
|
||||
double fightBackRange = 16.0; // Kidnapper notices within 16 blocks
|
||||
|
||||
if (distanceToKidnapper <= fightBackRange) {
|
||||
// Trigger Kidnapper fight back by setting helper as "attacker"
|
||||
// This activates KidnapperFightBackGoal which handles pursuit and attack
|
||||
kidnapper.setLastAttacker(helper);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[RESTRAINT] {} tried to untie {}, {} fights back!",
|
||||
helper.getName().getString(),
|
||||
npc.getName().getString(),
|
||||
kidnapper.getName().getString()
|
||||
);
|
||||
|
||||
// Block untying - send message to player
|
||||
helper.displayClientMessage(
|
||||
Component.translatable(
|
||||
"tiedup.message.kidnapper_guards_captive"
|
||||
),
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Kidnapper fight back - block untying if player is captive or job worker
|
||||
if (targetEntity instanceof Player targetPlayer) {
|
||||
List<EntityKidnapper> nearbyKidnappers = helper
|
||||
.level()
|
||||
.getEntitiesOfClass(
|
||||
EntityKidnapper.class,
|
||||
helper.getBoundingBox().inflate(16.0)
|
||||
);
|
||||
|
||||
for (EntityKidnapper kidnapper : nearbyKidnappers) {
|
||||
if (!kidnapper.isAlive()) continue;
|
||||
|
||||
// Check if player is kidnapper's current captive (held by leash)
|
||||
IBondageState captive = kidnapper.getCaptive();
|
||||
boolean isCaptive =
|
||||
captive != null && captive.asLivingEntity() == targetPlayer;
|
||||
|
||||
// Check if player is kidnapper's job worker
|
||||
UUID workerUUID = kidnapper.getJobWorkerUUID();
|
||||
boolean isJobWorker =
|
||||
workerUUID != null &&
|
||||
workerUUID.equals(targetPlayer.getUUID());
|
||||
|
||||
if (isCaptive || isJobWorker) {
|
||||
// Trigger Kidnapper fight back
|
||||
kidnapper.setLastAttacker(helper);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[RESTRAINT] {} tried to untie {} (captive={}, worker={}), {} fights back!",
|
||||
helper.getName().getString(),
|
||||
targetPlayer.getName().getString(),
|
||||
isCaptive,
|
||||
isJobWorker,
|
||||
kidnapper.getNpcName()
|
||||
);
|
||||
|
||||
// Block untying - send message to player
|
||||
helper.displayClientMessage(
|
||||
Component.translatable(
|
||||
"tiedup.message.kidnapper_guards_captive"
|
||||
),
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if helper is tied using IBondageState interface
|
||||
IBondageState helperKidnappedState = KidnappedHelper.getKidnappedState(
|
||||
helper
|
||||
);
|
||||
if (helperKidnappedState == null || helperKidnappedState.isTiedUp()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get PlayerBindState for task management (helper only)
|
||||
PlayerBindState helperState = PlayerBindState.getInstance(helper);
|
||||
if (helperState == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Block untying while force feeding
|
||||
TimedInteractTask activeFeedTask = helperState.getCurrentFeedingTask();
|
||||
if (activeFeedTask != null && !activeFeedTask.isStopped()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get untying duration (default: 10 seconds)
|
||||
int untyingSeconds = getUntyingDuration(helper);
|
||||
|
||||
// Phase 11: Check collar ownership for TiedUp NPCs
|
||||
// Non-owners take 3x longer and trigger alert to owners
|
||||
if (targetEntity instanceof com.tiedup.remake.entities.AbstractTiedUpNpc npc) {
|
||||
if (!npc.isCollarOwner(helper)) {
|
||||
// Non-owner: triple the untying time
|
||||
untyingSeconds *= 3;
|
||||
|
||||
// Alert all collar owners
|
||||
alertCollarOwners(npc, helper);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[RESTRAINT] Non-owner {} trying to free {} ({}s)",
|
||||
helper.getName().getString(),
|
||||
npc.getNpcName(),
|
||||
untyingSeconds
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Get current untying task (if any)
|
||||
UntyingTask currentTask = helperState.getCurrentUntyingTask();
|
||||
|
||||
// Check if we should start a new task or continue existing one
|
||||
if (
|
||||
currentTask == null ||
|
||||
!currentTask.isSameTarget(targetEntity) ||
|
||||
currentTask.isStopped()
|
||||
) {
|
||||
// Create new untying task (unified for Players and NPCs)
|
||||
UntyingPlayerTask newTask = new UntyingPlayerTask(
|
||||
targetState,
|
||||
targetEntity,
|
||||
untyingSeconds,
|
||||
helper.level(),
|
||||
helper
|
||||
);
|
||||
|
||||
// Start new task
|
||||
helperState.setCurrentUntyingTask(newTask);
|
||||
newTask.setUpTargetState(); // Initialize target's restraint state
|
||||
newTask.start();
|
||||
currentTask = newTask;
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[RESTRAINT] {} started untying {} ({} seconds)",
|
||||
helper.getName().getString(),
|
||||
targetEntity.getName().getString(),
|
||||
untyingSeconds
|
||||
);
|
||||
} else {
|
||||
// Continue existing task - ensure helper is set
|
||||
if (currentTask instanceof UntyingPlayerTask playerTask) {
|
||||
playerTask.setHelper(helper);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark this tick as active (progress will increase in onPlayerTick)
|
||||
// The tick() method in onPlayerTick handles progress increment/decrement
|
||||
currentTask.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a player is looking at a specific entity (raycast).
|
||||
*
|
||||
* @param player The player
|
||||
* @param target The target entity
|
||||
* @param maxDistance Maximum distance to check
|
||||
* @return true if player is looking at the target entity
|
||||
*/
|
||||
private static boolean isPlayerLookingAtEntity(
|
||||
Player player,
|
||||
net.minecraft.world.entity.LivingEntity target,
|
||||
double maxDistance
|
||||
) {
|
||||
// Get player's look vector
|
||||
net.minecraft.world.phys.Vec3 eyePos = player.getEyePosition(1.0F);
|
||||
net.minecraft.world.phys.Vec3 lookVec = player.getLookAngle();
|
||||
net.minecraft.world.phys.Vec3 endPos = eyePos.add(
|
||||
lookVec.x * maxDistance,
|
||||
lookVec.y * maxDistance,
|
||||
lookVec.z * maxDistance
|
||||
);
|
||||
|
||||
// Check if raycast hits the target entity
|
||||
net.minecraft.world.phys.AABB targetBounds = target
|
||||
.getBoundingBox()
|
||||
.inflate(0.3);
|
||||
java.util.Optional<net.minecraft.world.phys.Vec3> hit =
|
||||
targetBounds.clip(eyePos, endPos);
|
||||
|
||||
return hit.isPresent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the untying duration in seconds from GameRule.
|
||||
*
|
||||
* @param player The player (for accessing world/GameRules)
|
||||
* @return Duration in seconds (default: 10)
|
||||
*/
|
||||
private static int getUntyingDuration(Player player) {
|
||||
return SettingsAccessor.getUntyingPlayerTime(player.level().getGameRules());
|
||||
}
|
||||
|
||||
// ========== FORCE FEEDING MECHANIC ==========
|
||||
|
||||
/**
|
||||
* Handle force feeding a gagged entity (right-click with food).
|
||||
*
|
||||
* When a player right-clicks a gagged entity (player or NPC) while holding food,
|
||||
* starts or continues a force feeding task.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onForceFeedingTarget(
|
||||
PlayerInteractEvent.EntityInteract event
|
||||
) {
|
||||
if (event.getLevel().isClientSide) {
|
||||
return;
|
||||
}
|
||||
|
||||
Entity target = event.getTarget();
|
||||
Player feeder = event.getEntity();
|
||||
|
||||
if (
|
||||
!(target instanceof LivingEntity targetEntity) ||
|
||||
event.getHand() != InteractionHand.MAIN_HAND
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
ItemStack heldItem = feeder.getMainHandItem();
|
||||
if (heldItem.isEmpty() || !heldItem.getItem().isEdible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Target must have IBondageState state and be gagged
|
||||
IBondageState targetState = KidnappedHelper.getKidnappedState(
|
||||
targetEntity
|
||||
);
|
||||
if (targetState == null || !targetState.isGagged()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Feeder must not be tied up
|
||||
IBondageState feederState = KidnappedHelper.getKidnappedState(feeder);
|
||||
if (feederState != null && feederState.isTiedUp()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Distance and line-of-sight validation
|
||||
double distance = feeder.distanceTo(targetEntity);
|
||||
if (distance > 4.0) {
|
||||
return;
|
||||
}
|
||||
if (!feeder.hasLineOfSight(targetEntity)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get feeder's PlayerBindState for task management
|
||||
PlayerBindState feederBindState = PlayerBindState.getInstance(feeder);
|
||||
if (feederBindState == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Block feeding while untying
|
||||
UntyingTask activeUntieTask = feederBindState.getCurrentUntyingTask();
|
||||
if (activeUntieTask != null && !activeUntieTask.isStopped()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current feeding task (if any)
|
||||
TimedInteractTask currentTask = feederBindState.getCurrentFeedingTask();
|
||||
|
||||
if (
|
||||
currentTask == null ||
|
||||
!currentTask.isSameTarget(targetEntity) ||
|
||||
currentTask.isStopped()
|
||||
) {
|
||||
// Create new force feeding task (5 seconds)
|
||||
ForceFeedingTask newTask = new ForceFeedingTask(
|
||||
targetState,
|
||||
targetEntity,
|
||||
5,
|
||||
feeder.level(),
|
||||
feeder,
|
||||
heldItem,
|
||||
feeder.getInventory().selected
|
||||
);
|
||||
|
||||
feederBindState.setCurrentFeedingTask(newTask);
|
||||
newTask.setUpTargetState();
|
||||
newTask.start();
|
||||
currentTask = newTask;
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[RESTRAINT] {} started force feeding {} (5 seconds)",
|
||||
feeder.getName().getString(),
|
||||
targetEntity.getName().getString()
|
||||
);
|
||||
} else {
|
||||
// Continue existing task - ensure feeder is set
|
||||
if (currentTask instanceof ForceFeedingTask feedTask) {
|
||||
feedTask.setFeeder(feeder);
|
||||
}
|
||||
}
|
||||
|
||||
currentTask.update();
|
||||
|
||||
// Cancel to prevent mobInteract (avoids instant NPC feed)
|
||||
event.setCancellationResult(InteractionResult.SUCCESS);
|
||||
event.setCanceled(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alert all collar owners that someone is trying to free their slave.
|
||||
* Phase 11: Multiplayer protection system
|
||||
*
|
||||
* @param slave The slave being freed
|
||||
* @param liberator The player trying to free them
|
||||
*/
|
||||
private static void alertCollarOwners(
|
||||
com.tiedup.remake.entities.AbstractTiedUpNpc slave,
|
||||
Player liberator
|
||||
) {
|
||||
if (!(slave.level() instanceof ServerLevel serverLevel)) return;
|
||||
|
||||
ItemStack collar = slave.getEquipment(BodyRegionV2.NECK);
|
||||
if (
|
||||
collar.isEmpty() ||
|
||||
!(collar.getItem() instanceof ItemCollar collarItem)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<UUID> owners = collarItem.getOwners(collar);
|
||||
if (owners.isEmpty()) return;
|
||||
|
||||
// Create alert packet
|
||||
PacketSlaveBeingFreed alertPacket = new PacketSlaveBeingFreed(
|
||||
slave.getNpcName(),
|
||||
liberator.getName().getString(),
|
||||
slave.blockPosition().getX(),
|
||||
slave.blockPosition().getY(),
|
||||
slave.blockPosition().getZ()
|
||||
);
|
||||
|
||||
// Send to all online owners
|
||||
for (UUID ownerUUID : owners) {
|
||||
ServerPlayer owner = serverLevel
|
||||
.getServer()
|
||||
.getPlayerList()
|
||||
.getPlayer(ownerUUID);
|
||||
if (owner != null && owner != liberator) {
|
||||
ModNetwork.sendToPlayer(alertPacket, owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.tiedup.remake.events.system;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.items.ItemPadlock;
|
||||
import com.tiedup.remake.items.base.ILockable;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraftforge.event.AnvilUpdateEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
/**
|
||||
* Event handler for anvil-based padlock attachment.
|
||||
*
|
||||
* Phase 20: Padlock via Anvil
|
||||
*
|
||||
* Allows combining a bondage item (ILockable) with a Padlock in the anvil
|
||||
* to make the item "lockable". A lockable item can then be locked with a Key.
|
||||
*
|
||||
* Flow:
|
||||
* - Place ILockable item in left slot
|
||||
* - Place Padlock in right slot
|
||||
* - Output: Same item with lockable=true
|
||||
* - Cost: 1 XP level, consumes 1 padlock
|
||||
*/
|
||||
@Mod.EventBusSubscriber(modid = TiedUpMod.MOD_ID)
|
||||
public class AnvilEventHandler {
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onAnvilUpdate(AnvilUpdateEvent event) {
|
||||
ItemStack left = event.getLeft(); // Bondage item
|
||||
ItemStack right = event.getRight(); // Padlock
|
||||
|
||||
// Skip if either slot is empty
|
||||
if (left.isEmpty() || right.isEmpty()) return;
|
||||
|
||||
// Right slot must be a Padlock
|
||||
if (!(right.getItem() instanceof ItemPadlock)) return;
|
||||
|
||||
// Left slot must be ILockable
|
||||
if (!(left.getItem() instanceof ILockable lockable)) return;
|
||||
|
||||
// Check if item can have a padlock attached (tape, slime, vine, web cannot)
|
||||
if (!lockable.canAttachPadlock()) {
|
||||
return; // Item type cannot have padlock
|
||||
}
|
||||
|
||||
// Item must not already have a padlock attached
|
||||
if (lockable.isLockable(left)) {
|
||||
return; // Already has padlock
|
||||
}
|
||||
|
||||
// Create result: copy of left with lockable=true
|
||||
ItemStack result = left.copy();
|
||||
lockable.setLockable(result, true);
|
||||
|
||||
// Set anvil output
|
||||
event.setOutput(result);
|
||||
event.setCost(1); // 1 XP level cost
|
||||
event.setMaterialCost(1); // Consume 1 padlock
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[AnvilEventHandler] Padlock attachment preview: {} + Padlock",
|
||||
left.getDisplayName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package com.tiedup.remake.events.system;
|
||||
|
||||
import com.tiedup.remake.bounty.Bounty;
|
||||
import com.tiedup.remake.bounty.BountyManager;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.state.IBondageState;
|
||||
import com.tiedup.remake.state.PlayerBindState;
|
||||
import com.tiedup.remake.state.PlayerCaptorManager;
|
||||
import com.tiedup.remake.util.KidnappedHelper;
|
||||
import com.tiedup.remake.core.SettingsAccessor;
|
||||
import java.util.List;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraftforge.event.TickEvent;
|
||||
import net.minecraftforge.event.entity.player.PlayerEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
/**
|
||||
* Handles bounty delivery detection and player login events.
|
||||
*
|
||||
* Phase 17: Bounty System
|
||||
*
|
||||
* Detects when:
|
||||
* - A hunter brings a captive near the bounty client
|
||||
* - A player logs in (to return expired bounty rewards)
|
||||
*/
|
||||
@Mod.EventBusSubscriber(
|
||||
modid = TiedUpMod.MOD_ID,
|
||||
bus = Mod.EventBusSubscriber.Bus.FORGE
|
||||
)
|
||||
public class BountyDeliveryHandler {
|
||||
|
||||
// Check every 30 ticks (1.5 seconds) - optimized for performance
|
||||
private static final int CHECK_INTERVAL = 30;
|
||||
private static int tickCounter = 0;
|
||||
|
||||
/**
|
||||
* Periodically check for bounty deliveries.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onServerTick(TickEvent.ServerTickEvent event) {
|
||||
if (event.phase != TickEvent.Phase.END) return;
|
||||
|
||||
tickCounter++;
|
||||
if (tickCounter < CHECK_INTERVAL) return;
|
||||
tickCounter = 0;
|
||||
|
||||
// Check all players
|
||||
for (ServerPlayer player : event
|
||||
.getServer()
|
||||
.getPlayerList()
|
||||
.getPlayers()) {
|
||||
checkBountyDelivery(player);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this player can deliver any captives to nearby bounty clients.
|
||||
*/
|
||||
private static void checkBountyDelivery(ServerPlayer hunter) {
|
||||
// Get hunter's captor manager
|
||||
PlayerBindState hunterState = PlayerBindState.getInstance(hunter);
|
||||
if (hunterState == null) return;
|
||||
|
||||
PlayerCaptorManager captorManager = hunterState.getCaptorManager();
|
||||
if (captorManager == null || !captorManager.hasCaptives()) return;
|
||||
|
||||
// Get bounty manager
|
||||
BountyManager bountyManager = BountyManager.get(hunter.serverLevel());
|
||||
List<Bounty> bounties = bountyManager.getBounties(hunter.serverLevel());
|
||||
if (bounties.isEmpty()) return;
|
||||
|
||||
// Get delivery radius
|
||||
int radius = SettingsAccessor.getBountyDeliveryRadius(
|
||||
hunter.serverLevel().getGameRules()
|
||||
);
|
||||
double radiusSq = (double) radius * radius;
|
||||
|
||||
// Find nearby players using pre-maintained player list (faster than AABB query)
|
||||
List<ServerPlayer> nearbyPlayers = new java.util.ArrayList<>();
|
||||
for (Player p : hunter.level().players()) {
|
||||
if (
|
||||
p != hunter &&
|
||||
p instanceof ServerPlayer sp &&
|
||||
hunter.distanceToSqr(p) <= radiusSq
|
||||
) {
|
||||
nearbyPlayers.add(sp);
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-filter captives: only ServerPlayers that are tied (O(m) instead of O(n*m))
|
||||
List<ServerPlayer> validCaptives = new java.util.ArrayList<>();
|
||||
for (IBondageState captive : captorManager.getCaptives()) {
|
||||
LivingEntity captiveEntity = captive.asLivingEntity();
|
||||
if (captiveEntity == null) continue;
|
||||
if (
|
||||
!(captiveEntity instanceof ServerPlayer captivePlayer)
|
||||
) continue;
|
||||
IBondageState captiveState = KidnappedHelper.getKidnappedState(
|
||||
captivePlayer
|
||||
);
|
||||
if (captiveState == null || !captiveState.isTiedUp()) continue;
|
||||
validCaptives.add(captivePlayer);
|
||||
}
|
||||
|
||||
if (validCaptives.isEmpty()) return;
|
||||
|
||||
// Check each nearby player for potential delivery
|
||||
for (ServerPlayer potentialClient : nearbyPlayers) {
|
||||
for (ServerPlayer captivePlayer : validCaptives) {
|
||||
// Captive must be near the client
|
||||
if (
|
||||
captivePlayer.distanceTo(potentialClient) > radius
|
||||
) continue;
|
||||
|
||||
// Try to deliver
|
||||
boolean delivered = bountyManager.tryDeliverCaptive(
|
||||
hunter,
|
||||
potentialClient,
|
||||
captivePlayer
|
||||
);
|
||||
|
||||
if (delivered) {
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[BOUNTY] Delivery detected: {} delivered {} to {}",
|
||||
hunter.getName().getString(),
|
||||
captivePlayer.getName().getString(),
|
||||
potentialClient.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle player login - return pending bounty rewards.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent event) {
|
||||
if (event.getEntity() instanceof ServerPlayer player) {
|
||||
BountyManager manager = BountyManager.get(player.serverLevel());
|
||||
manager.onPlayerJoin(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
package com.tiedup.remake.events.system;
|
||||
|
||||
import com.tiedup.remake.blocks.BlockCellCore;
|
||||
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 java.util.UUID;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraftforge.event.TickEvent;
|
||||
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
|
||||
import net.minecraftforge.event.level.BlockEvent;
|
||||
import net.minecraftforge.eventbus.api.EventPriority;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import net.minecraftforge.registries.ForgeRegistries;
|
||||
|
||||
/**
|
||||
* Central event handler for Cell System V2 Phase 2.
|
||||
*
|
||||
* Handles:
|
||||
* - Selection mode (block click capture for Set Spawn/Delivery/Disguise)
|
||||
* - Door control (block prisoners/non-owners from cell doors)
|
||||
* - Breach detection (wall break → BREACHED/COMPROMISED state)
|
||||
* - Breach repair (block placed at breached position → repair)
|
||||
* - Selection timeout/cancel (player tick)
|
||||
*/
|
||||
@Mod.EventBusSubscriber(
|
||||
modid = TiedUpMod.MOD_ID,
|
||||
bus = Mod.EventBusSubscriber.Bus.FORGE
|
||||
)
|
||||
public class CellV2EventHandler {
|
||||
|
||||
// ==================== RIGHT-CLICK BLOCK ====================
|
||||
|
||||
@SubscribeEvent(priority = EventPriority.HIGH)
|
||||
public static void onRightClickBlock(
|
||||
PlayerInteractEvent.RightClickBlock event
|
||||
) {
|
||||
if (event.getLevel().isClientSide()) return;
|
||||
if (!(event.getEntity() instanceof ServerPlayer player)) return;
|
||||
|
||||
UUID playerId = player.getUUID();
|
||||
BlockPos clickedPos = event.getPos();
|
||||
ServerLevel level = player.serverLevel();
|
||||
|
||||
// 1. Check selection mode first
|
||||
CellSelectionManager.SelectionContext selection =
|
||||
CellSelectionManager.getSelection(playerId);
|
||||
if (selection != null) {
|
||||
event.setCanceled(true);
|
||||
handleSelectionClick(player, level, clickedPos, selection);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Check door control
|
||||
handleDoorControl(event, player, level, clickedPos);
|
||||
}
|
||||
|
||||
// ==================== BLOCK BREAK ====================
|
||||
|
||||
@SubscribeEvent(priority = EventPriority.HIGH)
|
||||
public static void onBlockBreak(BlockEvent.BreakEvent event) {
|
||||
if (!(event.getLevel() instanceof ServerLevel level)) return;
|
||||
|
||||
BlockPos pos = event.getPos();
|
||||
Player breaker = event.getPlayer();
|
||||
CellRegistryV2 registry = CellRegistryV2.get(level);
|
||||
|
||||
// Check if broken block is a wall of a V2 cell
|
||||
UUID cellId = registry.getCellIdAtWall(pos);
|
||||
if (cellId == null) return;
|
||||
|
||||
CellDataV2 cell = registry.getCell(cellId);
|
||||
if (cell == null) return;
|
||||
|
||||
// Prisoners cannot break the Cell Core
|
||||
if (level.getBlockState(pos).getBlock() instanceof BlockCellCore) {
|
||||
if (cell.hasPrisoner(breaker.getUUID())) {
|
||||
event.setCanceled(true);
|
||||
breaker.displayClientMessage(
|
||||
Component.translatable(
|
||||
"msg.tiedup.cell_core.cant_break_core"
|
||||
).withStyle(ChatFormatting.RED),
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Record breach
|
||||
registry.addBreach(cellId, pos);
|
||||
|
||||
// State transitions
|
||||
float breachPct = cell.getBreachPercentage();
|
||||
if (breachPct > 0.30f && cell.getState() != CellState.COMPROMISED) {
|
||||
cell.setState(CellState.COMPROMISED);
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[CellV2EventHandler] Cell {} COMPROMISED ({}% breached)",
|
||||
cellId.toString().substring(0, 8),
|
||||
(int) (breachPct * 100)
|
||||
);
|
||||
} else if (cell.getState() == CellState.INTACT) {
|
||||
cell.setState(CellState.BREACHED);
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[CellV2EventHandler] Cell {} BREACHED at {}",
|
||||
cellId.toString().substring(0, 8),
|
||||
pos.toShortString()
|
||||
);
|
||||
}
|
||||
|
||||
// Notify owner (skip if breaker is the owner)
|
||||
if (cell.getOwnerId() != null && !cell.isOwnedBy(breaker.getUUID())) {
|
||||
ServerPlayer owner = level
|
||||
.getServer()
|
||||
.getPlayerList()
|
||||
.getPlayer(cell.getOwnerId());
|
||||
if (owner != null) {
|
||||
String cellName =
|
||||
cell.getName() != null
|
||||
? cell.getName()
|
||||
: "Cell " + cellId.toString().substring(0, 8);
|
||||
SystemMessageManager.sendToPlayer(
|
||||
owner,
|
||||
SystemMessageManager.MessageCategory.CELL_BREACH
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== BLOCK PLACE ====================
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onBlockPlace(BlockEvent.EntityPlaceEvent event) {
|
||||
if (!(event.getLevel() instanceof ServerLevel level)) return;
|
||||
|
||||
BlockPos pos = event.getPos();
|
||||
CellRegistryV2 registry = CellRegistryV2.get(level);
|
||||
|
||||
// Check if placed position is a breached wall
|
||||
UUID cellId = registry.getCellIdAtBreach(pos);
|
||||
if (cellId == null) return;
|
||||
|
||||
CellDataV2 cell = registry.getCell(cellId);
|
||||
if (cell == null) return;
|
||||
|
||||
// Only repair if the placed block is solid
|
||||
BlockState placedState = event.getPlacedBlock();
|
||||
if (!placedState.isSolid()) return;
|
||||
|
||||
registry.repairBreach(cellId, pos);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[CellV2EventHandler] Breach repaired at {} in cell {}",
|
||||
pos.toShortString(),
|
||||
cellId.toString().substring(0, 8)
|
||||
);
|
||||
|
||||
// Check if all breaches are repaired
|
||||
if (
|
||||
cell.getBreachedPositions().isEmpty() &&
|
||||
cell.getState() == CellState.BREACHED
|
||||
) {
|
||||
cell.setState(CellState.INTACT);
|
||||
|
||||
// Notify owner
|
||||
if (cell.getOwnerId() != null) {
|
||||
ServerPlayer owner = level
|
||||
.getServer()
|
||||
.getPlayerList()
|
||||
.getPlayer(cell.getOwnerId());
|
||||
if (owner != null) {
|
||||
owner.displayClientMessage(
|
||||
Component.translatable(
|
||||
"msg.tiedup.cell_core.breach_repaired"
|
||||
).withStyle(ChatFormatting.GREEN),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[CellV2EventHandler] Cell {} fully repaired → INTACT",
|
||||
cellId.toString().substring(0, 8)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== PLAYER TICK ====================
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onPlayerTick(TickEvent.PlayerTickEvent event) {
|
||||
if (event.phase != TickEvent.Phase.END) return;
|
||||
if (!(event.player instanceof ServerPlayer player)) return;
|
||||
|
||||
UUID playerId = player.getUUID();
|
||||
if (!CellSelectionManager.isInSelectionMode(playerId)) return;
|
||||
|
||||
// Cancel on sneak
|
||||
if (player.isShiftKeyDown()) {
|
||||
CellSelectionManager.clearSelection(playerId);
|
||||
player.displayClientMessage(
|
||||
Component.translatable(
|
||||
"msg.tiedup.cell_core.selection.cancelled"
|
||||
).withStyle(ChatFormatting.YELLOW),
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel on timeout or distance
|
||||
if (
|
||||
CellSelectionManager.shouldCancel(playerId, player.blockPosition())
|
||||
) {
|
||||
CellSelectionManager.clearSelection(playerId);
|
||||
player.displayClientMessage(
|
||||
Component.translatable(
|
||||
"msg.tiedup.cell_core.selection.cancelled"
|
||||
).withStyle(ChatFormatting.YELLOW),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== SELECTION HANDLING ====================
|
||||
|
||||
private static void handleSelectionClick(
|
||||
ServerPlayer player,
|
||||
ServerLevel level,
|
||||
BlockPos clickedPos,
|
||||
CellSelectionManager.SelectionContext selection
|
||||
) {
|
||||
CellRegistryV2 registry = CellRegistryV2.get(level);
|
||||
CellDataV2 cell = registry.getCell(selection.cellId);
|
||||
if (cell == null) {
|
||||
CellSelectionManager.clearSelection(player.getUUID());
|
||||
return;
|
||||
}
|
||||
|
||||
switch (selection.mode) {
|
||||
case SET_SPAWN -> handleSetSpawn(
|
||||
player,
|
||||
level,
|
||||
clickedPos,
|
||||
cell,
|
||||
selection
|
||||
);
|
||||
case SET_DELIVERY -> handleSetDelivery(
|
||||
player,
|
||||
level,
|
||||
clickedPos,
|
||||
cell,
|
||||
selection
|
||||
);
|
||||
case SET_DISGUISE -> handleSetDisguise(
|
||||
player,
|
||||
level,
|
||||
clickedPos,
|
||||
cell,
|
||||
selection
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static void handleSetSpawn(
|
||||
ServerPlayer player,
|
||||
ServerLevel level,
|
||||
BlockPos clickedPos,
|
||||
CellDataV2 cell,
|
||||
CellSelectionManager.SelectionContext selection
|
||||
) {
|
||||
// Spawn must be inside the cell or on a wall block (e.g. floor)
|
||||
boolean isInterior = cell.isContainedInCell(clickedPos);
|
||||
boolean isWall = cell.isWallBlock(clickedPos);
|
||||
|
||||
if (!isInterior && !isWall) {
|
||||
player.displayClientMessage(
|
||||
Component.translatable(
|
||||
"msg.tiedup.cell_core.not_inside_cell"
|
||||
).withStyle(ChatFormatting.RED),
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// If clicked on a wall (floor/ceiling/wall), spawn above it
|
||||
BlockPos actualSpawn = isWall ? clickedPos.above() : clickedPos;
|
||||
|
||||
// Verify the actual spawn position is inside the cell
|
||||
if (isWall && !cell.isContainedInCell(actualSpawn)) {
|
||||
player.displayClientMessage(
|
||||
Component.translatable(
|
||||
"msg.tiedup.cell_core.not_inside_cell"
|
||||
).withStyle(ChatFormatting.RED),
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update spawn in both Core BE and CellDataV2
|
||||
BlockEntity be = level.getBlockEntity(selection.corePos);
|
||||
if (be instanceof CellCoreBlockEntity core) {
|
||||
core.setSpawnPoint(actualSpawn);
|
||||
}
|
||||
cell.setSpawnPoint(actualSpawn);
|
||||
|
||||
CellSelectionManager.clearSelection(player.getUUID());
|
||||
player.displayClientMessage(
|
||||
Component.translatable("msg.tiedup.cell_core.spawn_set").withStyle(
|
||||
ChatFormatting.GREEN
|
||||
),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
private static void handleSetDelivery(
|
||||
ServerPlayer player,
|
||||
ServerLevel level,
|
||||
BlockPos clickedPos,
|
||||
CellDataV2 cell,
|
||||
CellSelectionManager.SelectionContext selection
|
||||
) {
|
||||
// Delivery must be outside the cell
|
||||
if (cell.isContainedInCell(clickedPos)) {
|
||||
player.displayClientMessage(
|
||||
Component.translatable(
|
||||
"msg.tiedup.cell_core.must_be_outside"
|
||||
).withStyle(ChatFormatting.RED),
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
BlockEntity be = level.getBlockEntity(selection.corePos);
|
||||
if (be instanceof CellCoreBlockEntity core) {
|
||||
core.setDeliveryPoint(clickedPos);
|
||||
}
|
||||
cell.setDeliveryPoint(clickedPos);
|
||||
|
||||
CellSelectionManager.clearSelection(player.getUUID());
|
||||
player.displayClientMessage(
|
||||
Component.translatable(
|
||||
"msg.tiedup.cell_core.delivery_set"
|
||||
).withStyle(ChatFormatting.GREEN),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
private static void handleSetDisguise(
|
||||
ServerPlayer player,
|
||||
ServerLevel level,
|
||||
BlockPos clickedPos,
|
||||
CellDataV2 cell,
|
||||
CellSelectionManager.SelectionContext selection
|
||||
) {
|
||||
BlockState clickedState = level.getBlockState(clickedPos);
|
||||
|
||||
// Must be a solid block
|
||||
if (!clickedState.isSolid()) {
|
||||
player.displayClientMessage(
|
||||
Component.translatable(
|
||||
"msg.tiedup.cell_core.must_be_solid"
|
||||
).withStyle(ChatFormatting.RED),
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
BlockEntity be = level.getBlockEntity(selection.corePos);
|
||||
if (be instanceof CellCoreBlockEntity core) {
|
||||
core.setDisguiseState(clickedState);
|
||||
}
|
||||
|
||||
CellSelectionManager.clearSelection(player.getUUID());
|
||||
String blockName = clickedState.getBlock().getName().getString();
|
||||
player.displayClientMessage(
|
||||
Component.translatable(
|
||||
"msg.tiedup.cell_core.disguise_set",
|
||||
blockName
|
||||
).withStyle(ChatFormatting.GREEN),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== DOOR CONTROL ====================
|
||||
|
||||
private static void handleDoorControl(
|
||||
PlayerInteractEvent.RightClickBlock event,
|
||||
ServerPlayer player,
|
||||
ServerLevel level,
|
||||
BlockPos clickedPos
|
||||
) {
|
||||
CellRegistryV2 registry = CellRegistryV2.get(level);
|
||||
|
||||
// Check if the clicked position is a cell door (doors are in wall set)
|
||||
// or an interior linked redstone (buttons/levers)
|
||||
CellDataV2 cellByWall = registry.getCellByWall(clickedPos);
|
||||
CellDataV2 cellByInterior = registry.getCellContaining(clickedPos);
|
||||
|
||||
CellDataV2 cell = null;
|
||||
boolean isDoor = false;
|
||||
boolean isRedstone = false;
|
||||
|
||||
if (cellByWall != null) {
|
||||
// Check if it's in the cell's doors list
|
||||
if (cellByWall.getDoors().contains(clickedPos)) {
|
||||
cell = cellByWall;
|
||||
isDoor = true;
|
||||
}
|
||||
// Check linked redstone in walls
|
||||
if (cellByWall.getLinkedRedstone().contains(clickedPos)) {
|
||||
cell = cellByWall;
|
||||
isRedstone = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (cellByInterior != null && !isDoor && !isRedstone) {
|
||||
// Check linked redstone in interior
|
||||
if (cellByInterior.getLinkedRedstone().contains(clickedPos)) {
|
||||
cell = cellByInterior;
|
||||
isRedstone = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (cell == null || (!isDoor && !isRedstone)) return;
|
||||
|
||||
// Owner, camp cell, or OP can always interact
|
||||
if (cell.canPlayerManage(player.getUUID(), player.hasPermissions(2))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prisoner or non-owner → block interaction
|
||||
event.setCanceled(true);
|
||||
player.displayClientMessage(
|
||||
Component.translatable(
|
||||
"msg.tiedup.cell_core.door_locked"
|
||||
).withStyle(ChatFormatting.RED),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package com.tiedup.remake.events.system;
|
||||
|
||||
import com.tiedup.remake.core.SettingsAccessor;
|
||||
import com.tiedup.remake.core.SystemMessageManager;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.dialogue.GagTalkManager;
|
||||
import com.tiedup.remake.items.base.ItemGag;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
|
||||
import com.tiedup.remake.state.IBondageState;
|
||||
import com.tiedup.remake.util.GagMaterial;
|
||||
import com.tiedup.remake.util.KidnappedHelper;
|
||||
import com.tiedup.remake.util.TiedUpUtils;
|
||||
import java.util.List;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraftforge.event.CommandEvent;
|
||||
import net.minecraftforge.event.ServerChatEvent;
|
||||
import net.minecraftforge.eventbus.api.EventPriority;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
/**
|
||||
* ChatEventHandler - Intercepts chat messages to apply gag effects.
|
||||
* Evolution: Implements proximity-based chat for gagged players.
|
||||
*
|
||||
* Phase 14.1.5: Refactored to use IBondageState interface
|
||||
*
|
||||
* Security fix: Now blocks communication commands (/msg, /tell, etc.) when gagged
|
||||
* to prevent gag bypass exploit
|
||||
*/
|
||||
@Mod.EventBusSubscriber(modid = TiedUpMod.MOD_ID)
|
||||
public class ChatEventHandler {
|
||||
|
||||
/** List of communication commands that should be blocked when gagged */
|
||||
private static final String[] BLOCKED_COMMANDS = {
|
||||
"msg",
|
||||
"tell",
|
||||
"w",
|
||||
"whisper",
|
||||
"r",
|
||||
"reply",
|
||||
"me",
|
||||
};
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onPlayerChat(ServerChatEvent event) {
|
||||
ServerPlayer player = event.getPlayer();
|
||||
IBondageState state = KidnappedHelper.getKidnappedState(player);
|
||||
|
||||
if (state != null && state.isGagged()) {
|
||||
ItemStack gagStack = V2EquipmentHelper.getInRegion(
|
||||
player,
|
||||
BodyRegionV2.MOUTH
|
||||
);
|
||||
|
||||
if (
|
||||
!gagStack.isEmpty() &&
|
||||
gagStack.getItem() instanceof ItemGag gagItem
|
||||
) {
|
||||
String originalMessage = event.getRawText();
|
||||
GagMaterial material = gagItem.getGagMaterial();
|
||||
|
||||
// 1. Process the message through our GagTalkManager V2
|
||||
Component muffledMessage = GagTalkManager.processGagMessage(
|
||||
state,
|
||||
gagStack,
|
||||
originalMessage
|
||||
);
|
||||
|
||||
// 2. Proximity Chat Logic
|
||||
boolean useProximity = SettingsAccessor.isGagTalkProximityEnabled(
|
||||
player.level().getGameRules());
|
||||
|
||||
if (useProximity) {
|
||||
// Cancel global and send to nearby
|
||||
event.setCanceled(true);
|
||||
|
||||
Component finalChat = Component.literal("<")
|
||||
.append(player.getDisplayName())
|
||||
.append("> ")
|
||||
.append(muffledMessage);
|
||||
|
||||
double range = material.getTalkRange();
|
||||
|
||||
// Phase 14.2: Use TiedUpUtils for proximity and earplugs filtering
|
||||
List<ServerPlayer> nearbyPlayers =
|
||||
TiedUpUtils.getPlayersAround(
|
||||
player.level(),
|
||||
player.blockPosition(),
|
||||
range
|
||||
);
|
||||
|
||||
int listeners = 0;
|
||||
for (ServerPlayer other : nearbyPlayers) {
|
||||
// Check if receiver has earplugs (they can't hear)
|
||||
IBondageState receiverState =
|
||||
KidnappedHelper.getKidnappedState(other);
|
||||
if (
|
||||
receiverState != null && receiverState.hasEarplugs()
|
||||
) {
|
||||
// Can't hear - skip this player
|
||||
continue;
|
||||
}
|
||||
|
||||
other.sendSystemMessage(finalChat);
|
||||
if (other != player) listeners++;
|
||||
}
|
||||
|
||||
if (listeners == 0) {
|
||||
player.displayClientMessage(
|
||||
Component.translatable(
|
||||
"chat.tiedup.gag.no_one_heard"
|
||||
).withStyle(
|
||||
net.minecraft.ChatFormatting.ITALIC,
|
||||
net.minecraft.ChatFormatting.GRAY
|
||||
),
|
||||
true
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Just replace message but keep it global
|
||||
event.setMessage(muffledMessage);
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[Chat] {} muffled message processed (Proximity: {})",
|
||||
player.getName().getString(),
|
||||
useProximity
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercept commands to prevent gagged players from using communication commands.
|
||||
* Blocks /msg, /tell, /w, /whisper, /r, /reply, /me when player is gagged.
|
||||
*
|
||||
* Security fix: Prevents gag bypass exploit via private messages
|
||||
*/
|
||||
@SubscribeEvent(priority = EventPriority.HIGHEST)
|
||||
public static void onCommand(CommandEvent event) {
|
||||
// Only check if sender is a ServerPlayer
|
||||
if (
|
||||
!(event
|
||||
.getParseResults()
|
||||
.getContext()
|
||||
.getSource()
|
||||
.getEntity() instanceof
|
||||
ServerPlayer player)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if player is gagged
|
||||
IBondageState state = KidnappedHelper.getKidnappedState(player);
|
||||
if (state == null || !state.isGagged()) {
|
||||
return; // Not gagged, allow all commands
|
||||
}
|
||||
|
||||
// Get the command name (first part of the command string)
|
||||
String commandInput = event.getParseResults().getReader().getString();
|
||||
if (commandInput.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove leading slash if present
|
||||
String commandName = commandInput.startsWith("/")
|
||||
? commandInput.substring(1)
|
||||
: commandInput;
|
||||
// Get only the first word (command name)
|
||||
commandName = commandName.split(" ")[0].toLowerCase();
|
||||
|
||||
// Check if this is a blocked communication command
|
||||
for (String blockedCmd : BLOCKED_COMMANDS) {
|
||||
if (commandName.equals(blockedCmd)) {
|
||||
// Block the command
|
||||
event.setCanceled(true);
|
||||
|
||||
// Send muffled message to player
|
||||
SystemMessageManager.sendToPlayer(
|
||||
player,
|
||||
SystemMessageManager.MessageCategory.ERROR,
|
||||
"Mmmph! You can't use that command while gagged!"
|
||||
);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[Chat] Blocked command '{}' from gagged player {}",
|
||||
commandName,
|
||||
player.getName().getString()
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.tiedup.remake.events.system;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.minigame.LockpickSessionManager;
|
||||
import com.tiedup.remake.minigame.StruggleSessionManager;
|
||||
import net.minecraftforge.event.TickEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
/**
|
||||
* Server tick handler for mini-game session management.
|
||||
*
|
||||
* Handles:
|
||||
* - Continuous struggle session ticking (direction changes, resistance updates, shock checks)
|
||||
* - Session cleanup for expired/disconnected sessions
|
||||
*/
|
||||
@Mod.EventBusSubscriber(
|
||||
modid = TiedUpMod.MOD_ID,
|
||||
bus = Mod.EventBusSubscriber.Bus.FORGE
|
||||
)
|
||||
public class MiniGameTickHandler {
|
||||
|
||||
/**
|
||||
* Tick continuous struggle sessions every server tick.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onServerTick(TickEvent.ServerTickEvent event) {
|
||||
if (event.phase != TickEvent.Phase.END) return;
|
||||
|
||||
long currentTick = event.getServer().getTickCount();
|
||||
|
||||
// Tick continuous struggle sessions every tick
|
||||
StruggleSessionManager.getInstance().tickContinuousSessions(event.getServer(), currentTick);
|
||||
|
||||
// Cleanup expired sessions periodically
|
||||
StruggleSessionManager.getInstance().tickCleanup(currentTick);
|
||||
LockpickSessionManager.getInstance().tickCleanup(currentTick);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user