Files
TiedUp-/src/main/java/com/tiedup/remake/items/clothes/GenericClothes.java
NotEvil 75679c6718 feat(D-01/E): resistance & lock system rework (E1-E7)
E1: Initialize currentResistance in NBT at equip time from
    ResistanceComponent — eliminates MAX-scan fallback bug

E2: BuiltInLockComponent for organic items (already committed)

E3: canStruggle refactor — new model:
    - ARMS: always struggle-able (no lock gating)
    - Non-ARMS: only if locked OR built-in lock
    - Removed dead isItemLocked() from StruggleState + overrides

E4: canUnequip already handled by BuiltInLockComponent.blocksUnequip()
    via ComponentHolder delegation

E5: Help/assist mechanic deferred (needs UI design)

E6: Removed lock resistance from ILockable (5 methods + NBT key deleted)
    - GenericKnife: new knifeCutProgress NBT for cutting locks
    - StruggleAccessory: accessoryStruggleResistance NBT replaces lock resistance
    - PacketV2StruggleStart: uses config-based padlock resistance
    - All lock/unlock packets cleaned of initializeLockResistance/clearLockResistance

E7: Fixed 3 pre-existing bugs:
    - B2: DataDrivenItemRegistry.clear() synchronized on RELOAD_LOCK
    - B3: V2TyingPlayerTask validates heldStack before equip (prevents duplication)
    - B5: EntityKidnapperMerchant.remove() cleans playerToMerchant map (memory leak)
2026-04-15 03:23:49 +02:00

527 lines
17 KiB
Java

package com.tiedup.remake.items.clothes;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.ILockable;
import com.tiedup.remake.network.sync.SyncManager;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.IV2BondageItem;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import net.minecraft.ChatFormatting;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable;
/**
* Generic clothes item with full NBT-based configuration.
*
* <p>Clothes are cosmetic items that can:
* <ul>
* <li>Use dynamic textures from URLs</li>
* <li>Replace the entire player skin (full-skin mode)</li>
* <li>Force slim arm model</li>
* <li>Control visibility of wearer's body parts</li>
* <li>Be locked with padlocks</li>
* </ul>
*
* <p>Unlike other bondage items, clothes have NO gameplay effects - they are purely visual.
*/
public class GenericClothes extends Item implements ILockable, IV2BondageItem {
// ========== NBT KEYS ==========
public static final String NBT_DYNAMIC_TEXTURE = "dynamicTexture";
public static final String NBT_FULL_SKIN = "fullSkin";
public static final String NBT_SMALL_ARMS = "smallArms";
public static final String NBT_KEEP_HEAD = "keepHead";
public static final String NBT_LAYER_VISIBILITY = "layerVisibility";
public static final String NBT_LOCKED = "locked";
public static final String NBT_LOCKABLE = "lockable";
public static final String NBT_LOCKED_BY_KEY_UUID = "lockedByKeyUUID";
// Layer visibility keys
public static final String LAYER_HEAD = "head";
public static final String LAYER_BODY = "body";
public static final String LAYER_LEFT_ARM = "leftArm";
public static final String LAYER_RIGHT_ARM = "rightArm";
public static final String LAYER_LEFT_LEG = "leftLeg";
public static final String LAYER_RIGHT_LEG = "rightLeg";
public GenericClothes() {
super(new Item.Properties().stacksTo(16));
}
// ========== Lifecycle Hooks ==========
@Override
public void onEquipped(ItemStack stack, LivingEntity entity) {
// Clothes have no special equip effects - purely cosmetic
}
@Override
public void onUnequipped(ItemStack stack, LivingEntity entity) {
// Clothes have no special unequip effects
}
/**
* Called when player right-clicks another entity with clothes.
* Allows putting clothes on tied-up entities (Players and NPCs).
*
* Unlike other bondage items, clothes can also be put on non-tied players
* if game rules allow it (roleplay scenarios).
*
* @param stack The item stack
* @param player The player using the item
* @param target The entity being interacted with
* @param hand The hand holding the item
* @return SUCCESS if clothes equipped/replaced, PASS otherwise
*/
@Override
public InteractionResult interactLivingEntity(
ItemStack stack,
Player player,
LivingEntity target,
InteractionHand hand
) {
// Server-side only
if (player.level().isClientSide) {
return InteractionResult.SUCCESS;
}
// Check if target can wear clothes (Player, EntityDamsel, EntityKidnapper)
IBondageState targetState = KidnappedHelper.getKidnappedState(target);
if (targetState == null) {
return InteractionResult.PASS; // Entity cannot wear clothes
}
// Unlike gags/blindfolds, clothes can be put on non-tied players too
// But if tied, always allowed. If not tied, check if target allows it.
if (!targetState.isTiedUp() && !targetState.canChangeClothes(player)) {
return InteractionResult.PASS;
}
// Case 1: No clothes yet - equip new one
if (!targetState.hasClothes()) {
ItemStack clothesCopy = stack.copyWithCount(1);
targetState.equip(BodyRegionV2.TORSO, clothesCopy);
stack.shrink(1);
// Sync equipment to all tracking clients
if (target instanceof ServerPlayer serverPlayer) {
SyncManager.syncInventory(serverPlayer);
SyncManager.syncClothesConfig(serverPlayer);
}
TiedUpMod.LOGGER.info(
"[GenericClothes] {} put clothes on {}",
player.getName().getString(),
target.getName().getString()
);
return InteractionResult.SUCCESS;
}
// Case 2: Already has clothes - replace them
else {
ItemStack clothesCopy = stack.copyWithCount(1);
ItemStack oldClothes = targetState.replaceEquipment(
BodyRegionV2.TORSO,
clothesCopy,
false
);
if (!oldClothes.isEmpty()) {
stack.shrink(1);
targetState.kidnappedDropItem(oldClothes);
// Sync equipment to all tracking clients
if (target instanceof ServerPlayer serverPlayer) {
SyncManager.syncInventory(serverPlayer);
SyncManager.syncClothesConfig(serverPlayer);
}
TiedUpMod.LOGGER.info(
"[GenericClothes] {} replaced clothes on {}",
player.getName().getString(),
target.getName().getString()
);
return InteractionResult.SUCCESS;
}
}
return InteractionResult.PASS;
}
// ========== Dynamic Texture Methods ==========
/**
* Get the dynamic texture URL from this clothes item.
*
* @param stack The ItemStack to check
* @return The URL string, or null if not set
*/
@Nullable
public String getDynamicTextureUrl(ItemStack stack) {
CompoundTag tag = stack.getTag();
if (tag != null && tag.contains(NBT_DYNAMIC_TEXTURE)) {
String url = tag.getString(NBT_DYNAMIC_TEXTURE);
return url.isEmpty() ? null : url;
}
return null;
}
/**
* Set the dynamic texture URL for this clothes item.
*
* @param stack The ItemStack to modify
* @param url The URL to set
*/
public void setDynamicTextureUrl(ItemStack stack, String url) {
if (url != null && !url.isEmpty()) {
stack.getOrCreateTag().putString(NBT_DYNAMIC_TEXTURE, url);
}
}
/**
* Remove the dynamic texture URL from this clothes item.
*
* @param stack The ItemStack to modify
*/
public void removeDynamicTextureUrl(ItemStack stack) {
CompoundTag tag = stack.getTag();
if (tag != null) {
tag.remove(NBT_DYNAMIC_TEXTURE);
}
}
/**
* Check if this clothes item has a dynamic texture URL set.
*
* @param stack The ItemStack to check
* @return true if a URL is set
*/
public boolean hasDynamicTextureUrl(ItemStack stack) {
return getDynamicTextureUrl(stack) != null;
}
// ========== Full Skin / Small Arms Methods ==========
/**
* Check if full-skin mode is enabled.
* In full-skin mode, the clothes texture replaces the entire player skin.
*
* @param stack The ItemStack to check
* @return true if full-skin mode is enabled
*/
public boolean isFullSkinEnabled(ItemStack stack) {
CompoundTag tag = stack.getTag();
return tag != null && tag.getBoolean(NBT_FULL_SKIN);
}
/**
* Set full-skin mode.
*
* @param stack The ItemStack to modify
* @param enabled true to enable full-skin mode
*/
public void setFullSkinEnabled(ItemStack stack, boolean enabled) {
stack.getOrCreateTag().putBoolean(NBT_FULL_SKIN, enabled);
}
/**
* Check if small arms (slim model) should be forced.
*
* @param stack The ItemStack to check
* @return true if small arms should be forced
*/
public boolean shouldForceSmallArms(ItemStack stack) {
CompoundTag tag = stack.getTag();
return tag != null && tag.getBoolean(NBT_SMALL_ARMS);
}
/**
* Set whether small arms (slim model) should be forced.
*
* @param stack The ItemStack to modify
* @param enabled true to force small arms
*/
public void setForceSmallArms(ItemStack stack, boolean enabled) {
stack.getOrCreateTag().putBoolean(NBT_SMALL_ARMS, enabled);
}
/**
* Check if keep head mode is enabled.
* When enabled, the wearer's head/hat layers are preserved instead of being
* replaced by the clothes texture. Useful for keeping the original face.
*
* @param stack The ItemStack to check
* @return true if keep head mode is enabled
*/
public boolean isKeepHeadEnabled(ItemStack stack) {
CompoundTag tag = stack.getTag();
return tag != null && tag.getBoolean(NBT_KEEP_HEAD);
}
/**
* Set keep head mode.
* When enabled, the wearer's head/hat layers are preserved.
*
* @param stack The ItemStack to modify
* @param enabled true to keep the wearer's head
*/
public void setKeepHeadEnabled(ItemStack stack, boolean enabled) {
stack.getOrCreateTag().putBoolean(NBT_KEEP_HEAD, enabled);
}
// ========== Layer Visibility Methods ==========
/**
* Check if a specific body layer is enabled (visible on wearer).
* Defaults to true (visible) if not set.
*
* @param stack The ItemStack to check
* @param layer The layer key (use LAYER_* constants)
* @return true if the layer is visible
*/
public boolean isLayerEnabled(ItemStack stack, String layer) {
CompoundTag tag = stack.getTag();
if (tag == null || !tag.contains(NBT_LAYER_VISIBILITY)) {
return true; // Default: all layers visible
}
CompoundTag layers = tag.getCompound(NBT_LAYER_VISIBILITY);
// If not specified, default to visible
return !layers.contains(layer) || layers.getBoolean(layer);
}
/**
* Set the visibility of a specific body layer on the wearer.
*
* @param stack The ItemStack to modify
* @param layer The layer key (use LAYER_* constants)
* @param enabled true to show the layer, false to hide it
*/
public void setLayerEnabled(
ItemStack stack,
String layer,
boolean enabled
) {
CompoundTag tag = stack.getOrCreateTag();
CompoundTag layers = tag.contains(NBT_LAYER_VISIBILITY)
? tag.getCompound(NBT_LAYER_VISIBILITY)
: new CompoundTag();
layers.putBoolean(layer, enabled);
tag.put(NBT_LAYER_VISIBILITY, layers);
}
/**
* Get all layer visibility settings as a compound tag.
*
* @param stack The ItemStack to check
* @return The layer visibility compound, or null if not set
*/
@Nullable
public CompoundTag getLayerVisibility(ItemStack stack) {
CompoundTag tag = stack.getTag();
if (tag != null && tag.contains(NBT_LAYER_VISIBILITY)) {
return tag.getCompound(NBT_LAYER_VISIBILITY);
}
return null;
}
// ========== IV2BondageItem Implementation ==========
private static final Set<BodyRegionV2> REGIONS =
Collections.unmodifiableSet(EnumSet.of(BodyRegionV2.TORSO));
@Override
public Set<BodyRegionV2> getOccupiedRegions() {
return REGIONS;
}
@Override
@Nullable
public ResourceLocation getModelLocation() {
return null; // Clothes use URL-texture rendering, not GLB models
}
@Override
public int getPosePriority() {
return 0; // Cosmetic item, never forces a pose
}
@Override
public int getEscapeDifficulty() {
return 0; // Cosmetic, no struggle resistance
}
@Override
public boolean supportsColor() {
return false; // Color is handled via URL texture, not variant system
}
@Override
public boolean supportsSlimModel() {
return false; // Slim/wide is handled via NBT smallArms flag, not model variants
}
@Override
public boolean canEquip(ItemStack stack, LivingEntity entity) {
return true;
}
@Override
public boolean canUnequip(ItemStack stack, LivingEntity entity) {
return true;
}
// ========== ILockable Implementation ==========
@Override
public ItemStack setLocked(ItemStack stack, boolean state) {
stack.getOrCreateTag().putBoolean(NBT_LOCKED, state);
if (!state) {
// When unlocking, clear lock-related data
setJammed(stack, false);
}
return stack;
}
@Override
public boolean isLocked(ItemStack stack) {
CompoundTag tag = stack.getTag();
return tag != null && tag.getBoolean(NBT_LOCKED);
}
@Override
public ItemStack setLockable(ItemStack stack, boolean state) {
stack.getOrCreateTag().putBoolean(NBT_LOCKABLE, state);
return stack;
}
@Override
public boolean isLockable(ItemStack stack) {
CompoundTag tag = stack.getTag();
// Default to true if not set
return (
tag == null ||
!tag.contains(NBT_LOCKABLE) ||
tag.getBoolean(NBT_LOCKABLE)
);
}
@Override
@Nullable
public UUID getLockedByKeyUUID(ItemStack stack) {
CompoundTag tag = stack.getTag();
if (tag != null && tag.hasUUID(NBT_LOCKED_BY_KEY_UUID)) {
return tag.getUUID(NBT_LOCKED_BY_KEY_UUID);
}
return null;
}
@Override
public void setLockedByKeyUUID(ItemStack stack, @Nullable UUID keyUUID) {
CompoundTag tag = stack.getOrCreateTag();
if (keyUUID != null) {
tag.putUUID(NBT_LOCKED_BY_KEY_UUID, keyUUID);
setLocked(stack, true);
} else {
tag.remove(NBT_LOCKED_BY_KEY_UUID);
setLocked(stack, false);
}
}
// ========== Tooltip ==========
@Override
public void appendHoverText(
ItemStack stack,
@Nullable Level level,
List<Component> tooltip,
TooltipFlag flag
) {
super.appendHoverText(stack, level, tooltip, flag);
// Dynamic texture info
String url = getDynamicTextureUrl(stack);
if (url != null) {
tooltip.add(
Component.translatable(
"item.tiedup.clothes.tooltip.has_url"
).withStyle(ChatFormatting.GREEN)
);
if (isFullSkinEnabled(stack)) {
tooltip.add(
Component.translatable(
"item.tiedup.clothes.tooltip.full_skin"
).withStyle(ChatFormatting.AQUA)
);
}
if (shouldForceSmallArms(stack)) {
tooltip.add(
Component.translatable(
"item.tiedup.clothes.tooltip.small_arms"
).withStyle(ChatFormatting.AQUA)
);
}
} else {
tooltip.add(
Component.translatable(
"item.tiedup.clothes.tooltip.no_url"
).withStyle(ChatFormatting.GRAY)
);
}
// Layer visibility info
CompoundTag layers = getLayerVisibility(stack);
if (layers != null) {
StringBuilder disabled = new StringBuilder();
if (!isLayerEnabled(stack, LAYER_HEAD)) disabled.append("head ");
if (!isLayerEnabled(stack, LAYER_BODY)) disabled.append("body ");
if (!isLayerEnabled(stack, LAYER_LEFT_ARM)) disabled.append(
"L.arm "
);
if (!isLayerEnabled(stack, LAYER_RIGHT_ARM)) disabled.append(
"R.arm "
);
if (!isLayerEnabled(stack, LAYER_LEFT_LEG)) disabled.append(
"L.leg "
);
if (!isLayerEnabled(stack, LAYER_RIGHT_LEG)) disabled.append(
"R.leg "
);
if (!disabled.isEmpty()) {
tooltip.add(
Component.translatable(
"item.tiedup.clothes.tooltip.layers_disabled",
disabled.toString().trim()
).withStyle(ChatFormatting.YELLOW)
);
}
}
// Lock info
if (isLocked(stack)) {
tooltip.add(
Component.translatable("item.tiedup.locked").withStyle(
ChatFormatting.RED
)
);
}
}
}