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)
527 lines
17 KiB
Java
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
|
|
)
|
|
);
|
|
}
|
|
}
|
|
}
|