Remove internal phase comments and format code
Strip all Phase references, TODO/FUTURE roadmap notes, and internal planning comments from the codebase. Run Prettier for consistent formatting across all Java files.
This commit is contained in:
@@ -57,4 +57,4 @@ public enum BodyRegionV2 {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,7 @@ public class V2Blocks {
|
||||
public static final DeferredRegister<Block> BLOCKS =
|
||||
DeferredRegister.create(ForgeRegistries.BLOCKS, TiedUpMod.MOD_ID);
|
||||
|
||||
// ========================================
|
||||
// PET FURNITURE
|
||||
// ========================================
|
||||
|
||||
public static final RegistryObject<Block> PET_BOWL = BLOCKS.register(
|
||||
"pet_bowl",
|
||||
|
||||
@@ -18,9 +18,7 @@ public class V2Items {
|
||||
TiedUpMod.MOD_ID
|
||||
);
|
||||
|
||||
// ========================================
|
||||
// BLOCK ITEMS
|
||||
// ========================================
|
||||
|
||||
public static final RegistryObject<Item> PET_BOWL = ITEMS.register(
|
||||
"pet_bowl",
|
||||
|
||||
@@ -63,9 +63,7 @@ public class PetBedBlockEntity extends ObjBlockEntity {
|
||||
return new float[] { 0.0f, 0.0f, 0.0f };
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// OCCUPANCY
|
||||
// ========================================
|
||||
|
||||
public boolean isOccupied() {
|
||||
return occupantUUID != null;
|
||||
@@ -98,9 +96,7 @@ public class PetBedBlockEntity extends ObjBlockEntity {
|
||||
setChanged();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// OWNERSHIP
|
||||
// ========================================
|
||||
|
||||
@Nullable
|
||||
public UUID getOwnerUUID() {
|
||||
@@ -120,9 +116,7 @@ public class PetBedBlockEntity extends ObjBlockEntity {
|
||||
return ownerUUID == null;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// NBT
|
||||
// ========================================
|
||||
|
||||
@Override
|
||||
protected void saveAdditional(CompoundTag tag) {
|
||||
|
||||
@@ -6,7 +6,6 @@ import com.tiedup.remake.network.sync.PacketSyncPetBedState;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
@@ -16,6 +15,7 @@ import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Server-side tracker for pet bed sit/sleep state per player.
|
||||
|
||||
@@ -54,9 +54,7 @@ public class PetBowlBlockEntity extends ObjBlockEntity {
|
||||
return 1.0f; // Adjust based on model size
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// FOOD MANAGEMENT
|
||||
// ========================================
|
||||
|
||||
public boolean hasFood() {
|
||||
return hasFood && foodLevel > 0;
|
||||
@@ -102,9 +100,7 @@ public class PetBowlBlockEntity extends ObjBlockEntity {
|
||||
setChanged();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// NBT
|
||||
// ========================================
|
||||
|
||||
@Override
|
||||
protected void saveAdditional(CompoundTag tag) {
|
||||
|
||||
@@ -42,9 +42,7 @@ public class PetCageBlock extends Block implements EntityBlock {
|
||||
public static final DirectionProperty FACING =
|
||||
HorizontalDirectionalBlock.FACING;
|
||||
|
||||
// ========================================
|
||||
// OBJ MODEL DIMENSIONS (in model space, before rotation/translation)
|
||||
// ========================================
|
||||
private static final double MODEL_MIN_X = -1.137;
|
||||
private static final double MODEL_MAX_X = 1.0;
|
||||
private static final double MODEL_MIN_Z = -1.122;
|
||||
@@ -255,9 +253,7 @@ public class PetCageBlock extends Block implements EntityBlock {
|
||||
super.onRemove(state, level, pos, newState, isMoving);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// CAGE COLLISION (model-accurate)
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Compute the cage AABB in world coordinates for a given master position and facing.
|
||||
@@ -462,9 +458,7 @@ public class PetCageBlock extends Block implements EntityBlock {
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// MULTI-BLOCK LAYOUT (3x3x2)
|
||||
// ========================================
|
||||
|
||||
private static final int[][] GRID_OFFSETS;
|
||||
|
||||
|
||||
@@ -73,9 +73,7 @@ public class PetCageBlockEntity extends ObjBlockEntity {
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// OCCUPANT MANAGEMENT
|
||||
// ========================================
|
||||
|
||||
public boolean hasOccupant() {
|
||||
return occupantUUID != null;
|
||||
@@ -105,9 +103,7 @@ public class PetCageBlockEntity extends ObjBlockEntity {
|
||||
return server.getPlayerList().getPlayer(occupantUUID);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// NBT
|
||||
// ========================================
|
||||
|
||||
@Override
|
||||
protected void saveAdditional(CompoundTag tag) {
|
||||
|
||||
@@ -4,13 +4,13 @@ import com.tiedup.remake.core.TiedUpMod;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.ai.attributes.AttributeInstance;
|
||||
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Server-side manager for pet cage confinement.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.tiedup.remake.v2.bondage;
|
||||
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.BlockPos;
|
||||
@@ -23,7 +22,6 @@ import org.jetbrains.annotations.Nullable;
|
||||
*/
|
||||
@AutoRegisterCapability
|
||||
public interface IV2BondageEquipment {
|
||||
|
||||
/**
|
||||
* Get the item equipped in the given region.
|
||||
* @return The ItemStack in that region, or {@link ItemStack#EMPTY} — never null.
|
||||
@@ -72,9 +70,7 @@ public interface IV2BondageEquipment {
|
||||
*/
|
||||
void deserializeNBT(CompoundTag tag);
|
||||
|
||||
// ========================================
|
||||
// Pole leash persistence
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Whether the player was leashed to a pole when they disconnected.
|
||||
@@ -84,12 +80,14 @@ public interface IV2BondageEquipment {
|
||||
/**
|
||||
* Get the saved pole position, or null if none.
|
||||
*/
|
||||
@Nullable BlockPos getSavedPolePosition();
|
||||
@Nullable
|
||||
BlockPos getSavedPolePosition();
|
||||
|
||||
/**
|
||||
* Get the saved pole dimension, or null if none.
|
||||
*/
|
||||
@Nullable ResourceKey<Level> getSavedPoleDimension();
|
||||
@Nullable
|
||||
ResourceKey<Level> getSavedPoleDimension();
|
||||
|
||||
/**
|
||||
* Save the pole leash state for restoration on reconnect.
|
||||
@@ -101,9 +99,7 @@ public interface IV2BondageEquipment {
|
||||
*/
|
||||
void clearSavedPoleLeash();
|
||||
|
||||
// ========================================
|
||||
// Captor persistence
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Whether the player had a saved captor when they disconnected.
|
||||
@@ -113,7 +109,8 @@ public interface IV2BondageEquipment {
|
||||
/**
|
||||
* Get the saved captor UUID, or null if none.
|
||||
*/
|
||||
@Nullable UUID getSavedCaptorUUID();
|
||||
@Nullable
|
||||
UUID getSavedCaptorUUID();
|
||||
|
||||
/**
|
||||
* Save the captor UUID for restoration on reconnect.
|
||||
@@ -124,4 +121,4 @@ public interface IV2BondageEquipment {
|
||||
* Clear saved captor state.
|
||||
*/
|
||||
void clearSavedCaptor();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.tiedup.remake.v2.bondage;
|
||||
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
|
||||
import java.util.Set;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
@@ -16,7 +15,6 @@ import org.jetbrains.annotations.Nullable;
|
||||
* and lifecycle hooks.
|
||||
*/
|
||||
public interface IV2BondageItem {
|
||||
|
||||
// ===== REGIONS =====
|
||||
|
||||
/**
|
||||
@@ -164,4 +162,4 @@ public interface IV2BondageItem {
|
||||
default boolean canUnequip(ItemStack stack, LivingEntity entity) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ package com.tiedup.remake.v2.bondage;
|
||||
* Players use Forge capabilities instead — they do NOT implement this.
|
||||
*/
|
||||
public interface IV2EquipmentHolder {
|
||||
|
||||
/**
|
||||
* Get the V2 equipment storage for this entity.
|
||||
* @return The equipment instance, never null.
|
||||
|
||||
@@ -16,11 +16,13 @@ import net.minecraftforge.registries.RegistryObject;
|
||||
public class V2BondageItems {
|
||||
|
||||
public static final DeferredRegister<Item> ITEMS = DeferredRegister.create(
|
||||
ForgeRegistries.ITEMS, TiedUpMod.MOD_ID
|
||||
ForgeRegistries.ITEMS,
|
||||
TiedUpMod.MOD_ID
|
||||
);
|
||||
|
||||
public static final RegistryObject<Item> V2_HANDCUFFS = ITEMS.register(
|
||||
"v2_handcuffs", V2Handcuffs::new
|
||||
"v2_handcuffs",
|
||||
V2Handcuffs::new
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -28,7 +30,8 @@ public class V2BondageItems {
|
||||
* behavior varies per-stack via the {@code tiedup_item_id} NBT tag.
|
||||
*/
|
||||
public static final RegistryObject<Item> DATA_DRIVEN_ITEM = ITEMS.register(
|
||||
"data_driven_item", DataDrivenBondageItem::new
|
||||
"data_driven_item",
|
||||
DataDrivenBondageItem::new
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -38,6 +41,7 @@ public class V2BondageItems {
|
||||
* NBT tag on each stack.
|
||||
*/
|
||||
public static final RegistryObject<Item> FURNITURE_PLACER = ITEMS.register(
|
||||
"furniture_placer", FurniturePlacerItem::new
|
||||
"furniture_placer",
|
||||
FurniturePlacerItem::new
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import net.minecraft.world.item.ItemStack;
|
||||
* Carries displaced stacks for swap/supersede cases.
|
||||
*/
|
||||
public record V2EquipResult(Type type, List<ItemStack> displaced) {
|
||||
|
||||
public enum Type {
|
||||
/** Item equipped successfully into empty regions. */
|
||||
SUCCESS,
|
||||
@@ -17,19 +16,29 @@ public record V2EquipResult(Type type, List<ItemStack> displaced) {
|
||||
/** Global item superseded multiple sub-region items. */
|
||||
SUPERSEDED,
|
||||
/** Item could not be equipped due to unresolvable conflicts. */
|
||||
BLOCKED
|
||||
BLOCKED,
|
||||
}
|
||||
|
||||
/** Convenience: check if equip was blocked. */
|
||||
public boolean isBlocked() { return type == Type.BLOCKED; }
|
||||
public boolean isBlocked() {
|
||||
return type == Type.BLOCKED;
|
||||
}
|
||||
|
||||
/** Convenience: check if equip succeeded (any non-blocked result). */
|
||||
public boolean isSuccess() { return type != Type.BLOCKED; }
|
||||
public boolean isSuccess() {
|
||||
return type != Type.BLOCKED;
|
||||
}
|
||||
|
||||
// ===== Factory methods =====
|
||||
|
||||
public static final V2EquipResult SUCCESS = new V2EquipResult(Type.SUCCESS, List.of());
|
||||
public static final V2EquipResult BLOCKED = new V2EquipResult(Type.BLOCKED, List.of());
|
||||
public static final V2EquipResult SUCCESS = new V2EquipResult(
|
||||
Type.SUCCESS,
|
||||
List.of()
|
||||
);
|
||||
public static final V2EquipResult BLOCKED = new V2EquipResult(
|
||||
Type.BLOCKED,
|
||||
List.of()
|
||||
);
|
||||
|
||||
public static V2EquipResult swapped(ItemStack displaced) {
|
||||
return new V2EquipResult(Type.SWAPPED, List.of(displaced));
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package com.tiedup.remake.v2.bondage;
|
||||
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import java.util.ArrayList;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
@@ -36,7 +35,11 @@ public final class V2EquipmentManager {
|
||||
/**
|
||||
* Check if an item can be equipped without any conflicts (stack-aware).
|
||||
*/
|
||||
public static boolean canEquip(IV2BondageEquipment equip, IV2BondageItem item, ItemStack newStack) {
|
||||
public static boolean canEquip(
|
||||
IV2BondageEquipment equip,
|
||||
IV2BondageItem item,
|
||||
ItemStack newStack
|
||||
) {
|
||||
return findAllConflicts(equip, item, newStack).isEmpty();
|
||||
}
|
||||
|
||||
@@ -66,14 +69,24 @@ public final class V2EquipmentManager {
|
||||
}
|
||||
|
||||
// 2. Existing items' getBlockedRegions() block new item's regions
|
||||
for (Map.Entry<BodyRegionV2, ItemStack> entry : equip.getAllEquipped().entrySet()) {
|
||||
for (Map.Entry<BodyRegionV2, ItemStack> entry : equip
|
||||
.getAllEquipped()
|
||||
.entrySet()) {
|
||||
ItemStack equipped = entry.getValue();
|
||||
if (seen.containsKey(equipped)) continue;
|
||||
if (equipped.getItem() instanceof IV2BondageItem equippedItem) {
|
||||
for (BodyRegionV2 newRegion : item.getOccupiedRegions(newStack)) {
|
||||
if (equippedItem.getBlockedRegions(equipped).contains(newRegion)) {
|
||||
for (BodyRegionV2 newRegion : item.getOccupiedRegions(
|
||||
newStack
|
||||
)) {
|
||||
if (
|
||||
equippedItem
|
||||
.getBlockedRegions(equipped)
|
||||
.contains(newRegion)
|
||||
) {
|
||||
seen.put(equipped, Boolean.TRUE);
|
||||
conflicts.add(new ConflictEntry(entry.getKey(), equipped));
|
||||
conflicts.add(
|
||||
new ConflictEntry(entry.getKey(), equipped)
|
||||
);
|
||||
break; // One conflict per item is enough
|
||||
}
|
||||
}
|
||||
@@ -116,7 +129,8 @@ public final class V2EquipmentManager {
|
||||
}
|
||||
|
||||
// De-duplicate conflicts by stack identity
|
||||
IdentityHashMap<ItemStack, ConflictEntry> uniqueConflicts = new IdentityHashMap<>();
|
||||
IdentityHashMap<ItemStack, ConflictEntry> uniqueConflicts =
|
||||
new IdentityHashMap<>();
|
||||
for (ConflictEntry c : conflicts) {
|
||||
uniqueConflicts.putIfAbsent(c.stack(), c);
|
||||
}
|
||||
@@ -125,7 +139,9 @@ public final class V2EquipmentManager {
|
||||
if (uniqueConflicts.size() == 1) {
|
||||
ConflictEntry conflict = uniqueConflicts.values().iterator().next();
|
||||
ItemStack conflictStack = conflict.stack();
|
||||
if (conflictStack.getItem() instanceof IV2BondageItem conflictItem) {
|
||||
if (
|
||||
conflictStack.getItem() instanceof IV2BondageItem conflictItem
|
||||
) {
|
||||
if (conflictItem.canUnequip(conflictStack, entity)) {
|
||||
removeAllRegionsOf(equip, conflictStack);
|
||||
conflictItem.onUnequipped(conflictStack, entity);
|
||||
@@ -171,7 +187,10 @@ public final class V2EquipmentManager {
|
||||
if (cs.getItem() instanceof IV2BondageItem ci) {
|
||||
ci.onUnequipped(cs, entity);
|
||||
} else {
|
||||
TiedUpMod.LOGGER.warn("[V2] Supersede removed non-V2 item {} from equipment", cs);
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[V2] Supersede removed non-V2 item {} from equipment",
|
||||
cs
|
||||
);
|
||||
}
|
||||
displaced.add(cs);
|
||||
}
|
||||
@@ -199,7 +218,10 @@ public final class V2EquipmentManager {
|
||||
* Remove an item from all regions by identity scan.
|
||||
* Uses full BodyRegionV2.values() scan to prevent orphan stacks.
|
||||
*/
|
||||
public static void removeAllRegionsOf(IV2BondageEquipment equip, ItemStack stack) {
|
||||
public static void removeAllRegionsOf(
|
||||
IV2BondageEquipment equip,
|
||||
ItemStack stack
|
||||
) {
|
||||
for (BodyRegionV2 region : BodyRegionV2.values()) {
|
||||
//noinspection ObjectEquality — intentional identity comparison
|
||||
if (equip.getInRegion(region) == stack) {
|
||||
@@ -207,5 +229,4 @@ public final class V2EquipmentManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,11 +42,15 @@ public class V2BondageEquipment implements IV2BondageEquipment {
|
||||
private final EnumMap<BodyRegionV2, ItemStack> regions;
|
||||
|
||||
// Pole leash persistence
|
||||
@Nullable private BlockPos savedPolePosition;
|
||||
@Nullable private ResourceKey<Level> savedPoleDimension;
|
||||
@Nullable
|
||||
private BlockPos savedPolePosition;
|
||||
|
||||
@Nullable
|
||||
private ResourceKey<Level> savedPoleDimension;
|
||||
|
||||
// Captor persistence
|
||||
@Nullable private UUID savedCaptorUUID;
|
||||
@Nullable
|
||||
private UUID savedCaptorUUID;
|
||||
|
||||
public V2BondageEquipment() {
|
||||
this.regions = new EnumMap<>(BodyRegionV2.class);
|
||||
@@ -104,11 +108,16 @@ public class V2BondageEquipment implements IV2BondageEquipment {
|
||||
public boolean isRegionBlocked(BodyRegionV2 region) {
|
||||
if (region == null) return false;
|
||||
// Check if any equipped item's getBlockedRegions() includes this region
|
||||
for (Map.Entry<BodyRegionV2, ItemStack> entry : getAllEquipped().entrySet()) {
|
||||
for (Map.Entry<
|
||||
BodyRegionV2,
|
||||
ItemStack
|
||||
> entry : getAllEquipped().entrySet()) {
|
||||
ItemStack stack = entry.getValue();
|
||||
if (stack.getItem() instanceof IV2BondageItem item) {
|
||||
if (item.getBlockedRegions(stack).contains(region)
|
||||
&& !item.getOccupiedRegions(stack).contains(region)) {
|
||||
if (
|
||||
item.getBlockedRegions(stack).contains(region) &&
|
||||
!item.getOccupiedRegions(stack).contains(region)
|
||||
) {
|
||||
// Blocked by another item (not self-blocking via occupation)
|
||||
return true;
|
||||
}
|
||||
@@ -137,9 +146,7 @@ public class V2BondageEquipment implements IV2BondageEquipment {
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Pole leash persistence
|
||||
// ========================================
|
||||
|
||||
@Override
|
||||
public boolean wasLeashedToPole() {
|
||||
@@ -170,9 +177,7 @@ public class V2BondageEquipment implements IV2BondageEquipment {
|
||||
this.savedPoleDimension = null;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Captor persistence
|
||||
// ========================================
|
||||
|
||||
@Override
|
||||
public boolean hasSavedCaptor() {
|
||||
@@ -195,9 +200,7 @@ public class V2BondageEquipment implements IV2BondageEquipment {
|
||||
this.savedCaptorUUID = null;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// NBT serialization
|
||||
// ========================================
|
||||
|
||||
@Override
|
||||
public CompoundTag serializeNBT() {
|
||||
@@ -238,7 +241,10 @@ public class V2BondageEquipment implements IV2BondageEquipment {
|
||||
// Pole leash persistence
|
||||
if (savedPolePosition != null && savedPoleDimension != null) {
|
||||
root.putLong("pole_position", savedPolePosition.asLong());
|
||||
root.putString("pole_dimension", savedPoleDimension.location().toString());
|
||||
root.putString(
|
||||
"pole_dimension",
|
||||
savedPoleDimension.location().toString()
|
||||
);
|
||||
}
|
||||
|
||||
// Captor persistence
|
||||
@@ -279,7 +285,8 @@ public class V2BondageEquipment implements IV2BondageEquipment {
|
||||
if (!key.endsWith(NBT_ALSO_SUFFIX)) continue;
|
||||
|
||||
String primaryRegionName = key.substring(
|
||||
0, key.length() - NBT_ALSO_SUFFIX.length()
|
||||
0,
|
||||
key.length() - NBT_ALSO_SUFFIX.length()
|
||||
);
|
||||
ItemStack primaryStack = loadedStacks.get(primaryRegionName);
|
||||
if (primaryStack == null) continue;
|
||||
@@ -298,11 +305,15 @@ public class V2BondageEquipment implements IV2BondageEquipment {
|
||||
if (tag.contains("pole_position") && tag.contains("pole_dimension")) {
|
||||
try {
|
||||
savedPolePosition = BlockPos.of(tag.getLong("pole_position"));
|
||||
savedPoleDimension = ResourceKey.create(Registries.DIMENSION,
|
||||
new ResourceLocation(tag.getString("pole_dimension")));
|
||||
savedPoleDimension = ResourceKey.create(
|
||||
Registries.DIMENSION,
|
||||
new ResourceLocation(tag.getString("pole_dimension"))
|
||||
);
|
||||
} catch (net.minecraft.ResourceLocationException e) {
|
||||
com.tiedup.remake.core.TiedUpMod.LOGGER.warn(
|
||||
"Invalid pole dimension in NBT, clearing saved pole data: {}", e.getMessage());
|
||||
"Invalid pole dimension in NBT, clearing saved pole data: {}",
|
||||
e.getMessage()
|
||||
);
|
||||
savedPolePosition = null;
|
||||
savedPoleDimension = null;
|
||||
}
|
||||
@@ -318,4 +329,4 @@ public class V2BondageEquipment implements IV2BondageEquipment {
|
||||
savedCaptorUUID = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,4 +59,4 @@ public class V2BondageEquipmentProvider
|
||||
public void invalidate() {
|
||||
optional.invalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ import com.tiedup.remake.network.ModNetwork;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.v2.bondage.IV2BondageEquipment;
|
||||
import com.tiedup.remake.v2.bondage.IV2BondageItem;
|
||||
import com.tiedup.remake.v2.bondage.IV2EquipmentHolder;
|
||||
import com.tiedup.remake.v2.bondage.V2EquipResult;
|
||||
import com.tiedup.remake.v2.bondage.V2EquipmentManager;
|
||||
import com.tiedup.remake.v2.bondage.IV2EquipmentHolder;
|
||||
import com.tiedup.remake.v2.bondage.network.PacketSyncV2Equipment;
|
||||
import java.util.Map;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
@@ -24,7 +24,6 @@ import org.jetbrains.annotations.Nullable;
|
||||
* is the authority for equipment state.
|
||||
*
|
||||
* Currently dispatches only for Players (via Forge capability).
|
||||
* Phase 6 will add Damsel/ArmorStand support.
|
||||
*/
|
||||
public final class V2EquipmentHelper {
|
||||
|
||||
@@ -40,7 +39,8 @@ public final class V2EquipmentHelper {
|
||||
public static IV2BondageEquipment getEquipment(LivingEntity entity) {
|
||||
if (entity == null) return null;
|
||||
if (entity instanceof Player player) {
|
||||
return player.getCapability(V2BondageEquipmentProvider.V2_BONDAGE_EQUIPMENT)
|
||||
return player
|
||||
.getCapability(V2BondageEquipmentProvider.V2_BONDAGE_EQUIPMENT)
|
||||
.orElse(null);
|
||||
}
|
||||
// V2 equipment holders (EntityDamsel, etc.)
|
||||
@@ -54,7 +54,10 @@ public final class V2EquipmentHelper {
|
||||
* Get the item in a specific region for an entity.
|
||||
* @return The ItemStack, or {@link ItemStack#EMPTY} if empty or entity unsupported.
|
||||
*/
|
||||
public static ItemStack getInRegion(LivingEntity entity, BodyRegionV2 region) {
|
||||
public static ItemStack getInRegion(
|
||||
LivingEntity entity,
|
||||
BodyRegionV2 region
|
||||
) {
|
||||
IV2BondageEquipment equip = getEquipment(entity);
|
||||
if (equip == null) return ItemStack.EMPTY;
|
||||
return equip.getInRegion(region);
|
||||
@@ -63,7 +66,10 @@ public final class V2EquipmentHelper {
|
||||
/**
|
||||
* Check if a region is directly occupied on the given entity.
|
||||
*/
|
||||
public static boolean isRegionOccupied(LivingEntity entity, BodyRegionV2 region) {
|
||||
public static boolean isRegionOccupied(
|
||||
LivingEntity entity,
|
||||
BodyRegionV2 region
|
||||
) {
|
||||
IV2BondageEquipment equip = getEquipment(entity);
|
||||
if (equip == null) return false;
|
||||
return equip.isRegionOccupied(region);
|
||||
@@ -72,7 +78,10 @@ public final class V2EquipmentHelper {
|
||||
/**
|
||||
* Check if a region is blocked by any equipped item's {@link IV2BondageItem#getBlockedRegions()}.
|
||||
*/
|
||||
public static boolean isRegionBlocked(LivingEntity entity, BodyRegionV2 region) {
|
||||
public static boolean isRegionBlocked(
|
||||
LivingEntity entity,
|
||||
BodyRegionV2 region
|
||||
) {
|
||||
IV2BondageEquipment equip = getEquipment(entity);
|
||||
if (equip == null) return false;
|
||||
return equip.isRegionBlocked(region);
|
||||
@@ -82,7 +91,9 @@ public final class V2EquipmentHelper {
|
||||
* Get all equipped items (de-duplicated) for an entity.
|
||||
* @return Unmodifiable map, or empty map if entity unsupported.
|
||||
*/
|
||||
public static Map<BodyRegionV2, ItemStack> getAllEquipped(LivingEntity entity) {
|
||||
public static Map<BodyRegionV2, ItemStack> getAllEquipped(
|
||||
LivingEntity entity
|
||||
) {
|
||||
IV2BondageEquipment equip = getEquipment(entity);
|
||||
if (equip == null) return Map.of();
|
||||
return equip.getAllEquipped();
|
||||
@@ -110,10 +121,15 @@ public final class V2EquipmentHelper {
|
||||
* @param stack The ItemStack to equip (must implement IV2BondageItem)
|
||||
* @return The equip result, or {@link V2EquipResult#BLOCKED} if invalid
|
||||
*/
|
||||
public static V2EquipResult equipItem(LivingEntity entity, ItemStack stack) {
|
||||
public static V2EquipResult equipItem(
|
||||
LivingEntity entity,
|
||||
ItemStack stack
|
||||
) {
|
||||
if (entity.level().isClientSide) return V2EquipResult.BLOCKED;
|
||||
if (stack == null || stack.isEmpty()) return V2EquipResult.BLOCKED;
|
||||
if (!(stack.getItem() instanceof IV2BondageItem item)) return V2EquipResult.BLOCKED;
|
||||
if (
|
||||
!(stack.getItem() instanceof IV2BondageItem item)
|
||||
) return V2EquipResult.BLOCKED;
|
||||
|
||||
IV2BondageEquipment equip = getEquipment(entity);
|
||||
if (equip == null) return V2EquipResult.BLOCKED;
|
||||
@@ -122,7 +138,12 @@ public final class V2EquipmentHelper {
|
||||
|
||||
// Copy the stack so the original isn't mutated
|
||||
ItemStack equipCopy = stack.copy();
|
||||
V2EquipResult result = V2EquipmentManager.tryEquip(equip, item, equipCopy, entity);
|
||||
V2EquipResult result = V2EquipmentManager.tryEquip(
|
||||
equip,
|
||||
item,
|
||||
equipCopy,
|
||||
entity
|
||||
);
|
||||
|
||||
if (result.isSuccess()) {
|
||||
item.onEquipped(equipCopy, entity);
|
||||
@@ -139,7 +160,10 @@ public final class V2EquipmentHelper {
|
||||
* @param region The region to unequip from
|
||||
* @return The removed ItemStack, or {@link ItemStack#EMPTY} if nothing removed
|
||||
*/
|
||||
public static ItemStack unequipFromRegion(LivingEntity entity, BodyRegionV2 region) {
|
||||
public static ItemStack unequipFromRegion(
|
||||
LivingEntity entity,
|
||||
BodyRegionV2 region
|
||||
) {
|
||||
return unequipFromRegion(entity, region, false);
|
||||
}
|
||||
|
||||
@@ -225,13 +249,13 @@ public final class V2EquipmentHelper {
|
||||
}
|
||||
|
||||
PacketSyncV2Equipment packet = new PacketSyncV2Equipment(
|
||||
entity.getId(), equip.serializeNBT()
|
||||
entity.getId(),
|
||||
equip.serializeNBT()
|
||||
);
|
||||
|
||||
if (entity instanceof ServerPlayer serverPlayer) {
|
||||
ModNetwork.sendToAllTrackingAndSelf(packet, serverPlayer);
|
||||
} else {
|
||||
// Phase 6: NPC support — send to all tracking the entity
|
||||
ModNetwork.sendToAllTrackingEntity(packet, entity);
|
||||
}
|
||||
}
|
||||
@@ -250,8 +274,9 @@ public final class V2EquipmentHelper {
|
||||
if (equip == null) return;
|
||||
|
||||
PacketSyncV2Equipment packet = new PacketSyncV2Equipment(
|
||||
entity.getId(), equip.serializeNBT()
|
||||
entity.getId(),
|
||||
equip.serializeNBT()
|
||||
);
|
||||
ModNetwork.sendToPlayer(packet, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,9 @@ import com.tiedup.remake.v2.furniture.SeatDefinition;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import net.minecraft.client.model.HumanoidModel;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.client.player.AbstractClientPlayer;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.client.renderer.entity.LivingEntityRenderer;
|
||||
import net.minecraft.client.renderer.entity.RenderLayerParent;
|
||||
import net.minecraft.client.renderer.entity.layers.RenderLayer;
|
||||
@@ -51,8 +51,10 @@ import org.joml.Matrix4f;
|
||||
* ALL entities (no local-player-only guard).
|
||||
*/
|
||||
@OnlyIn(Dist.CLIENT)
|
||||
public class V2BondageRenderLayer<T extends LivingEntity, M extends HumanoidModel<T>>
|
||||
extends RenderLayer<T, M> {
|
||||
public class V2BondageRenderLayer<
|
||||
T extends LivingEntity,
|
||||
M extends HumanoidModel<T>
|
||||
> extends RenderLayer<T, M> {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger("GltfPipeline");
|
||||
|
||||
@@ -84,9 +86,9 @@ public class V2BondageRenderLayer<T extends LivingEntity, M extends HumanoidMode
|
||||
// Get V2 equipment via capability (Players) or IV2EquipmentHolder (Damsels)
|
||||
IV2BondageEquipment equipment = null;
|
||||
if (entity instanceof Player player) {
|
||||
equipment = player.getCapability(
|
||||
V2BondageEquipmentProvider.V2_BONDAGE_EQUIPMENT
|
||||
).orElse(null);
|
||||
equipment = player
|
||||
.getCapability(V2BondageEquipmentProvider.V2_BONDAGE_EQUIPMENT)
|
||||
.orElse(null);
|
||||
} else if (entity instanceof IV2EquipmentHolder holder) {
|
||||
equipment = holder.getV2Equipment();
|
||||
}
|
||||
@@ -102,7 +104,10 @@ public class V2BondageRenderLayer<T extends LivingEntity, M extends HumanoidMode
|
||||
|
||||
// Skip rendering items in regions blocked by furniture/seat provider
|
||||
Set<BodyRegionV2> furnitureBlocked = Set.of();
|
||||
if (entity.isPassenger() && entity.getVehicle() instanceof ISeatProvider provider) {
|
||||
if (
|
||||
entity.isPassenger() &&
|
||||
entity.getVehicle() instanceof ISeatProvider provider
|
||||
) {
|
||||
SeatDefinition seat = provider.getSeatForPassenger(entity);
|
||||
if (seat != null) {
|
||||
furnitureBlocked = seat.blockedRegions();
|
||||
@@ -133,7 +138,8 @@ public class V2BondageRenderLayer<T extends LivingEntity, M extends HumanoidMode
|
||||
} else {
|
||||
isSlim = false;
|
||||
}
|
||||
ResourceLocation modelLocation = (isSlim && bondageItem.supportsSlimModel(stack))
|
||||
ResourceLocation modelLocation = (isSlim &&
|
||||
bondageItem.supportsSlimModel(stack))
|
||||
? bondageItem.getSlimModelLocation(stack)
|
||||
: null;
|
||||
if (modelLocation == null) {
|
||||
@@ -146,16 +152,22 @@ public class V2BondageRenderLayer<T extends LivingEntity, M extends HumanoidMode
|
||||
// Load GLB data from cache
|
||||
GltfData data = GltfCache.get(modelLocation);
|
||||
if (data == null) {
|
||||
LOGGER.debug("[V2Render] Failed to load GLB for item {}: {}",
|
||||
stack.getItem(), modelLocation);
|
||||
LOGGER.debug(
|
||||
"[V2Render] Failed to load GLB for item {}: {}",
|
||||
stack.getItem(),
|
||||
modelLocation
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Compute joint matrices for this specific GLB model
|
||||
// Each GLB has its own skeleton, so matrices are per-item
|
||||
Matrix4f[] joints = GltfLiveBoneReader.computeJointMatricesFromModel(
|
||||
parentModel, data, entity
|
||||
);
|
||||
Matrix4f[] joints =
|
||||
GltfLiveBoneReader.computeJointMatricesFromModel(
|
||||
parentModel,
|
||||
data,
|
||||
entity
|
||||
);
|
||||
if (joints == null) {
|
||||
// Fallback to GLB-internal skinning
|
||||
joints = GltfSkinningEngine.computeJointMatrices(data);
|
||||
@@ -169,20 +181,31 @@ public class V2BondageRenderLayer<T extends LivingEntity, M extends HumanoidMode
|
||||
Map<String, Integer> tintColors = TintColorResolver.resolve(stack);
|
||||
if (!tintColors.isEmpty() && data.primitives().size() > 1) {
|
||||
// Multi-primitive mesh with tint data: render per-primitive with colors
|
||||
RenderType renderType = GltfMeshRenderer.getRenderTypeForDefaultTexture();
|
||||
RenderType renderType =
|
||||
GltfMeshRenderer.getRenderTypeForDefaultTexture();
|
||||
GltfMeshRenderer.renderSkinnedTinted(
|
||||
data, joints, poseStack, buffer,
|
||||
packedLight, packedOverlay, renderType, tintColors
|
||||
data,
|
||||
joints,
|
||||
poseStack,
|
||||
buffer,
|
||||
packedLight,
|
||||
packedOverlay,
|
||||
renderType,
|
||||
tintColors
|
||||
);
|
||||
} else {
|
||||
// Standard path: single primitive or no tint data
|
||||
GltfMeshRenderer.renderSkinned(
|
||||
data, joints, poseStack, buffer,
|
||||
packedLight, packedOverlay
|
||||
data,
|
||||
joints,
|
||||
poseStack,
|
||||
buffer,
|
||||
packedLight,
|
||||
packedOverlay
|
||||
);
|
||||
}
|
||||
|
||||
poseStack.popPose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,12 +137,18 @@ public class DataDrivenBondageItem extends AbstractV2BondageItem {
|
||||
IV2BondageEquipment equip = V2EquipmentHelper.getEquipment(entity);
|
||||
if (equip != null) {
|
||||
int maxDifficulty = -1;
|
||||
for (Map.Entry<BodyRegionV2, ItemStack> entry : equip.getAllEquipped().entrySet()) {
|
||||
for (Map.Entry<BodyRegionV2, ItemStack> entry : equip
|
||||
.getAllEquipped()
|
||||
.entrySet()) {
|
||||
ItemStack stack = entry.getValue();
|
||||
if (stack.getItem() == this) {
|
||||
DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack);
|
||||
DataDrivenItemDefinition def =
|
||||
DataDrivenItemRegistry.get(stack);
|
||||
if (def != null) {
|
||||
maxDifficulty = Math.max(maxDifficulty, def.escapeDifficulty());
|
||||
maxDifficulty = Math.max(
|
||||
maxDifficulty,
|
||||
def.escapeDifficulty()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -157,12 +163,18 @@ public class DataDrivenBondageItem extends AbstractV2BondageItem {
|
||||
@Override
|
||||
public void notifyStruggle(LivingEntity entity) {
|
||||
// Play a generic chain sound for data-driven items
|
||||
entity.level().playSound(
|
||||
null, entity.getX(), entity.getY(), entity.getZ(),
|
||||
net.minecraft.sounds.SoundEvents.CHAIN_STEP,
|
||||
net.minecraft.sounds.SoundSource.PLAYERS,
|
||||
0.4f, 1.0f
|
||||
);
|
||||
entity
|
||||
.level()
|
||||
.playSound(
|
||||
null,
|
||||
entity.getX(),
|
||||
entity.getY(),
|
||||
entity.getZ(),
|
||||
net.minecraft.sounds.SoundEvents.CHAIN_STEP,
|
||||
net.minecraft.sounds.SoundSource.PLAYERS,
|
||||
0.4f,
|
||||
1.0f
|
||||
);
|
||||
}
|
||||
|
||||
// ===== DISPLAY NAME =====
|
||||
@@ -189,7 +201,9 @@ public class DataDrivenBondageItem extends AbstractV2BondageItem {
|
||||
public static ItemStack createStack(ResourceLocation itemId) {
|
||||
if (V2BondageItems.DATA_DRIVEN_ITEM == null) return ItemStack.EMPTY;
|
||||
ItemStack stack = new ItemStack(V2BondageItems.DATA_DRIVEN_ITEM.get());
|
||||
stack.getOrCreateTag().putString(DataDrivenItemRegistry.NBT_ITEM_ID, itemId.toString());
|
||||
stack
|
||||
.getOrCreateTag()
|
||||
.putString(DataDrivenItemRegistry.NBT_ITEM_ID, itemId.toString());
|
||||
return stack;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,8 @@ public record DataDrivenItemDefinition(
|
||||
* Optional per-item overrides for the movement style's default values.
|
||||
* Requires {@code movementStyle} to be non-null (ignored otherwise).
|
||||
*/
|
||||
@Nullable com.tiedup.remake.v2.bondage.movement.MovementModifier movementModifier,
|
||||
@Nullable
|
||||
com.tiedup.remake.v2.bondage.movement.MovementModifier movementModifier,
|
||||
|
||||
/**
|
||||
* Per-animation bone whitelist. Maps animation name (e.g. "idle", "struggle")
|
||||
|
||||
@@ -52,7 +52,9 @@ import org.jetbrains.annotations.Nullable;
|
||||
*/
|
||||
public final class DataDrivenItemParser {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger("DataDrivenItems");
|
||||
private static final Logger LOGGER = LogManager.getLogger(
|
||||
"DataDrivenItems"
|
||||
);
|
||||
|
||||
private DataDrivenItemParser() {}
|
||||
|
||||
@@ -64,7 +66,10 @@ public final class DataDrivenItemParser {
|
||||
* @return the parsed definition, or null if the file is invalid
|
||||
*/
|
||||
@Nullable
|
||||
public static DataDrivenItemDefinition parse(InputStream input, ResourceLocation fileId) {
|
||||
public static DataDrivenItemDefinition parse(
|
||||
InputStream input,
|
||||
ResourceLocation fileId
|
||||
) {
|
||||
try {
|
||||
JsonObject root = JsonParser.parseReader(
|
||||
new InputStreamReader(input, StandardCharsets.UTF_8)
|
||||
@@ -72,7 +77,11 @@ public final class DataDrivenItemParser {
|
||||
|
||||
return parseObject(root, fileId);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("[DataDrivenItems] Failed to parse JSON {}: {}", fileId, e.getMessage());
|
||||
LOGGER.error(
|
||||
"[DataDrivenItems] Failed to parse JSON {}: {}",
|
||||
fileId,
|
||||
e.getMessage()
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -85,19 +94,28 @@ public final class DataDrivenItemParser {
|
||||
* @return the parsed definition, or null if validation fails
|
||||
*/
|
||||
@Nullable
|
||||
public static DataDrivenItemDefinition parseObject(JsonObject root, ResourceLocation fileId) {
|
||||
public static DataDrivenItemDefinition parseObject(
|
||||
JsonObject root,
|
||||
ResourceLocation fileId
|
||||
) {
|
||||
// Validate type field
|
||||
String type = getStringOrNull(root, "type");
|
||||
if (!"tiedup:bondage_item".equals(type)) {
|
||||
LOGGER.error("[DataDrivenItems] Skipping {}: invalid or missing type '{}' (expected 'tiedup:bondage_item')",
|
||||
fileId, type);
|
||||
LOGGER.error(
|
||||
"[DataDrivenItems] Skipping {}: invalid or missing type '{}' (expected 'tiedup:bondage_item')",
|
||||
fileId,
|
||||
type
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Required: display_name
|
||||
String displayName = getStringOrNull(root, "display_name");
|
||||
if (displayName == null || displayName.isEmpty()) {
|
||||
LOGGER.error("[DataDrivenItems] Skipping {}: missing 'display_name'", fileId);
|
||||
LOGGER.error(
|
||||
"[DataDrivenItems] Skipping {}: missing 'display_name'",
|
||||
fileId
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -107,33 +125,63 @@ public final class DataDrivenItemParser {
|
||||
// Required: model
|
||||
String modelStr = getStringOrNull(root, "model");
|
||||
if (modelStr == null || modelStr.isEmpty()) {
|
||||
LOGGER.error("[DataDrivenItems] Skipping {}: missing 'model'", fileId);
|
||||
LOGGER.error(
|
||||
"[DataDrivenItems] Skipping {}: missing 'model'",
|
||||
fileId
|
||||
);
|
||||
return null;
|
||||
}
|
||||
ResourceLocation modelLocation = ResourceLocation.tryParse(modelStr);
|
||||
if (modelLocation == null) {
|
||||
LOGGER.error("[DataDrivenItems] Skipping {}: invalid model ResourceLocation '{}'", fileId, modelStr);
|
||||
LOGGER.error(
|
||||
"[DataDrivenItems] Skipping {}: invalid model ResourceLocation '{}'",
|
||||
fileId,
|
||||
modelStr
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Optional: slim_model
|
||||
ResourceLocation slimModelLocation = parseOptionalResourceLocation(root, "slim_model", fileId);
|
||||
ResourceLocation slimModelLocation = parseOptionalResourceLocation(
|
||||
root,
|
||||
"slim_model",
|
||||
fileId
|
||||
);
|
||||
|
||||
// Optional: texture
|
||||
ResourceLocation texturePath = parseOptionalResourceLocation(root, "texture", fileId);
|
||||
ResourceLocation texturePath = parseOptionalResourceLocation(
|
||||
root,
|
||||
"texture",
|
||||
fileId
|
||||
);
|
||||
|
||||
// Optional: animation_source
|
||||
ResourceLocation animationSource = parseOptionalResourceLocation(root, "animation_source", fileId);
|
||||
ResourceLocation animationSource = parseOptionalResourceLocation(
|
||||
root,
|
||||
"animation_source",
|
||||
fileId
|
||||
);
|
||||
|
||||
// Required: regions (non-empty)
|
||||
Set<BodyRegionV2> occupiedRegions = parseRegions(root, "regions", fileId);
|
||||
Set<BodyRegionV2> occupiedRegions = parseRegions(
|
||||
root,
|
||||
"regions",
|
||||
fileId
|
||||
);
|
||||
if (occupiedRegions == null || occupiedRegions.isEmpty()) {
|
||||
LOGGER.error("[DataDrivenItems] Skipping {}: missing or empty 'regions'", fileId);
|
||||
LOGGER.error(
|
||||
"[DataDrivenItems] Skipping {}: missing or empty 'regions'",
|
||||
fileId
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Optional: blocked_regions (defaults to regions)
|
||||
Set<BodyRegionV2> blockedRegions = parseRegions(root, "blocked_regions", fileId);
|
||||
Set<BodyRegionV2> blockedRegions = parseRegions(
|
||||
root,
|
||||
"blocked_regions",
|
||||
fileId
|
||||
);
|
||||
if (blockedRegions == null || blockedRegions.isEmpty()) {
|
||||
blockedRegions = occupiedRegions;
|
||||
}
|
||||
@@ -148,13 +196,25 @@ public final class DataDrivenItemParser {
|
||||
boolean lockable = getBooleanOrDefault(root, "lockable", true);
|
||||
|
||||
// Optional: supports_color (default false)
|
||||
boolean supportsColor = getBooleanOrDefault(root, "supports_color", false);
|
||||
boolean supportsColor = getBooleanOrDefault(
|
||||
root,
|
||||
"supports_color",
|
||||
false
|
||||
);
|
||||
|
||||
// Optional: tint_channels (default empty)
|
||||
Map<String, Integer> tintChannels = parseTintChannels(root, "tint_channels", fileId);
|
||||
Map<String, Integer> tintChannels = parseTintChannels(
|
||||
root,
|
||||
"tint_channels",
|
||||
fileId
|
||||
);
|
||||
|
||||
// Optional: icon (item model ResourceLocation for inventory sprite)
|
||||
ResourceLocation icon = parseOptionalResourceLocation(root, "icon", fileId);
|
||||
ResourceLocation icon = parseOptionalResourceLocation(
|
||||
root,
|
||||
"icon",
|
||||
fileId
|
||||
);
|
||||
|
||||
// Optional: movement_style (requires valid MovementStyle name)
|
||||
MovementStyle movementStyle = null;
|
||||
@@ -162,14 +222,21 @@ public final class DataDrivenItemParser {
|
||||
if (movementStyleStr != null && !movementStyleStr.isEmpty()) {
|
||||
movementStyle = MovementStyle.fromName(movementStyleStr);
|
||||
if (movementStyle == null) {
|
||||
LOGGER.warn("[DataDrivenItems] In {}: unknown movement_style '{}', ignoring",
|
||||
fileId, movementStyleStr);
|
||||
LOGGER.warn(
|
||||
"[DataDrivenItems] In {}: unknown movement_style '{}', ignoring",
|
||||
fileId,
|
||||
movementStyleStr
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Optional: movement_modifier (requires movement_style to be set)
|
||||
MovementModifier movementModifier = null;
|
||||
if (movementStyle != null && root.has("movement_modifier") && root.get("movement_modifier").isJsonObject()) {
|
||||
if (
|
||||
movementStyle != null &&
|
||||
root.has("movement_modifier") &&
|
||||
root.get("movement_modifier").isJsonObject()
|
||||
) {
|
||||
JsonObject modObj = root.getAsJsonObject("movement_modifier");
|
||||
Float speedMul = getFloatOrNull(modObj, "speed_multiplier");
|
||||
Boolean jumpDis = getBooleanOrNull(modObj, "jump_disabled");
|
||||
@@ -177,14 +244,22 @@ public final class DataDrivenItemParser {
|
||||
movementModifier = new MovementModifier(speedMul, jumpDis);
|
||||
}
|
||||
} else if (movementStyle == null && root.has("movement_modifier")) {
|
||||
LOGGER.warn("[DataDrivenItems] In {}: movement_modifier ignored because movement_style is absent",
|
||||
fileId);
|
||||
LOGGER.warn(
|
||||
"[DataDrivenItems] In {}: movement_modifier ignored because movement_style is absent",
|
||||
fileId
|
||||
);
|
||||
}
|
||||
|
||||
// Required: animation_bones (per-animation bone whitelist)
|
||||
Map<String, Set<String>> animationBones = parseAnimationBones(root, fileId);
|
||||
Map<String, Set<String>> animationBones = parseAnimationBones(
|
||||
root,
|
||||
fileId
|
||||
);
|
||||
if (animationBones == null) {
|
||||
LOGGER.error("[DataDrivenItems] Skipping {}: missing or invalid 'animation_bones'", fileId);
|
||||
LOGGER.error(
|
||||
"[DataDrivenItems] Skipping {}: missing or invalid 'animation_bones'",
|
||||
fileId
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -200,14 +275,30 @@ public final class DataDrivenItemParser {
|
||||
if (idPath.endsWith(".json")) {
|
||||
idPath = idPath.substring(0, idPath.length() - 5);
|
||||
}
|
||||
ResourceLocation id = new ResourceLocation(fileId.getNamespace(), idPath);
|
||||
ResourceLocation id = new ResourceLocation(
|
||||
fileId.getNamespace(),
|
||||
idPath
|
||||
);
|
||||
|
||||
return new DataDrivenItemDefinition(
|
||||
id, displayName, translationKey, modelLocation, slimModelLocation,
|
||||
texturePath, animationSource, occupiedRegions, blockedRegions,
|
||||
posePriority, escapeDifficulty,
|
||||
lockable, supportsColor, tintChannels, icon,
|
||||
movementStyle, movementModifier, animationBones
|
||||
id,
|
||||
displayName,
|
||||
translationKey,
|
||||
modelLocation,
|
||||
slimModelLocation,
|
||||
texturePath,
|
||||
animationSource,
|
||||
occupiedRegions,
|
||||
blockedRegions,
|
||||
posePriority,
|
||||
escapeDifficulty,
|
||||
lockable,
|
||||
supportsColor,
|
||||
tintChannels,
|
||||
icon,
|
||||
movementStyle,
|
||||
movementModifier,
|
||||
animationBones
|
||||
);
|
||||
}
|
||||
|
||||
@@ -223,7 +314,11 @@ public final class DataDrivenItemParser {
|
||||
}
|
||||
}
|
||||
|
||||
private static int getIntOrDefault(JsonObject obj, String key, int defaultValue) {
|
||||
private static int getIntOrDefault(
|
||||
JsonObject obj,
|
||||
String key,
|
||||
int defaultValue
|
||||
) {
|
||||
if (!obj.has(key) || obj.get(key).isJsonNull()) return defaultValue;
|
||||
try {
|
||||
return obj.get(key).getAsInt();
|
||||
@@ -232,7 +327,11 @@ public final class DataDrivenItemParser {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean getBooleanOrDefault(JsonObject obj, String key, boolean defaultValue) {
|
||||
private static boolean getBooleanOrDefault(
|
||||
JsonObject obj,
|
||||
String key,
|
||||
boolean defaultValue
|
||||
) {
|
||||
if (!obj.has(key) || obj.get(key).isJsonNull()) return defaultValue;
|
||||
try {
|
||||
return obj.get(key).getAsBoolean();
|
||||
@@ -263,13 +362,20 @@ public final class DataDrivenItemParser {
|
||||
|
||||
@Nullable
|
||||
private static ResourceLocation parseOptionalResourceLocation(
|
||||
JsonObject obj, String key, ResourceLocation fileId
|
||||
JsonObject obj,
|
||||
String key,
|
||||
ResourceLocation fileId
|
||||
) {
|
||||
String value = getStringOrNull(obj, key);
|
||||
if (value == null || value.isEmpty()) return null;
|
||||
ResourceLocation loc = ResourceLocation.tryParse(value);
|
||||
if (loc == null) {
|
||||
LOGGER.warn("[DataDrivenItems] In {}: invalid ResourceLocation for '{}': '{}'", fileId, key, value);
|
||||
LOGGER.warn(
|
||||
"[DataDrivenItems] In {}: invalid ResourceLocation for '{}': '{}'",
|
||||
fileId,
|
||||
key,
|
||||
value
|
||||
);
|
||||
}
|
||||
return loc;
|
||||
}
|
||||
@@ -279,7 +385,11 @@ public final class DataDrivenItemParser {
|
||||
* Unknown region names are logged as warnings and skipped.
|
||||
*/
|
||||
@Nullable
|
||||
private static Set<BodyRegionV2> parseRegions(JsonObject obj, String key, ResourceLocation fileId) {
|
||||
private static Set<BodyRegionV2> parseRegions(
|
||||
JsonObject obj,
|
||||
String key,
|
||||
ResourceLocation fileId
|
||||
) {
|
||||
if (!obj.has(key) || !obj.get(key).isJsonArray()) return null;
|
||||
|
||||
JsonArray arr = obj.getAsJsonArray(key);
|
||||
@@ -293,12 +403,20 @@ public final class DataDrivenItemParser {
|
||||
if (region != null) {
|
||||
regions.add(region);
|
||||
} else {
|
||||
LOGGER.warn("[DataDrivenItems] In {}: unknown region '{}' in '{}', skipping",
|
||||
fileId, name, key);
|
||||
LOGGER.warn(
|
||||
"[DataDrivenItems] In {}: unknown region '{}' in '{}', skipping",
|
||||
fileId,
|
||||
name,
|
||||
key
|
||||
);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("[DataDrivenItems] In {}: invalid element in '{}': {}",
|
||||
fileId, key, e.getMessage());
|
||||
LOGGER.warn(
|
||||
"[DataDrivenItems] In {}: invalid element in '{}': {}",
|
||||
fileId,
|
||||
key,
|
||||
e.getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,18 +439,29 @@ public final class DataDrivenItemParser {
|
||||
* @param fileId the source file for error messages
|
||||
* @return an unmodifiable map of channel names to RGB ints, or empty map if absent
|
||||
*/
|
||||
private static Map<String, Integer> parseTintChannels(JsonObject obj, String key, ResourceLocation fileId) {
|
||||
private static Map<String, Integer> parseTintChannels(
|
||||
JsonObject obj,
|
||||
String key,
|
||||
ResourceLocation fileId
|
||||
) {
|
||||
if (!obj.has(key) || !obj.get(key).isJsonObject()) return Map.of();
|
||||
JsonObject channels = obj.getAsJsonObject(key);
|
||||
Map<String, Integer> result = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, JsonElement> entry : channels.entrySet()) {
|
||||
try {
|
||||
String hex = entry.getValue().getAsString();
|
||||
int color = Integer.parseInt(hex.startsWith("#") ? hex.substring(1) : hex, 16);
|
||||
int color = Integer.parseInt(
|
||||
hex.startsWith("#") ? hex.substring(1) : hex,
|
||||
16
|
||||
);
|
||||
result.put(entry.getKey(), color);
|
||||
} catch (NumberFormatException e) {
|
||||
LOGGER.warn("[DataDrivenItems] In {}: invalid hex color '{}' for tint channel '{}'",
|
||||
fileId, entry.getValue(), entry.getKey());
|
||||
LOGGER.warn(
|
||||
"[DataDrivenItems] In {}: invalid hex color '{}' for tint channel '{}'",
|
||||
fileId,
|
||||
entry.getValue(),
|
||||
entry.getKey()
|
||||
);
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableMap(result);
|
||||
@@ -340,7 +469,12 @@ public final class DataDrivenItemParser {
|
||||
|
||||
/** Valid PlayerAnimator bone names for animation_bones validation. */
|
||||
private static final Set<String> VALID_BONE_NAMES = Set.of(
|
||||
"head", "body", "rightArm", "leftArm", "rightLeg", "leftLeg"
|
||||
"head",
|
||||
"body",
|
||||
"rightArm",
|
||||
"leftArm",
|
||||
"rightLeg",
|
||||
"leftLeg"
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -364,14 +498,23 @@ public final class DataDrivenItemParser {
|
||||
* @return unmodifiable map of animation name to bone set, or null if absent/invalid
|
||||
*/
|
||||
@Nullable
|
||||
private static Map<String, Set<String>> parseAnimationBones(JsonObject obj, ResourceLocation fileId) {
|
||||
if (!obj.has("animation_bones") || !obj.get("animation_bones").isJsonObject()) {
|
||||
private static Map<String, Set<String>> parseAnimationBones(
|
||||
JsonObject obj,
|
||||
ResourceLocation fileId
|
||||
) {
|
||||
if (
|
||||
!obj.has("animation_bones") ||
|
||||
!obj.get("animation_bones").isJsonObject()
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
JsonObject bonesObj = obj.getAsJsonObject("animation_bones");
|
||||
if (bonesObj.size() == 0) {
|
||||
LOGGER.error("[DataDrivenItems] In {}: 'animation_bones' is empty", fileId);
|
||||
LOGGER.error(
|
||||
"[DataDrivenItems] In {}: 'animation_bones' is empty",
|
||||
fileId
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -381,8 +524,11 @@ public final class DataDrivenItemParser {
|
||||
JsonElement value = entry.getValue();
|
||||
|
||||
if (!value.isJsonArray()) {
|
||||
LOGGER.warn("[DataDrivenItems] In {}: animation_bones['{}'] is not an array, skipping",
|
||||
fileId, animName);
|
||||
LOGGER.warn(
|
||||
"[DataDrivenItems] In {}: animation_bones['{}'] is not an array, skipping",
|
||||
fileId,
|
||||
animName
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -394,29 +540,41 @@ public final class DataDrivenItemParser {
|
||||
if (VALID_BONE_NAMES.contains(boneName)) {
|
||||
bones.add(boneName);
|
||||
} else {
|
||||
LOGGER.warn("[DataDrivenItems] In {}: animation_bones['{}'] contains unknown bone '{}', skipping",
|
||||
fileId, animName, boneName);
|
||||
LOGGER.warn(
|
||||
"[DataDrivenItems] In {}: animation_bones['{}'] contains unknown bone '{}', skipping",
|
||||
fileId,
|
||||
animName,
|
||||
boneName
|
||||
);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("[DataDrivenItems] In {}: invalid element in animation_bones['{}']",
|
||||
fileId, animName);
|
||||
LOGGER.warn(
|
||||
"[DataDrivenItems] In {}: invalid element in animation_bones['{}']",
|
||||
fileId,
|
||||
animName
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!bones.isEmpty()) {
|
||||
result.put(animName, Collections.unmodifiableSet(bones));
|
||||
} else {
|
||||
LOGGER.warn("[DataDrivenItems] In {}: animation_bones['{}'] resolved to empty set, skipping",
|
||||
fileId, animName);
|
||||
LOGGER.warn(
|
||||
"[DataDrivenItems] In {}: animation_bones['{}'] resolved to empty set, skipping",
|
||||
fileId,
|
||||
animName
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.isEmpty()) {
|
||||
LOGGER.error("[DataDrivenItems] In {}: 'animation_bones' has no valid entries", fileId);
|
||||
LOGGER.error(
|
||||
"[DataDrivenItems] In {}: 'animation_bones' has no valid entries",
|
||||
fileId
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return Collections.unmodifiableMap(result);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -29,7 +29,10 @@ public final class DataDrivenItemRegistry {
|
||||
* Volatile reference to an unmodifiable map. Reload builds a new map
|
||||
* and swaps atomically; consumer threads always see a consistent snapshot.
|
||||
*/
|
||||
private static volatile Map<ResourceLocation, DataDrivenItemDefinition> DEFINITIONS = Map.of();
|
||||
private static volatile Map<
|
||||
ResourceLocation,
|
||||
DataDrivenItemDefinition
|
||||
> DEFINITIONS = Map.of();
|
||||
|
||||
private DataDrivenItemRegistry() {}
|
||||
|
||||
@@ -39,7 +42,9 @@ public final class DataDrivenItemRegistry {
|
||||
*
|
||||
* @param newDefs the new definitions map (will be defensively copied)
|
||||
*/
|
||||
public static void reload(Map<ResourceLocation, DataDrivenItemDefinition> newDefs) {
|
||||
public static void reload(
|
||||
Map<ResourceLocation, DataDrivenItemDefinition> newDefs
|
||||
) {
|
||||
DEFINITIONS = Collections.unmodifiableMap(new HashMap<>(newDefs));
|
||||
}
|
||||
|
||||
@@ -53,8 +58,12 @@ public final class DataDrivenItemRegistry {
|
||||
*
|
||||
* @param newDefs the definitions to merge (will overwrite existing entries with same key)
|
||||
*/
|
||||
public static void mergeAll(Map<ResourceLocation, DataDrivenItemDefinition> newDefs) {
|
||||
Map<ResourceLocation, DataDrivenItemDefinition> merged = new HashMap<>(DEFINITIONS);
|
||||
public static void mergeAll(
|
||||
Map<ResourceLocation, DataDrivenItemDefinition> newDefs
|
||||
) {
|
||||
Map<ResourceLocation, DataDrivenItemDefinition> merged = new HashMap<>(
|
||||
DEFINITIONS
|
||||
);
|
||||
merged.putAll(newDefs);
|
||||
DEFINITIONS = Collections.unmodifiableMap(merged);
|
||||
}
|
||||
@@ -81,7 +90,9 @@ public final class DataDrivenItemRegistry {
|
||||
if (stack.isEmpty()) return null;
|
||||
CompoundTag tag = stack.getTag();
|
||||
if (tag == null || !tag.contains(NBT_ITEM_ID)) return null;
|
||||
ResourceLocation id = ResourceLocation.tryParse(tag.getString(NBT_ITEM_ID));
|
||||
ResourceLocation id = ResourceLocation.tryParse(
|
||||
tag.getString(NBT_ITEM_ID)
|
||||
);
|
||||
if (id == null) return null;
|
||||
return DEFINITIONS.get(id);
|
||||
}
|
||||
|
||||
@@ -21,50 +21,80 @@ import org.apache.logging.log4j.Logger;
|
||||
* <p>Follows the same pattern as {@link com.tiedup.remake.client.animation.context.ContextGlbRegistry}:
|
||||
* prepare phase is a no-op, apply phase scans + parses + atomic-swaps the registry.</p>
|
||||
*/
|
||||
public class DataDrivenItemReloadListener extends SimplePreparableReloadListener<Void> {
|
||||
public class DataDrivenItemReloadListener
|
||||
extends SimplePreparableReloadListener<Void>
|
||||
{
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger("DataDrivenItems");
|
||||
private static final Logger LOGGER = LogManager.getLogger(
|
||||
"DataDrivenItems"
|
||||
);
|
||||
|
||||
/** Resource directory containing item definition JSON files. */
|
||||
private static final String DIRECTORY = "tiedup_items";
|
||||
|
||||
@Override
|
||||
protected Void prepare(ResourceManager resourceManager, ProfilerFiller profiler) {
|
||||
protected Void prepare(
|
||||
ResourceManager resourceManager,
|
||||
ProfilerFiller profiler
|
||||
) {
|
||||
// No preparation needed — parsing happens in apply phase
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void apply(Void nothing, ResourceManager resourceManager, ProfilerFiller profiler) {
|
||||
Map<ResourceLocation, DataDrivenItemDefinition> newDefs = new HashMap<>();
|
||||
protected void apply(
|
||||
Void nothing,
|
||||
ResourceManager resourceManager,
|
||||
ProfilerFiller profiler
|
||||
) {
|
||||
Map<ResourceLocation, DataDrivenItemDefinition> newDefs =
|
||||
new HashMap<>();
|
||||
|
||||
Map<ResourceLocation, Resource> resources = resourceManager.listResources(
|
||||
DIRECTORY, loc -> loc.getPath().endsWith(".json")
|
||||
);
|
||||
Map<ResourceLocation, Resource> resources =
|
||||
resourceManager.listResources(DIRECTORY, loc ->
|
||||
loc.getPath().endsWith(".json")
|
||||
);
|
||||
|
||||
int skipped = 0;
|
||||
|
||||
for (Map.Entry<ResourceLocation, Resource> entry : resources.entrySet()) {
|
||||
for (Map.Entry<
|
||||
ResourceLocation,
|
||||
Resource
|
||||
> entry : resources.entrySet()) {
|
||||
ResourceLocation fileId = entry.getKey();
|
||||
Resource resource = entry.getValue();
|
||||
|
||||
try (InputStream input = resource.open()) {
|
||||
DataDrivenItemDefinition def = DataDrivenItemParser.parse(input, fileId);
|
||||
DataDrivenItemDefinition def = DataDrivenItemParser.parse(
|
||||
input,
|
||||
fileId
|
||||
);
|
||||
|
||||
if (def != null) {
|
||||
// Check for duplicate IDs
|
||||
if (newDefs.containsKey(def.id())) {
|
||||
LOGGER.warn("[DataDrivenItems] Duplicate item ID '{}' from file '{}' — overwriting previous definition",
|
||||
def.id(), fileId);
|
||||
LOGGER.warn(
|
||||
"[DataDrivenItems] Duplicate item ID '{}' from file '{}' — overwriting previous definition",
|
||||
def.id(),
|
||||
fileId
|
||||
);
|
||||
}
|
||||
|
||||
newDefs.put(def.id(), def);
|
||||
LOGGER.debug("[DataDrivenItems] Loaded: {} -> '{}'", def.id(), def.displayName());
|
||||
LOGGER.debug(
|
||||
"[DataDrivenItems] Loaded: {} -> '{}'",
|
||||
def.id(),
|
||||
def.displayName()
|
||||
);
|
||||
} else {
|
||||
skipped++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("[DataDrivenItems] Failed to read resource {}: {}", fileId, e.getMessage());
|
||||
LOGGER.error(
|
||||
"[DataDrivenItems] Failed to read resource {}: {}",
|
||||
fileId,
|
||||
e.getMessage()
|
||||
);
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
@@ -73,7 +103,11 @@ public class DataDrivenItemReloadListener extends SimplePreparableReloadListener
|
||||
// overwrite client-only definitions on integrated server
|
||||
DataDrivenItemRegistry.mergeAll(newDefs);
|
||||
|
||||
LOGGER.info("[DataDrivenItems] Loaded {} item definitions ({} skipped) from {} JSON files",
|
||||
newDefs.size(), skipped, resources.size());
|
||||
LOGGER.info(
|
||||
"[DataDrivenItems] Loaded {} item definitions ({} skipped) from {} JSON files",
|
||||
newDefs.size(),
|
||||
skipped,
|
||||
resources.size()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,50 +23,80 @@ import org.apache.logging.log4j.Logger;
|
||||
* <p>Registered via {@link net.minecraftforge.event.AddReloadListenerEvent} in
|
||||
* {@link com.tiedup.remake.core.TiedUpMod.ForgeEvents}.</p>
|
||||
*/
|
||||
public class DataDrivenItemServerReloadListener extends SimplePreparableReloadListener<Void> {
|
||||
public class DataDrivenItemServerReloadListener
|
||||
extends SimplePreparableReloadListener<Void>
|
||||
{
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger("DataDrivenItems");
|
||||
private static final Logger LOGGER = LogManager.getLogger(
|
||||
"DataDrivenItems"
|
||||
);
|
||||
|
||||
/** Resource directory containing item definition JSON files (under data/). */
|
||||
private static final String DIRECTORY = "tiedup_items";
|
||||
|
||||
@Override
|
||||
protected Void prepare(ResourceManager resourceManager, ProfilerFiller profiler) {
|
||||
protected Void prepare(
|
||||
ResourceManager resourceManager,
|
||||
ProfilerFiller profiler
|
||||
) {
|
||||
// No preparation needed -- parsing happens in apply phase
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void apply(Void nothing, ResourceManager resourceManager, ProfilerFiller profiler) {
|
||||
Map<ResourceLocation, DataDrivenItemDefinition> newDefs = new HashMap<>();
|
||||
protected void apply(
|
||||
Void nothing,
|
||||
ResourceManager resourceManager,
|
||||
ProfilerFiller profiler
|
||||
) {
|
||||
Map<ResourceLocation, DataDrivenItemDefinition> newDefs =
|
||||
new HashMap<>();
|
||||
|
||||
Map<ResourceLocation, Resource> resources = resourceManager.listResources(
|
||||
DIRECTORY, loc -> loc.getPath().endsWith(".json")
|
||||
);
|
||||
Map<ResourceLocation, Resource> resources =
|
||||
resourceManager.listResources(DIRECTORY, loc ->
|
||||
loc.getPath().endsWith(".json")
|
||||
);
|
||||
|
||||
int skipped = 0;
|
||||
|
||||
for (Map.Entry<ResourceLocation, Resource> entry : resources.entrySet()) {
|
||||
for (Map.Entry<
|
||||
ResourceLocation,
|
||||
Resource
|
||||
> entry : resources.entrySet()) {
|
||||
ResourceLocation fileId = entry.getKey();
|
||||
Resource resource = entry.getValue();
|
||||
|
||||
try (InputStream input = resource.open()) {
|
||||
DataDrivenItemDefinition def = DataDrivenItemParser.parse(input, fileId);
|
||||
DataDrivenItemDefinition def = DataDrivenItemParser.parse(
|
||||
input,
|
||||
fileId
|
||||
);
|
||||
|
||||
if (def != null) {
|
||||
// Check for duplicate IDs
|
||||
if (newDefs.containsKey(def.id())) {
|
||||
LOGGER.warn("[DataDrivenItems] Server: Duplicate item ID '{}' from file '{}' -- overwriting previous definition",
|
||||
def.id(), fileId);
|
||||
LOGGER.warn(
|
||||
"[DataDrivenItems] Server: Duplicate item ID '{}' from file '{}' -- overwriting previous definition",
|
||||
def.id(),
|
||||
fileId
|
||||
);
|
||||
}
|
||||
|
||||
newDefs.put(def.id(), def);
|
||||
LOGGER.debug("[DataDrivenItems] Server loaded: {} -> '{}'", def.id(), def.displayName());
|
||||
LOGGER.debug(
|
||||
"[DataDrivenItems] Server loaded: {} -> '{}'",
|
||||
def.id(),
|
||||
def.displayName()
|
||||
);
|
||||
} else {
|
||||
skipped++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("[DataDrivenItems] Server: Failed to read resource {}: {}", fileId, e.getMessage());
|
||||
LOGGER.error(
|
||||
"[DataDrivenItems] Server: Failed to read resource {}: {}",
|
||||
fileId,
|
||||
e.getMessage()
|
||||
);
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
@@ -75,7 +105,11 @@ public class DataDrivenItemServerReloadListener extends SimplePreparableReloadLi
|
||||
// definitions aren't overwritten on integrated server
|
||||
DataDrivenItemRegistry.mergeAll(newDefs);
|
||||
|
||||
LOGGER.info("[DataDrivenItems] Server loaded {} item definitions ({} skipped) from {} JSON files",
|
||||
newDefs.size(), skipped, resources.size());
|
||||
LOGGER.info(
|
||||
"[DataDrivenItems] Server loaded {} item definitions ({} skipped) from {} JSON files",
|
||||
newDefs.size(),
|
||||
skipped,
|
||||
resources.size()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,10 @@ import org.jetbrains.annotations.Nullable;
|
||||
* Subclasses implement: getOccupiedRegions(), getModelLocation(), getPosePriority(),
|
||||
* getResistanceId(), notifyStruggle().
|
||||
*/
|
||||
public abstract class AbstractV2BondageItem extends Item
|
||||
implements IV2BondageItem, ILockable, IHasResistance {
|
||||
public abstract class AbstractV2BondageItem
|
||||
extends Item
|
||||
implements IV2BondageItem, ILockable, IHasResistance
|
||||
{
|
||||
|
||||
protected AbstractV2BondageItem(Properties properties) {
|
||||
super(properties);
|
||||
@@ -45,7 +47,11 @@ public abstract class AbstractV2BondageItem extends Item
|
||||
// Right-click in air does nothing for self-equip — consistent with V1 behavior.
|
||||
|
||||
@Override
|
||||
public InteractionResultHolder<ItemStack> use(Level level, Player player, InteractionHand hand) {
|
||||
public InteractionResultHolder<ItemStack> use(
|
||||
Level level,
|
||||
Player player,
|
||||
InteractionHand hand
|
||||
) {
|
||||
return InteractionResultHolder.pass(player.getItemInHand(hand));
|
||||
}
|
||||
|
||||
@@ -53,7 +59,10 @@ public abstract class AbstractV2BondageItem extends Item
|
||||
|
||||
@Override
|
||||
public InteractionResult interactLivingEntity(
|
||||
ItemStack stack, Player player, LivingEntity target, InteractionHand hand
|
||||
ItemStack stack,
|
||||
Player player,
|
||||
LivingEntity target,
|
||||
InteractionHand hand
|
||||
) {
|
||||
// Client returns SUCCESS for arm swing animation. Server may reject —
|
||||
// minor visual desync is accepted Forge pattern (same as vanilla food/bow).
|
||||
@@ -94,7 +103,10 @@ public abstract class AbstractV2BondageItem extends Item
|
||||
|
||||
@Override
|
||||
public void appendHoverText(
|
||||
ItemStack stack, @Nullable Level level, List<Component> tooltip, TooltipFlag flag
|
||||
ItemStack stack,
|
||||
@Nullable Level level,
|
||||
List<Component> tooltip,
|
||||
TooltipFlag flag
|
||||
) {
|
||||
super.appendHoverText(stack, level, tooltip, flag);
|
||||
// Lock status from ILockable
|
||||
@@ -102,19 +114,29 @@ public abstract class AbstractV2BondageItem extends Item
|
||||
// Escape difficulty
|
||||
int difficulty = getEscapeDifficulty(stack);
|
||||
if (difficulty > 0) {
|
||||
tooltip.add(Component.translatable("item.tiedup.tooltip.escape_difficulty", difficulty)
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
tooltip.add(
|
||||
Component.translatable(
|
||||
"item.tiedup.tooltip.escape_difficulty",
|
||||
difficulty
|
||||
).withStyle(ChatFormatting.GRAY)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== IV2BondageItem DEFAULTS =====
|
||||
|
||||
@Override
|
||||
public int getEscapeDifficulty() { return 0; }
|
||||
public int getEscapeDifficulty() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsColor() { return false; }
|
||||
public boolean supportsColor() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSlimModel() { return false; }
|
||||
public boolean supportsSlimModel() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ public class V2Handcuffs extends AbstractV2BondageItem {
|
||||
Collections.unmodifiableSet(EnumSet.of(BodyRegionV2.ARMS));
|
||||
|
||||
private static final ResourceLocation MODEL = new ResourceLocation(
|
||||
TiedUpMod.MOD_ID, "models/gltf/v2/handcuffs/cuffs_prototype.glb"
|
||||
TiedUpMod.MOD_ID,
|
||||
"models/gltf/v2/handcuffs/cuffs_prototype.glb"
|
||||
);
|
||||
|
||||
public V2Handcuffs() {
|
||||
@@ -28,27 +29,43 @@ public class V2Handcuffs extends AbstractV2BondageItem {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<BodyRegionV2> getOccupiedRegions() { return REGIONS; }
|
||||
public Set<BodyRegionV2> getOccupiedRegions() {
|
||||
return REGIONS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceLocation getModelLocation() { return MODEL; }
|
||||
public ResourceLocation getModelLocation() {
|
||||
return MODEL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPosePriority() { return 30; }
|
||||
public int getPosePriority() {
|
||||
return 30;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEscapeDifficulty() { return 100; }
|
||||
public int getEscapeDifficulty() {
|
||||
return 100;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getResistanceId() { return "handcuffs"; }
|
||||
public String getResistanceId() {
|
||||
return "handcuffs";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyStruggle(LivingEntity entity) {
|
||||
entity.level().playSound(
|
||||
null, entity.getX(), entity.getY(), entity.getZ(),
|
||||
net.minecraft.sounds.SoundEvents.CHAIN_STEP,
|
||||
net.minecraft.sounds.SoundSource.PLAYERS,
|
||||
0.4f, 1.0f
|
||||
);
|
||||
entity
|
||||
.level()
|
||||
.playSound(
|
||||
null,
|
||||
entity.getX(),
|
||||
entity.getY(),
|
||||
entity.getZ(),
|
||||
net.minecraft.sounds.SoundEvents.CHAIN_STEP,
|
||||
net.minecraft.sounds.SoundSource.PLAYERS,
|
||||
0.4f,
|
||||
1.0f
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.tiedup.remake.v2.bondage.movement;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Movement styles that change how a bound player physically moves.
|
||||
@@ -16,7 +16,6 @@ import com.tiedup.remake.v2.BodyRegionV2;
|
||||
* to avoid pulling server-only classes into client code.</p>
|
||||
*/
|
||||
public enum MovementStyle {
|
||||
|
||||
/** Swaying side-to-side gait, visual zigzag via animation. Jump allowed. */
|
||||
WADDLE(1, 0.6f, false),
|
||||
|
||||
@@ -33,7 +32,11 @@ public enum MovementStyle {
|
||||
private final float defaultSpeedMultiplier;
|
||||
private final boolean defaultJumpDisabled;
|
||||
|
||||
MovementStyle(int severity, float defaultSpeedMultiplier, boolean defaultJumpDisabled) {
|
||||
MovementStyle(
|
||||
int severity,
|
||||
float defaultSpeedMultiplier,
|
||||
boolean defaultJumpDisabled
|
||||
) {
|
||||
this.severity = severity;
|
||||
this.defaultSpeedMultiplier = defaultSpeedMultiplier;
|
||||
this.defaultJumpDisabled = defaultJumpDisabled;
|
||||
|
||||
@@ -61,18 +61,23 @@ public class MovementStyleManager {
|
||||
// RestraintEffectUtils used this UUID with ADDITION operation and addPermanentModifier().
|
||||
// Players upgrading from V1 may still have this modifier saved in their NBT.
|
||||
// Removed on tick to prevent double stacking with V2 MULTIPLY_BASE modifiers.
|
||||
private static final UUID V1_BIND_SPEED_MODIFIER_UUID =
|
||||
UUID.fromString("7f3c7c8e-9d4e-4c7a-8e5f-1a2b3c4d5e6f");
|
||||
private static final UUID V1_BIND_SPEED_MODIFIER_UUID = UUID.fromString(
|
||||
"7f3c7c8e-9d4e-4c7a-8e5f-1a2b3c4d5e6f"
|
||||
);
|
||||
|
||||
// --- Unique UUIDs for AttributeModifiers (one per style to allow clean removal) ---
|
||||
private static final UUID WADDLE_SPEED_UUID =
|
||||
UUID.fromString("d7a1c001-0000-0000-0000-000000000001");
|
||||
private static final UUID SHUFFLE_SPEED_UUID =
|
||||
UUID.fromString("d7a1c001-0000-0000-0000-000000000002");
|
||||
private static final UUID HOP_SPEED_UUID =
|
||||
UUID.fromString("d7a1c001-0000-0000-0000-000000000003");
|
||||
private static final UUID CRAWL_SPEED_UUID =
|
||||
UUID.fromString("d7a1c001-0000-0000-0000-000000000004");
|
||||
private static final UUID WADDLE_SPEED_UUID = UUID.fromString(
|
||||
"d7a1c001-0000-0000-0000-000000000001"
|
||||
);
|
||||
private static final UUID SHUFFLE_SPEED_UUID = UUID.fromString(
|
||||
"d7a1c001-0000-0000-0000-000000000002"
|
||||
);
|
||||
private static final UUID HOP_SPEED_UUID = UUID.fromString(
|
||||
"d7a1c001-0000-0000-0000-000000000003"
|
||||
);
|
||||
private static final UUID CRAWL_SPEED_UUID = UUID.fromString(
|
||||
"d7a1c001-0000-0000-0000-000000000004"
|
||||
);
|
||||
|
||||
// --- Hop tuning constants ---
|
||||
private static final double HOP_Y_IMPULSE = 0.28;
|
||||
@@ -119,7 +124,11 @@ public class MovementStyleManager {
|
||||
// --- Skip conditions ---
|
||||
// Update last position even when suspended to prevent false movement
|
||||
// detection on resume (e.g., teleport while riding)
|
||||
if (player.isPassenger() || player.isDeadOrDying() || state.isStruggling()) {
|
||||
if (
|
||||
player.isPassenger() ||
|
||||
player.isDeadOrDying() ||
|
||||
state.isStruggling()
|
||||
) {
|
||||
state.lastX = player.getX();
|
||||
state.lastY = player.getY();
|
||||
state.lastZ = player.getZ();
|
||||
@@ -138,8 +147,8 @@ public class MovementStyleManager {
|
||||
|
||||
// --- Resolve current style from equipped items ---
|
||||
IV2BondageEquipment equipment = V2EquipmentHelper.getEquipment(player);
|
||||
Map<BodyRegionV2, ItemStack> equipped = equipment != null
|
||||
? equipment.getAllEquipped() : Map.of();
|
||||
Map<BodyRegionV2, ItemStack> equipped =
|
||||
equipment != null ? equipment.getAllEquipped() : Map.of();
|
||||
ResolvedMovement resolved = MovementStyleResolver.resolve(equipped);
|
||||
|
||||
// --- Compare with current active style ---
|
||||
@@ -163,7 +172,9 @@ public class MovementStyleManager {
|
||||
|
||||
// Sync to all tracking clients (animation + crawl pose)
|
||||
ModNetwork.sendToAllTrackingAndSelf(
|
||||
new PacketSyncMovementStyle(player.getUUID(), newStyle), player);
|
||||
new PacketSyncMovementStyle(player.getUUID(), newStyle),
|
||||
player
|
||||
);
|
||||
}
|
||||
|
||||
// --- Per-style tick ---
|
||||
@@ -229,15 +240,18 @@ public class MovementStyleManager {
|
||||
private static double getJumpBoostFactor(Player player) {
|
||||
var jumpBoost = player.getEffect(MobEffects.JUMP);
|
||||
if (jumpBoost != null) {
|
||||
return 1.0 + (jumpBoost.getAmplifier() + 1) * 0.1 / 0.42;
|
||||
return 1.0 + ((jumpBoost.getAmplifier() + 1) * 0.1) / 0.42;
|
||||
}
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
// ==================== Lifecycle ====================
|
||||
|
||||
private static void onActivate(ServerPlayer player, PlayerBindState state,
|
||||
MovementStyle style) {
|
||||
private static void onActivate(
|
||||
ServerPlayer player,
|
||||
PlayerBindState state,
|
||||
MovementStyle style
|
||||
) {
|
||||
switch (style) {
|
||||
case WADDLE -> activateWaddle(player, state);
|
||||
case SHUFFLE -> activateShuffle(player, state);
|
||||
@@ -246,8 +260,11 @@ public class MovementStyleManager {
|
||||
}
|
||||
}
|
||||
|
||||
private static void onDeactivate(ServerPlayer player, PlayerBindState state,
|
||||
MovementStyle style) {
|
||||
private static void onDeactivate(
|
||||
ServerPlayer player,
|
||||
PlayerBindState state,
|
||||
MovementStyle style
|
||||
) {
|
||||
switch (style) {
|
||||
case WADDLE -> deactivateWaddle(player, state);
|
||||
case SHUFFLE -> deactivateShuffle(player, state);
|
||||
@@ -267,12 +284,22 @@ public class MovementStyleManager {
|
||||
|
||||
// ==================== Waddle ====================
|
||||
|
||||
private static void activateWaddle(ServerPlayer player, PlayerBindState state) {
|
||||
applySpeedModifier(player, WADDLE_SPEED_UUID, "tiedup.waddle_speed",
|
||||
state.getResolvedMovementSpeed());
|
||||
private static void activateWaddle(
|
||||
ServerPlayer player,
|
||||
PlayerBindState state
|
||||
) {
|
||||
applySpeedModifier(
|
||||
player,
|
||||
WADDLE_SPEED_UUID,
|
||||
"tiedup.waddle_speed",
|
||||
state.getResolvedMovementSpeed()
|
||||
);
|
||||
}
|
||||
|
||||
private static void deactivateWaddle(ServerPlayer player, PlayerBindState state) {
|
||||
private static void deactivateWaddle(
|
||||
ServerPlayer player,
|
||||
PlayerBindState state
|
||||
) {
|
||||
removeSpeedModifier(player, WADDLE_SPEED_UUID);
|
||||
}
|
||||
|
||||
@@ -283,31 +310,54 @@ public class MovementStyleManager {
|
||||
|
||||
// ==================== Shuffle ====================
|
||||
|
||||
private static void activateShuffle(ServerPlayer player, PlayerBindState state) {
|
||||
applySpeedModifier(player, SHUFFLE_SPEED_UUID, "tiedup.shuffle_speed",
|
||||
state.getResolvedMovementSpeed());
|
||||
private static void activateShuffle(
|
||||
ServerPlayer player,
|
||||
PlayerBindState state
|
||||
) {
|
||||
applySpeedModifier(
|
||||
player,
|
||||
SHUFFLE_SPEED_UUID,
|
||||
"tiedup.shuffle_speed",
|
||||
state.getResolvedMovementSpeed()
|
||||
);
|
||||
}
|
||||
|
||||
private static void deactivateShuffle(ServerPlayer player, PlayerBindState state) {
|
||||
private static void deactivateShuffle(
|
||||
ServerPlayer player,
|
||||
PlayerBindState state
|
||||
) {
|
||||
removeSpeedModifier(player, SHUFFLE_SPEED_UUID);
|
||||
}
|
||||
|
||||
private static void tickShuffle(ServerPlayer player, PlayerBindState state) {
|
||||
private static void tickShuffle(
|
||||
ServerPlayer player,
|
||||
PlayerBindState state
|
||||
) {
|
||||
// Shuffle: speed reduction via attribute is sufficient. No per-tick work.
|
||||
}
|
||||
|
||||
// ==================== Hop ====================
|
||||
|
||||
private static void activateHop(ServerPlayer player, PlayerBindState state) {
|
||||
private static void activateHop(
|
||||
ServerPlayer player,
|
||||
PlayerBindState state
|
||||
) {
|
||||
// Apply base speed reduction (~15% base speed between hops)
|
||||
applySpeedModifier(player, HOP_SPEED_UUID, "tiedup.hop_speed",
|
||||
state.getResolvedMovementSpeed());
|
||||
applySpeedModifier(
|
||||
player,
|
||||
HOP_SPEED_UUID,
|
||||
"tiedup.hop_speed",
|
||||
state.getResolvedMovementSpeed()
|
||||
);
|
||||
state.hopCooldown = 0;
|
||||
state.hopStartupPending = true;
|
||||
state.hopStartupTicks = HOP_STARTUP_DELAY_TICKS;
|
||||
}
|
||||
|
||||
private static void deactivateHop(ServerPlayer player, PlayerBindState state) {
|
||||
private static void deactivateHop(
|
||||
ServerPlayer player,
|
||||
PlayerBindState state
|
||||
) {
|
||||
removeSpeedModifier(player, HOP_SPEED_UUID);
|
||||
state.hopCooldown = 0;
|
||||
state.hopStartupPending = false;
|
||||
@@ -325,8 +375,9 @@ public class MovementStyleManager {
|
||||
* </ul>
|
||||
*/
|
||||
private static void tickHop(ServerPlayer player, PlayerBindState state) {
|
||||
boolean isMoving = player.distanceToSqr(state.lastX, state.lastY, state.lastZ)
|
||||
> MOVEMENT_THRESHOLD_SQ;
|
||||
boolean isMoving =
|
||||
player.distanceToSqr(state.lastX, state.lastY, state.lastZ) >
|
||||
MOVEMENT_THRESHOLD_SQ;
|
||||
|
||||
// Decrement cooldown
|
||||
if (state.hopCooldown > 0) {
|
||||
@@ -351,8 +402,10 @@ public class MovementStyleManager {
|
||||
} else if (!isMoving) {
|
||||
state.hopNotMovingTicks++;
|
||||
// Reset startup if not moving for >= 2 consecutive ticks
|
||||
if (state.hopNotMovingTicks >= HOP_STARTUP_RESET_TICKS
|
||||
&& !state.hopStartupPending) {
|
||||
if (
|
||||
state.hopNotMovingTicks >= HOP_STARTUP_RESET_TICKS &&
|
||||
!state.hopStartupPending
|
||||
) {
|
||||
state.hopStartupPending = true;
|
||||
state.hopStartupTicks = HOP_STARTUP_DELAY_TICKS;
|
||||
}
|
||||
@@ -373,9 +426,9 @@ public class MovementStyleManager {
|
||||
Vec3 currentMotion = player.getDeltaMovement();
|
||||
|
||||
player.setDeltaMovement(
|
||||
currentMotion.x + forward.x * HOP_FORWARD_IMPULSE,
|
||||
HOP_Y_IMPULSE,
|
||||
currentMotion.z + forward.z * HOP_FORWARD_IMPULSE
|
||||
currentMotion.x + forward.x * HOP_FORWARD_IMPULSE,
|
||||
HOP_Y_IMPULSE,
|
||||
currentMotion.z + forward.z * HOP_FORWARD_IMPULSE
|
||||
);
|
||||
|
||||
state.hopCooldown = HOP_COOLDOWN_TICKS;
|
||||
@@ -386,15 +439,25 @@ public class MovementStyleManager {
|
||||
|
||||
// ==================== Crawl ====================
|
||||
|
||||
private static void activateCrawl(ServerPlayer player, PlayerBindState state) {
|
||||
applySpeedModifier(player, CRAWL_SPEED_UUID, "tiedup.crawl_speed",
|
||||
state.getResolvedMovementSpeed());
|
||||
private static void activateCrawl(
|
||||
ServerPlayer player,
|
||||
PlayerBindState state
|
||||
) {
|
||||
applySpeedModifier(
|
||||
player,
|
||||
CRAWL_SPEED_UUID,
|
||||
"tiedup.crawl_speed",
|
||||
state.getResolvedMovementSpeed()
|
||||
);
|
||||
player.setForcedPose(Pose.SWIMMING);
|
||||
player.refreshDimensions();
|
||||
state.pendingPoseRestore = false;
|
||||
}
|
||||
|
||||
private static void deactivateCrawl(ServerPlayer player, PlayerBindState state) {
|
||||
private static void deactivateCrawl(
|
||||
ServerPlayer player,
|
||||
PlayerBindState state
|
||||
) {
|
||||
removeSpeedModifier(player, CRAWL_SPEED_UUID);
|
||||
|
||||
// Space check: can the player stand up?
|
||||
@@ -427,8 +490,10 @@ public class MovementStyleManager {
|
||||
* Called every tick regardless of active style (step 2 in tick flow).
|
||||
* Retries until space is available for the player to stand.
|
||||
*/
|
||||
private static void tryRestoreStandingPose(ServerPlayer player,
|
||||
PlayerBindState state) {
|
||||
private static void tryRestoreStandingPose(
|
||||
ServerPlayer player,
|
||||
PlayerBindState state
|
||||
) {
|
||||
EntityDimensions standDims = player.getDimensions(Pose.STANDING);
|
||||
AABB standBox = standDims.makeBoundingBox(player.position());
|
||||
boolean canStand = player.level().noCollision(player, standBox);
|
||||
@@ -437,8 +502,10 @@ public class MovementStyleManager {
|
||||
player.setForcedPose(null);
|
||||
player.refreshDimensions();
|
||||
state.pendingPoseRestore = false;
|
||||
LOGGER.debug("Restored standing pose for {} (pending pose restore cleared)",
|
||||
player.getName().getString());
|
||||
LOGGER.debug(
|
||||
"Restored standing pose for {} (pending pose restore cleared)",
|
||||
player.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,10 +524,15 @@ public class MovementStyleManager {
|
||||
*/
|
||||
private static void cleanupV1Modifier(ServerPlayer player) {
|
||||
AttributeInstance attr = player.getAttribute(Attributes.MOVEMENT_SPEED);
|
||||
if (attr != null && attr.getModifier(V1_BIND_SPEED_MODIFIER_UUID) != null) {
|
||||
if (
|
||||
attr != null &&
|
||||
attr.getModifier(V1_BIND_SPEED_MODIFIER_UUID) != null
|
||||
) {
|
||||
attr.removeModifier(V1_BIND_SPEED_MODIFIER_UUID);
|
||||
LOGGER.info("Removed stale V1 speed modifier from player {}",
|
||||
player.getName().getString());
|
||||
LOGGER.info(
|
||||
"Removed stale V1 speed modifier from player {}",
|
||||
player.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -481,8 +553,12 @@ public class MovementStyleManager {
|
||||
* @param name modifier name (for debugging in F3 screen)
|
||||
* @param multiplier the desired speed fraction (0.0-1.0)
|
||||
*/
|
||||
private static void applySpeedModifier(ServerPlayer player, UUID uuid, String name,
|
||||
float multiplier) {
|
||||
private static void applySpeedModifier(
|
||||
ServerPlayer player,
|
||||
UUID uuid,
|
||||
String name,
|
||||
float multiplier
|
||||
) {
|
||||
AttributeInstance attr = player.getAttribute(Attributes.MOVEMENT_SPEED);
|
||||
if (attr == null) return;
|
||||
|
||||
@@ -491,8 +567,14 @@ public class MovementStyleManager {
|
||||
|
||||
// MULTIPLY_BASE: value of -(1 - multiplier) reduces base speed to multiplier fraction
|
||||
double value = -(1.0 - multiplier);
|
||||
attr.addTransientModifier(new AttributeModifier(uuid, name,
|
||||
value, AttributeModifier.Operation.MULTIPLY_BASE));
|
||||
attr.addTransientModifier(
|
||||
new AttributeModifier(
|
||||
uuid,
|
||||
name,
|
||||
value,
|
||||
AttributeModifier.Operation.MULTIPLY_BASE
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -47,7 +47,9 @@ public final class MovementStyleResolver {
|
||||
* @param equipped map of region to ItemStack (from {@code IV2BondageEquipment.getAllEquipped()})
|
||||
* @return the resolved movement, or {@link ResolvedMovement#NONE} if no styled items
|
||||
*/
|
||||
public static ResolvedMovement resolve(Map<BodyRegionV2, ItemStack> equipped) {
|
||||
public static ResolvedMovement resolve(
|
||||
Map<BodyRegionV2, ItemStack> equipped
|
||||
) {
|
||||
if (equipped == null || equipped.isEmpty()) {
|
||||
return ResolvedMovement.NONE;
|
||||
}
|
||||
@@ -70,16 +72,20 @@ public final class MovementStyleResolver {
|
||||
int severity = style.getSeverity();
|
||||
int regionOrdinal = region.ordinal();
|
||||
|
||||
if (severity > bestSeverity
|
||||
|| (severity == bestSeverity && regionOrdinal < bestRegionOrdinal)) {
|
||||
if (
|
||||
severity > bestSeverity ||
|
||||
(severity == bestSeverity &&
|
||||
regionOrdinal < bestRegionOrdinal)
|
||||
) {
|
||||
bestStyle = style;
|
||||
MovementModifier mod = def.movementModifier();
|
||||
bestSpeed = (mod != null && mod.speedMultiplier() != null)
|
||||
? mod.speedMultiplier()
|
||||
: style.getDefaultSpeedMultiplier();
|
||||
bestJumpDisabled = (mod != null && mod.jumpDisabled() != null)
|
||||
? mod.jumpDisabled()
|
||||
: style.isDefaultJumpDisabled();
|
||||
? mod.speedMultiplier()
|
||||
: style.getDefaultSpeedMultiplier();
|
||||
bestJumpDisabled = (mod != null &&
|
||||
mod.jumpDisabled() != null)
|
||||
? mod.jumpDisabled()
|
||||
: style.isDefaultJumpDisabled();
|
||||
bestSeverity = severity;
|
||||
bestRegionOrdinal = regionOrdinal;
|
||||
}
|
||||
@@ -92,8 +98,11 @@ public final class MovementStyleResolver {
|
||||
int severity = fallback.style.getSeverity();
|
||||
int regionOrdinal = region.ordinal();
|
||||
|
||||
if (severity > bestSeverity
|
||||
|| (severity == bestSeverity && regionOrdinal < bestRegionOrdinal)) {
|
||||
if (
|
||||
severity > bestSeverity ||
|
||||
(severity == bestSeverity &&
|
||||
regionOrdinal < bestRegionOrdinal)
|
||||
) {
|
||||
bestStyle = fallback.style;
|
||||
bestSpeed = fallback.speed;
|
||||
bestJumpDisabled = fallback.jumpDisabled;
|
||||
@@ -139,10 +148,16 @@ public final class MovementStyleResolver {
|
||||
PoseType poseType = bindItem.getPoseType();
|
||||
|
||||
return switch (poseType) {
|
||||
case WRAP, LATEX_SACK ->
|
||||
new V1Fallback(MovementStyle.SHUFFLE, V1_IMMOBILIZED_SPEED, true);
|
||||
case DOG, HUMAN_CHAIR ->
|
||||
new V1Fallback(MovementStyle.CRAWL, V1_STANDARD_SPEED, true);
|
||||
case WRAP, LATEX_SACK -> new V1Fallback(
|
||||
MovementStyle.SHUFFLE,
|
||||
V1_IMMOBILIZED_SPEED,
|
||||
true
|
||||
);
|
||||
case DOG, HUMAN_CHAIR -> new V1Fallback(
|
||||
MovementStyle.CRAWL,
|
||||
V1_STANDARD_SPEED,
|
||||
true
|
||||
);
|
||||
default ->
|
||||
// STANDARD, STRAITJACKET: shuffle at V1 standard speed
|
||||
new V1Fallback(MovementStyle.SHUFFLE, V1_STANDARD_SPEED, true);
|
||||
@@ -150,5 +165,9 @@ public final class MovementStyleResolver {
|
||||
}
|
||||
|
||||
/** Internal holder for V1 fallback resolution result. */
|
||||
private record V1Fallback(MovementStyle style, float speed, boolean jumpDisabled) {}
|
||||
private record V1Fallback(
|
||||
MovementStyle style,
|
||||
float speed,
|
||||
boolean jumpDisabled
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,10 @@ public record ResolvedMovement(
|
||||
/** Final jump-disabled flag (style default or item override). */
|
||||
boolean jumpDisabled
|
||||
) {
|
||||
|
||||
/** Sentinel for "no movement style active". */
|
||||
public static final ResolvedMovement NONE = new ResolvedMovement(null, 1.0f, false);
|
||||
public static final ResolvedMovement NONE = new ResolvedMovement(
|
||||
null,
|
||||
1.0f,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,9 @@ import org.apache.logging.log4j.Logger;
|
||||
*/
|
||||
public class PacketSyncV2Equipment {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger("PacketSyncV2Equipment");
|
||||
private static final Logger LOGGER = LogManager.getLogger(
|
||||
"PacketSyncV2Equipment"
|
||||
);
|
||||
|
||||
private final int entityId;
|
||||
private final CompoundTag data;
|
||||
@@ -45,12 +47,18 @@ public class PacketSyncV2Equipment {
|
||||
public static PacketSyncV2Equipment decode(FriendlyByteBuf buf) {
|
||||
int entityId = buf.readInt();
|
||||
CompoundTag data = buf.readNbt();
|
||||
return new PacketSyncV2Equipment(entityId, data != null ? data : new CompoundTag());
|
||||
return new PacketSyncV2Equipment(
|
||||
entityId,
|
||||
data != null ? data : new CompoundTag()
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== Handler ====================
|
||||
|
||||
public static void handle(PacketSyncV2Equipment msg, Supplier<NetworkEvent.Context> ctxSupplier) {
|
||||
public static void handle(
|
||||
PacketSyncV2Equipment msg,
|
||||
Supplier<NetworkEvent.Context> ctxSupplier
|
||||
) {
|
||||
NetworkEvent.Context ctx = ctxSupplier.get();
|
||||
ctx.enqueueWork(() -> {
|
||||
if (FMLEnvironment.dist == Dist.CLIENT) {
|
||||
@@ -74,7 +82,8 @@ public class PacketSyncV2Equipment {
|
||||
holder.getV2Equipment().deserializeNBT(msg.data);
|
||||
return;
|
||||
}
|
||||
living.getCapability(V2BondageEquipmentProvider.V2_BONDAGE_EQUIPMENT)
|
||||
living
|
||||
.getCapability(V2BondageEquipmentProvider.V2_BONDAGE_EQUIPMENT)
|
||||
.ifPresent(equip -> equip.deserializeNBT(msg.data));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,13 +36,20 @@ import net.minecraftforge.network.NetworkEvent;
|
||||
*/
|
||||
public class PacketV2LockToggle {
|
||||
|
||||
public enum Action { LOCK, UNLOCK }
|
||||
public enum Action {
|
||||
LOCK,
|
||||
UNLOCK,
|
||||
}
|
||||
|
||||
private final int targetEntityId;
|
||||
private final BodyRegionV2 region;
|
||||
private final Action action;
|
||||
|
||||
public PacketV2LockToggle(int targetEntityId, BodyRegionV2 region, Action action) {
|
||||
public PacketV2LockToggle(
|
||||
int targetEntityId,
|
||||
BodyRegionV2 region,
|
||||
Action action
|
||||
) {
|
||||
this.targetEntityId = targetEntityId;
|
||||
this.region = region;
|
||||
this.action = action;
|
||||
@@ -62,7 +69,10 @@ public class PacketV2LockToggle {
|
||||
);
|
||||
}
|
||||
|
||||
public static void handle(PacketV2LockToggle msg, Supplier<NetworkEvent.Context> ctxSupplier) {
|
||||
public static void handle(
|
||||
PacketV2LockToggle msg,
|
||||
Supplier<NetworkEvent.Context> ctxSupplier
|
||||
) {
|
||||
NetworkEvent.Context ctx = ctxSupplier.get();
|
||||
ctx.enqueueWork(() -> {
|
||||
ServerPlayer sender = ctx.getSender();
|
||||
@@ -75,13 +85,18 @@ public class PacketV2LockToggle {
|
||||
}
|
||||
|
||||
private static void handleServer(
|
||||
ServerPlayer sender, int targetEntityId, BodyRegionV2 region, Action action
|
||||
ServerPlayer sender,
|
||||
int targetEntityId,
|
||||
BodyRegionV2 region,
|
||||
Action action
|
||||
) {
|
||||
Entity rawTarget = sender.level().getEntity(targetEntityId);
|
||||
if (!(rawTarget instanceof LivingEntity target)) return;
|
||||
|
||||
// Distance + line-of-sight validation
|
||||
if (sender.distanceTo(target) > 4.0 || !sender.hasLineOfSight(target)) return;
|
||||
if (
|
||||
sender.distanceTo(target) > 4.0 || !sender.hasLineOfSight(target)
|
||||
) return;
|
||||
|
||||
ItemStack stack = V2EquipmentHelper.getInRegion(target, region);
|
||||
if (stack.isEmpty()) return;
|
||||
@@ -91,8 +106,9 @@ public class PacketV2LockToggle {
|
||||
ItemStack mainHand = sender.getItemInHand(InteractionHand.MAIN_HAND);
|
||||
ItemStack offHand = sender.getItemInHand(InteractionHand.OFF_HAND);
|
||||
|
||||
boolean hasMasterKey = mainHand.getItem() instanceof ItemMasterKey
|
||||
|| offHand.getItem() instanceof ItemMasterKey;
|
||||
boolean hasMasterKey =
|
||||
mainHand.getItem() instanceof ItemMasterKey ||
|
||||
offHand.getItem() instanceof ItemMasterKey;
|
||||
|
||||
ItemKey heldKey = null;
|
||||
ItemStack heldKeyStack = ItemStack.EMPTY;
|
||||
@@ -115,8 +131,11 @@ public class PacketV2LockToggle {
|
||||
lockable.setLockedByKeyUUID(stack, keyUUID);
|
||||
lockable.initializeLockResistance(stack);
|
||||
|
||||
TiedUpMod.LOGGER.debug("[V2LockToggle] Locked region {} on entity {}",
|
||||
region.name(), target.getName().getString());
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[V2LockToggle] Locked region {} on entity {}",
|
||||
region.name(),
|
||||
target.getName().getString()
|
||||
);
|
||||
}
|
||||
case UNLOCK -> {
|
||||
if (!lockable.isLocked(stack)) return;
|
||||
@@ -133,8 +152,11 @@ public class PacketV2LockToggle {
|
||||
return;
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.debug("[V2LockToggle] Unlocked region {} on entity {}",
|
||||
region.name(), target.getName().getString());
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[V2LockToggle] Unlocked region {} on entity {}",
|
||||
region.name(),
|
||||
target.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,9 @@ import org.apache.logging.log4j.Logger;
|
||||
*/
|
||||
public class PacketV2SelfEquip {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger("PacketV2SelfEquip");
|
||||
private static final Logger LOGGER = LogManager.getLogger(
|
||||
"PacketV2SelfEquip"
|
||||
);
|
||||
|
||||
private final BodyRegionV2 region;
|
||||
private final int inventorySlot;
|
||||
@@ -36,10 +38,16 @@ public class PacketV2SelfEquip {
|
||||
}
|
||||
|
||||
public static PacketV2SelfEquip decode(FriendlyByteBuf buf) {
|
||||
return new PacketV2SelfEquip(buf.readEnum(BodyRegionV2.class), buf.readVarInt());
|
||||
return new PacketV2SelfEquip(
|
||||
buf.readEnum(BodyRegionV2.class),
|
||||
buf.readVarInt()
|
||||
);
|
||||
}
|
||||
|
||||
public static void handle(PacketV2SelfEquip msg, Supplier<NetworkEvent.Context> ctxSupplier) {
|
||||
public static void handle(
|
||||
PacketV2SelfEquip msg,
|
||||
Supplier<NetworkEvent.Context> ctxSupplier
|
||||
) {
|
||||
NetworkEvent.Context ctx = ctxSupplier.get();
|
||||
ctx.enqueueWork(() -> {
|
||||
ServerPlayer player = ctx.getSender();
|
||||
@@ -47,26 +55,46 @@ public class PacketV2SelfEquip {
|
||||
if (!PacketRateLimiter.allowPacket(player, "action")) return;
|
||||
|
||||
// Validate slot index
|
||||
if (msg.inventorySlot < 0 || msg.inventorySlot >= player.getInventory().getContainerSize()) return;
|
||||
if (
|
||||
msg.inventorySlot < 0 ||
|
||||
msg.inventorySlot >= player.getInventory().getContainerSize()
|
||||
) return;
|
||||
|
||||
ItemStack stack = player.getInventory().getItem(msg.inventorySlot);
|
||||
if (stack.isEmpty()) return;
|
||||
if (!(stack.getItem() instanceof IV2BondageItem bondageItem)) return;
|
||||
if (
|
||||
!(stack.getItem() instanceof IV2BondageItem bondageItem)
|
||||
) return;
|
||||
|
||||
// Warn if data-driven item has no definition (missing JSON or reload issue)
|
||||
if (bondageItem instanceof DataDrivenBondageItem && DataDrivenItemRegistry.get(stack) == null) {
|
||||
LOGGER.warn("[V2SelfEquip] Data-driven item in slot {} has no definition — equip blocked. Stack NBT: {}",
|
||||
msg.inventorySlot, stack.getTag());
|
||||
if (
|
||||
bondageItem instanceof DataDrivenBondageItem &&
|
||||
DataDrivenItemRegistry.get(stack) == null
|
||||
) {
|
||||
LOGGER.warn(
|
||||
"[V2SelfEquip] Data-driven item in slot {} has no definition — equip blocked. Stack NBT: {}",
|
||||
msg.inventorySlot,
|
||||
stack.getTag()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate item targets this region
|
||||
if (!bondageItem.getOccupiedRegions(stack).contains(msg.region)) return;
|
||||
if (
|
||||
!bondageItem.getOccupiedRegions(stack).contains(msg.region)
|
||||
) return;
|
||||
|
||||
// Furniture seat blocks this region
|
||||
if (player.isPassenger() && player.getVehicle() instanceof com.tiedup.remake.v2.furniture.ISeatProvider provider) {
|
||||
com.tiedup.remake.v2.furniture.SeatDefinition seat = provider.getSeatForPassenger(player);
|
||||
if (seat != null && seat.blockedRegions().contains(msg.region)) {
|
||||
if (
|
||||
player.isPassenger() &&
|
||||
player.getVehicle() instanceof
|
||||
com.tiedup.remake.v2.furniture.ISeatProvider provider
|
||||
) {
|
||||
com.tiedup.remake.v2.furniture.SeatDefinition seat =
|
||||
provider.getSeatForPassenger(player);
|
||||
if (
|
||||
seat != null && seat.blockedRegions().contains(msg.region)
|
||||
) {
|
||||
return; // Region blocked by furniture
|
||||
}
|
||||
}
|
||||
@@ -80,7 +108,9 @@ public class PacketV2SelfEquip {
|
||||
if (result.displaced() != null) {
|
||||
for (ItemStack displaced : result.displaced()) {
|
||||
if (!displaced.isEmpty()) {
|
||||
player.getInventory().placeItemBackInInventory(displaced);
|
||||
player
|
||||
.getInventory()
|
||||
.placeItemBackInInventory(displaced);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,10 @@ public class PacketV2SelfLock {
|
||||
return new PacketV2SelfLock(buf.readEnum(BodyRegionV2.class));
|
||||
}
|
||||
|
||||
public static void handle(PacketV2SelfLock msg, Supplier<NetworkEvent.Context> ctxSupplier) {
|
||||
public static void handle(
|
||||
PacketV2SelfLock msg,
|
||||
Supplier<NetworkEvent.Context> ctxSupplier
|
||||
) {
|
||||
NetworkEvent.Context ctx = ctxSupplier.get();
|
||||
ctx.enqueueWork(() -> {
|
||||
ServerPlayer player = ctx.getSender();
|
||||
@@ -39,17 +42,31 @@ public class PacketV2SelfLock {
|
||||
if (!PacketRateLimiter.allowPacket(player, "action")) return;
|
||||
|
||||
// Arms must be free to self-lock
|
||||
if (V2EquipmentHelper.isRegionOccupied(player, BodyRegionV2.ARMS)) return;
|
||||
if (
|
||||
V2EquipmentHelper.isRegionOccupied(player, BodyRegionV2.ARMS)
|
||||
) return;
|
||||
|
||||
ItemStack equipped = V2EquipmentHelper.getInRegion(player, msg.region);
|
||||
ItemStack equipped = V2EquipmentHelper.getInRegion(
|
||||
player,
|
||||
msg.region
|
||||
);
|
||||
if (equipped.isEmpty()) return;
|
||||
if (!(equipped.getItem() instanceof ILockable lockable)) return;
|
||||
if (!lockable.isLockable(equipped) || lockable.isLocked(equipped)) return;
|
||||
if (
|
||||
!lockable.isLockable(equipped) || lockable.isLocked(equipped)
|
||||
) return;
|
||||
|
||||
// Furniture seat blocks this region
|
||||
if (player.isPassenger() && player.getVehicle() instanceof com.tiedup.remake.v2.furniture.ISeatProvider provider) {
|
||||
com.tiedup.remake.v2.furniture.SeatDefinition seat = provider.getSeatForPassenger(player);
|
||||
if (seat != null && seat.blockedRegions().contains(msg.region)) {
|
||||
if (
|
||||
player.isPassenger() &&
|
||||
player.getVehicle() instanceof
|
||||
com.tiedup.remake.v2.furniture.ISeatProvider provider
|
||||
) {
|
||||
com.tiedup.remake.v2.furniture.SeatDefinition seat =
|
||||
provider.getSeatForPassenger(player);
|
||||
if (
|
||||
seat != null && seat.blockedRegions().contains(msg.region)
|
||||
) {
|
||||
return; // Region blocked by furniture
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,10 @@ public class PacketV2SelfRemove {
|
||||
return new PacketV2SelfRemove(buf.readEnum(BodyRegionV2.class));
|
||||
}
|
||||
|
||||
public static void handle(PacketV2SelfRemove msg, Supplier<NetworkEvent.Context> ctxSupplier) {
|
||||
public static void handle(
|
||||
PacketV2SelfRemove msg,
|
||||
Supplier<NetworkEvent.Context> ctxSupplier
|
||||
) {
|
||||
NetworkEvent.Context ctx = ctxSupplier.get();
|
||||
ctx.enqueueWork(() -> {
|
||||
ServerPlayer player = ctx.getSender();
|
||||
@@ -56,25 +59,33 @@ public class PacketV2SelfRemove {
|
||||
|
||||
// Arm restraints cannot be self-removed — must use struggle
|
||||
if (item.getOccupiedRegions(stack).contains(BodyRegionV2.ARMS)) {
|
||||
TiedUpMod.LOGGER.debug("[V2SelfRemove] Blocked: item occupies ARMS, must struggle");
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[V2SelfRemove] Blocked: item occupies ARMS, must struggle"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Cannot manipulate buckles/clasps with bound arms
|
||||
if (V2EquipmentHelper.isRegionOccupied(player, BodyRegionV2.ARMS)) {
|
||||
TiedUpMod.LOGGER.debug("[V2SelfRemove] Blocked: player's ARMS are occupied");
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[V2SelfRemove] Blocked: player's ARMS are occupied"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Cannot manipulate buckles/clasps with covered hands
|
||||
if (V2EquipmentHelper.isRegionOccupied(player, BodyRegionV2.HANDS)) {
|
||||
TiedUpMod.LOGGER.debug("[V2SelfRemove] Blocked: player's HANDS are occupied");
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[V2SelfRemove] Blocked: player's HANDS are occupied"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check item allows unequip (not locked)
|
||||
if (!item.canUnequip(stack, player)) {
|
||||
TiedUpMod.LOGGER.debug("[V2SelfRemove] Blocked: item canUnequip=false (locked?)");
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[V2SelfRemove] Blocked: item canUnequip=false (locked?)"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,10 @@ public class PacketV2SelfUnlock {
|
||||
return new PacketV2SelfUnlock(buf.readEnum(BodyRegionV2.class));
|
||||
}
|
||||
|
||||
public static void handle(PacketV2SelfUnlock msg, Supplier<NetworkEvent.Context> ctxSupplier) {
|
||||
public static void handle(
|
||||
PacketV2SelfUnlock msg,
|
||||
Supplier<NetworkEvent.Context> ctxSupplier
|
||||
) {
|
||||
NetworkEvent.Context ctx = ctxSupplier.get();
|
||||
ctx.enqueueWork(() -> {
|
||||
ServerPlayer player = ctx.getSender();
|
||||
@@ -40,24 +43,39 @@ public class PacketV2SelfUnlock {
|
||||
if (!PacketRateLimiter.allowPacket(player, "action")) return;
|
||||
|
||||
// Arms must be free to self-unlock
|
||||
if (V2EquipmentHelper.isRegionOccupied(player, BodyRegionV2.ARMS)) return;
|
||||
if (
|
||||
V2EquipmentHelper.isRegionOccupied(player, BodyRegionV2.ARMS)
|
||||
) return;
|
||||
|
||||
ItemStack equipped = V2EquipmentHelper.getInRegion(player, msg.region);
|
||||
ItemStack equipped = V2EquipmentHelper.getInRegion(
|
||||
player,
|
||||
msg.region
|
||||
);
|
||||
if (equipped.isEmpty()) return;
|
||||
if (!(equipped.getItem() instanceof ILockable lockable)) return;
|
||||
if (!lockable.isLocked(equipped)) return;
|
||||
|
||||
// Furniture seat blocks this region
|
||||
if (player.isPassenger() && player.getVehicle() instanceof com.tiedup.remake.v2.furniture.ISeatProvider provider) {
|
||||
com.tiedup.remake.v2.furniture.SeatDefinition seat = provider.getSeatForPassenger(player);
|
||||
if (seat != null && seat.blockedRegions().contains(msg.region)) {
|
||||
if (
|
||||
player.isPassenger() &&
|
||||
player.getVehicle() instanceof
|
||||
com.tiedup.remake.v2.furniture.ISeatProvider provider
|
||||
) {
|
||||
com.tiedup.remake.v2.furniture.SeatDefinition seat =
|
||||
provider.getSeatForPassenger(player);
|
||||
if (
|
||||
seat != null && seat.blockedRegions().contains(msg.region)
|
||||
) {
|
||||
return; // Region blocked by furniture
|
||||
}
|
||||
}
|
||||
|
||||
// Find matching key in inventory
|
||||
UUID lockedByUUID = lockable.getLockedByKeyUUID(equipped);
|
||||
ItemStack keyStack = findMatchingKeyInInventory(player, lockedByUUID);
|
||||
ItemStack keyStack = findMatchingKeyInInventory(
|
||||
player,
|
||||
lockedByUUID
|
||||
);
|
||||
if (keyStack.isEmpty()) return;
|
||||
|
||||
lockable.setLockedByKeyUUID(equipped, null);
|
||||
@@ -70,7 +88,10 @@ public class PacketV2SelfUnlock {
|
||||
* Find a key in the player's inventory that matches the lock UUID,
|
||||
* or a master key that unlocks anything.
|
||||
*/
|
||||
private static ItemStack findMatchingKeyInInventory(ServerPlayer player, UUID lockedByUUID) {
|
||||
private static ItemStack findMatchingKeyInInventory(
|
||||
ServerPlayer player,
|
||||
UUID lockedByUUID
|
||||
) {
|
||||
for (int i = 0; i < player.getInventory().getContainerSize(); i++) {
|
||||
ItemStack s = player.getInventory().getItem(i);
|
||||
if (s.isEmpty()) continue;
|
||||
@@ -78,7 +99,10 @@ public class PacketV2SelfUnlock {
|
||||
if (s.is(ModItems.MASTER_KEY.get())) return s;
|
||||
// Regular key: must match the lock UUID
|
||||
if (s.getItem() instanceof ItemKey key) {
|
||||
if (lockedByUUID != null && lockedByUUID.equals(key.getKeyUUID(s))) {
|
||||
if (
|
||||
lockedByUUID != null &&
|
||||
lockedByUUID.equals(key.getKeyUUID(s))
|
||||
) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,10 @@ public class PacketV2StruggleStart {
|
||||
return new PacketV2StruggleStart(buf.readEnum(BodyRegionV2.class));
|
||||
}
|
||||
|
||||
public static void handle(PacketV2StruggleStart msg, Supplier<NetworkEvent.Context> ctxSupplier) {
|
||||
public static void handle(
|
||||
PacketV2StruggleStart msg,
|
||||
Supplier<NetworkEvent.Context> ctxSupplier
|
||||
) {
|
||||
NetworkEvent.Context ctx = ctxSupplier.get();
|
||||
ctx.enqueueWork(() -> {
|
||||
ServerPlayer player = ctx.getSender();
|
||||
@@ -72,7 +75,9 @@ public class PacketV2StruggleStart {
|
||||
if (!resistanceItem.canBeStruggledOut(stack)) return;
|
||||
|
||||
// RISK-002 fix: respect server config
|
||||
if (!com.tiedup.remake.core.ModConfig.SERVER.struggleMiniGameEnabled.get()) return;
|
||||
if (
|
||||
!com.tiedup.remake.core.ModConfig.SERVER.struggleMiniGameEnabled.get()
|
||||
) return;
|
||||
|
||||
int resistance = resistanceItem.getCurrentResistance(stack, player);
|
||||
boolean isLocked = false;
|
||||
@@ -87,9 +92,13 @@ public class PacketV2StruggleStart {
|
||||
if (resistance <= 0) return;
|
||||
|
||||
StruggleSessionManager manager = StruggleSessionManager.getInstance();
|
||||
ContinuousStruggleMiniGameState session = manager.startV2StruggleSession(
|
||||
player, region, resistance, isLocked
|
||||
);
|
||||
ContinuousStruggleMiniGameState session =
|
||||
manager.startV2StruggleSession(
|
||||
player,
|
||||
region,
|
||||
resistance,
|
||||
isLocked
|
||||
);
|
||||
|
||||
if (session != null) {
|
||||
ModNetwork.sendToPlayer(
|
||||
|
||||
@@ -34,7 +34,10 @@ public class DataDrivenIconBakedModel implements BakedModel {
|
||||
* @param original the original baked model to wrap (provides quads, transforms, etc.)
|
||||
* @param overrides the custom overrides that resolve icon models from NBT
|
||||
*/
|
||||
public DataDrivenIconBakedModel(BakedModel original, DataDrivenIconOverrides overrides) {
|
||||
public DataDrivenIconBakedModel(
|
||||
BakedModel original,
|
||||
DataDrivenIconOverrides overrides
|
||||
) {
|
||||
this.original = original;
|
||||
this.overrides = overrides;
|
||||
}
|
||||
@@ -58,7 +61,11 @@ public class DataDrivenIconBakedModel implements BakedModel {
|
||||
// ===== Delegated methods =====
|
||||
|
||||
@Override
|
||||
public List<BakedQuad> getQuads(@Nullable BlockState state, @Nullable Direction direction, RandomSource random) {
|
||||
public List<BakedQuad> getQuads(
|
||||
@Nullable BlockState state,
|
||||
@Nullable Direction direction,
|
||||
RandomSource random
|
||||
) {
|
||||
return original.getQuads(state, direction, random);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import com.tiedup.remake.v2.furniture.FurniturePlacerItem;
|
||||
import com.tiedup.remake.v2.furniture.FurnitureRegistry;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.renderer.block.model.ItemOverrides;
|
||||
@@ -20,6 +19,7 @@ import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Custom {@link ItemOverrides} that switches the rendered item model based on
|
||||
@@ -43,7 +43,9 @@ import org.apache.logging.log4j.Logger;
|
||||
@OnlyIn(Dist.CLIENT)
|
||||
public class DataDrivenIconOverrides extends ItemOverrides {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger("DataDrivenIcons");
|
||||
private static final Logger LOGGER = LogManager.getLogger(
|
||||
"DataDrivenIcons"
|
||||
);
|
||||
|
||||
/**
|
||||
* Identifies which type of NBT-driven item this override handles.
|
||||
@@ -52,7 +54,7 @@ public class DataDrivenIconOverrides extends ItemOverrides {
|
||||
/** Data-driven bondage items (reads {@code tiedup_item_id} NBT). */
|
||||
BONDAGE_ITEM,
|
||||
/** Furniture placer items (reads {@code tiedup_furniture_id} NBT). */
|
||||
FURNITURE_PLACER
|
||||
FURNITURE_PLACER,
|
||||
}
|
||||
|
||||
private final Mode mode;
|
||||
@@ -65,18 +67,21 @@ public class DataDrivenIconOverrides extends ItemOverrides {
|
||||
* <p>Values are never null (ConcurrentHashMap forbids nulls). Missing icons
|
||||
* are tracked separately in {@link #knownMissing}.</p>
|
||||
*/
|
||||
private final Map<ResourceLocation, BakedModel> iconModelCache = new ConcurrentHashMap<>();
|
||||
private final Map<ResourceLocation, BakedModel> iconModelCache =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Set of icon ResourceLocations that were looked up but not found.
|
||||
* Prevents repeated failed lookups from hitting the model registry every frame.
|
||||
*/
|
||||
private final Map<ResourceLocation, Boolean> knownMissing = new ConcurrentHashMap<>();
|
||||
private final Map<ResourceLocation, Boolean> knownMissing =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Set of icon ResourceLocations that we already logged a warning for (to avoid log spam).
|
||||
*/
|
||||
private final Map<ResourceLocation, Boolean> warnedMissing = new ConcurrentHashMap<>();
|
||||
private final Map<ResourceLocation, Boolean> warnedMissing =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
public DataDrivenIconOverrides(Mode mode) {
|
||||
super();
|
||||
@@ -130,11 +135,14 @@ public class DataDrivenIconOverrides extends ItemOverrides {
|
||||
|
||||
switch (mode) {
|
||||
case BONDAGE_ITEM: {
|
||||
DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack);
|
||||
DataDrivenItemDefinition def = DataDrivenItemRegistry.get(
|
||||
stack
|
||||
);
|
||||
return def != null ? def.icon() : null;
|
||||
}
|
||||
case FURNITURE_PLACER: {
|
||||
String furnitureIdStr = FurniturePlacerItem.getFurnitureIdFromStack(stack);
|
||||
String furnitureIdStr =
|
||||
FurniturePlacerItem.getFurnitureIdFromStack(stack);
|
||||
if (furnitureIdStr == null) return null;
|
||||
FurnitureDefinition def = FurnitureRegistry.get(furnitureIdStr);
|
||||
return def != null ? def.icon() : null;
|
||||
@@ -188,7 +196,10 @@ public class DataDrivenIconOverrides extends ItemOverrides {
|
||||
// Strategy 3: Try as-is with "inventory" variant
|
||||
// e.g., "tiedup:armbinder" → ModelResourceLocation("tiedup:armbinder", "inventory")
|
||||
if (!path.contains("/")) {
|
||||
ModelResourceLocation mrl = new ModelResourceLocation(iconRL, "inventory");
|
||||
ModelResourceLocation mrl = new ModelResourceLocation(
|
||||
iconRL,
|
||||
"inventory"
|
||||
);
|
||||
model = mc.getModelManager().getModel(mrl);
|
||||
if (model != missingModel) {
|
||||
return model;
|
||||
@@ -198,9 +209,14 @@ public class DataDrivenIconOverrides extends ItemOverrides {
|
||||
// Not found — log once
|
||||
if (!warnedMissing.containsKey(iconRL)) {
|
||||
warnedMissing.put(iconRL, Boolean.TRUE);
|
||||
LOGGER.warn("[DataDrivenIcons] Icon model not found for '{}' (mode={}). "
|
||||
+ "Ensure a model JSON exists at assets/{}/models/{}.json or the item is registered.",
|
||||
iconRL, mode, iconRL.getNamespace(), iconRL.getPath());
|
||||
LOGGER.warn(
|
||||
"[DataDrivenIcons] Icon model not found for '{}' (mode={}). " +
|
||||
"Ensure a model JSON exists at assets/{}/models/{}.json or the item is registered.",
|
||||
iconRL,
|
||||
mode,
|
||||
iconRL.getNamespace(),
|
||||
iconRL.getPath()
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -141,13 +141,15 @@ public class V2ClientSetup {
|
||||
|
||||
DataDrivenIconOverrides overrides = new DataDrivenIconOverrides(mode);
|
||||
DataDrivenIconBakedModel wrapped = new DataDrivenIconBakedModel(
|
||||
originalItemModel, overrides
|
||||
originalItemModel,
|
||||
overrides
|
||||
);
|
||||
models.put(itemModelLoc, wrapped);
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[V2ClientSetup] Wrapped {} model with icon overrides (mode={})",
|
||||
itemName, mode
|
||||
itemName,
|
||||
mode
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,11 +19,13 @@ import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.ClientGamePacketListener;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.network.syncher.EntityDataAccessor;
|
||||
import net.minecraft.network.syncher.EntityDataSerializers;
|
||||
import net.minecraft.network.syncher.SynchedEntityData;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.sounds.SoundEvent;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
@@ -33,8 +35,6 @@ import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.Pose;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.sounds.SoundEvent;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import net.minecraftforge.entity.IEntityAdditionalSpawnData;
|
||||
@@ -59,25 +59,36 @@ import org.jetbrains.annotations.Nullable;
|
||||
* Forge to include the {@code IEntityAdditionalSpawnData} buffer in the spawn
|
||||
* packet. Without this override, the entity would be invisible on clients.</p>
|
||||
*/
|
||||
public class EntityFurniture extends Entity
|
||||
implements ISeatProvider, IEntityAdditionalSpawnData {
|
||||
public class EntityFurniture
|
||||
extends Entity
|
||||
implements ISeatProvider, IEntityAdditionalSpawnData
|
||||
{
|
||||
|
||||
// ========== SynchedEntityData Accessors ==========
|
||||
|
||||
/** The furniture definition ID (ResourceLocation string form). */
|
||||
private static final EntityDataAccessor<String> FURNITURE_ID =
|
||||
SynchedEntityData.defineId(EntityFurniture.class, EntityDataSerializers.STRING);
|
||||
SynchedEntityData.defineId(
|
||||
EntityFurniture.class,
|
||||
EntityDataSerializers.STRING
|
||||
);
|
||||
|
||||
/**
|
||||
* Bitmask of locked seats. Bit N corresponds to seat index N
|
||||
* in {@link FurnitureDefinition#seats()}. Max 8 seats per furniture.
|
||||
*/
|
||||
private static final EntityDataAccessor<Byte> SEAT_LOCK_BITS =
|
||||
SynchedEntityData.defineId(EntityFurniture.class, EntityDataSerializers.BYTE);
|
||||
SynchedEntityData.defineId(
|
||||
EntityFurniture.class,
|
||||
EntityDataSerializers.BYTE
|
||||
);
|
||||
|
||||
/** Current animation state (idle, occupied, locking, struggle). */
|
||||
private static final EntityDataAccessor<Byte> ANIM_STATE =
|
||||
SynchedEntityData.defineId(EntityFurniture.class, EntityDataSerializers.BYTE);
|
||||
SynchedEntityData.defineId(
|
||||
EntityFurniture.class,
|
||||
EntityDataSerializers.BYTE
|
||||
);
|
||||
|
||||
// ========== Animation State Constants ==========
|
||||
|
||||
@@ -166,7 +177,10 @@ public class EntityFurniture extends Entity
|
||||
public EntityDimensions getDimensions(Pose pose) {
|
||||
FurnitureDefinition def = getDefinition();
|
||||
if (def != null) {
|
||||
return EntityDimensions.fixed(def.hitboxWidth(), def.hitboxHeight());
|
||||
return EntityDimensions.fixed(
|
||||
def.hitboxWidth(),
|
||||
def.hitboxHeight()
|
||||
);
|
||||
}
|
||||
return EntityDimensions.fixed(1.0f, 1.0f);
|
||||
}
|
||||
@@ -228,7 +242,9 @@ public class EntityFurniture extends Entity
|
||||
if (locked) {
|
||||
writeFurnitureReconnectionTag(serverPlayer, seatId);
|
||||
} else {
|
||||
serverPlayer.getPersistentData().remove("tiedup_locked_furniture");
|
||||
serverPlayer
|
||||
.getPersistentData()
|
||||
.remove("tiedup_locked_furniture");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -258,7 +274,10 @@ public class EntityFurniture extends Entity
|
||||
* @param player the server player locked in this furniture
|
||||
* @param seatId the seat ID the player is locked in
|
||||
*/
|
||||
private void writeFurnitureReconnectionTag(ServerPlayer player, String seatId) {
|
||||
private void writeFurnitureReconnectionTag(
|
||||
ServerPlayer player,
|
||||
String seatId
|
||||
) {
|
||||
CompoundTag tag = new CompoundTag();
|
||||
BlockPos pos = this.blockPosition();
|
||||
tag.putInt("x", pos.getX());
|
||||
@@ -311,13 +330,21 @@ public class EntityFurniture extends Entity
|
||||
* by spacing seats evenly along the furniture's local right axis.</p>
|
||||
*/
|
||||
@Override
|
||||
protected void positionRider(Entity passenger, Entity.MoveFunction moveFunction) {
|
||||
protected void positionRider(
|
||||
Entity passenger,
|
||||
Entity.MoveFunction moveFunction
|
||||
) {
|
||||
if (!this.hasPassenger(passenger)) return;
|
||||
|
||||
SeatDefinition seat = getSeatForPassenger(passenger);
|
||||
if (seat == null) {
|
||||
// Fallback: center of entity at base height
|
||||
moveFunction.accept(passenger, this.getX(), this.getY(), this.getZ());
|
||||
moveFunction.accept(
|
||||
passenger,
|
||||
this.getX(),
|
||||
this.getY(),
|
||||
this.getZ()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -326,10 +353,13 @@ public class EntityFurniture extends Entity
|
||||
if (this.level().isClientSide) {
|
||||
FurnitureDefinition def = getDefinition();
|
||||
if (def != null) {
|
||||
double[] pos = com.tiedup.remake.v2.furniture.client.FurnitureSeatPositionHelper
|
||||
.getSeatWorldPosition(
|
||||
def, seat.id(),
|
||||
this.getX(), this.getY(), this.getZ(),
|
||||
double[] pos =
|
||||
com.tiedup.remake.v2.furniture.client.FurnitureSeatPositionHelper.getSeatWorldPosition(
|
||||
def,
|
||||
seat.id(),
|
||||
this.getX(),
|
||||
this.getY(),
|
||||
this.getZ(),
|
||||
this.getYRot()
|
||||
);
|
||||
if (pos != null) {
|
||||
@@ -349,7 +379,8 @@ public class EntityFurniture extends Entity
|
||||
float yawRad = (float) Math.toRadians(this.getYRot());
|
||||
double rightX = -Math.sin(yawRad + Math.PI / 2.0);
|
||||
double rightZ = Math.cos(yawRad + Math.PI / 2.0);
|
||||
double offset = seatCount == 1 ? 0.0 : (seatIdx - (seatCount - 1) / 2.0);
|
||||
double offset =
|
||||
seatCount == 1 ? 0.0 : (seatIdx - (seatCount - 1) / 2.0);
|
||||
|
||||
moveFunction.accept(
|
||||
passenger,
|
||||
@@ -464,7 +495,10 @@ public class EntityFurniture extends Entity
|
||||
* @return the best matching seat, or null if no occupied lockable seats exist
|
||||
*/
|
||||
@Nullable
|
||||
private SeatDefinition findNearestOccupiedLockableSeat(Player player, FurnitureDefinition def) {
|
||||
private SeatDefinition findNearestOccupiedLockableSeat(
|
||||
Player player,
|
||||
FurnitureDefinition def
|
||||
) {
|
||||
Vec3 playerPos = player.getEyePosition();
|
||||
Vec3 lookDir = player.getLookAngle();
|
||||
float yawRad = (float) Math.toRadians(this.getYRot());
|
||||
@@ -482,7 +516,9 @@ public class EntityFurniture extends Entity
|
||||
SeatDefinition seat = seats.get(i);
|
||||
|
||||
// Only consider occupied, lockable seats
|
||||
if (!seat.lockable() || !seatAssignments.containsValue(seat.id())) continue;
|
||||
if (
|
||||
!seat.lockable() || !seatAssignments.containsValue(seat.id())
|
||||
) continue;
|
||||
|
||||
double offset = seatCount == 1 ? 0.0 : (i - (seatCount - 1) / 2.0);
|
||||
Vec3 seatWorldPos = new Vec3(
|
||||
@@ -533,9 +569,20 @@ public class EntityFurniture extends Entity
|
||||
if (passenger instanceof ServerPlayer sp) {
|
||||
FurnitureDefinition def = getDefinition();
|
||||
if (def != null && def.feedback().deniedSound() != null) {
|
||||
sp.level().playSound(null, sp.getX(), sp.getY(), sp.getZ(),
|
||||
SoundEvent.createVariableRangeEvent(def.feedback().deniedSound()),
|
||||
SoundSource.PLAYERS, 0.5f, 1.0f);
|
||||
sp
|
||||
.level()
|
||||
.playSound(
|
||||
null,
|
||||
sp.getX(),
|
||||
sp.getY(),
|
||||
sp.getZ(),
|
||||
SoundEvent.createVariableRangeEvent(
|
||||
def.feedback().deniedSound()
|
||||
),
|
||||
SoundSource.PLAYERS,
|
||||
0.5f,
|
||||
1.0f
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -546,7 +593,11 @@ public class EntityFurniture extends Entity
|
||||
String lockedSeatId = seat.id();
|
||||
Entity self = this;
|
||||
this.getServer().execute(() -> {
|
||||
if (passenger.isAlive() && self.isAlive() && !self.isRemoved()) {
|
||||
if (
|
||||
passenger.isAlive() &&
|
||||
self.isAlive() &&
|
||||
!self.isRemoved()
|
||||
) {
|
||||
passenger.startRiding(self, true);
|
||||
// Re-assign the specific seat (not just first available)
|
||||
assignSeat(passenger, lockedSeatId);
|
||||
@@ -575,7 +626,9 @@ public class EntityFurniture extends Entity
|
||||
if (!this.level().isClientSide) {
|
||||
this.entityData.set(ANIM_STATE, STATE_EXITING);
|
||||
this.transitionTicksLeft = 20;
|
||||
this.transitionTargetState = this.getPassengers().isEmpty() ? STATE_IDLE : STATE_OCCUPIED;
|
||||
this.transitionTargetState = this.getPassengers().isEmpty()
|
||||
? STATE_IDLE
|
||||
: STATE_OCCUPIED;
|
||||
}
|
||||
|
||||
// Client-side: immediately stop the furniture pose animation on the dismounting player.
|
||||
@@ -583,8 +636,9 @@ public class EntityFurniture extends Entity
|
||||
// The safety tick in AnimationTickHandler also catches this after a 3-tick grace period,
|
||||
// but stopping immediately avoids a visible animation glitch during dismount.
|
||||
if (this.level().isClientSide && passenger instanceof Player) {
|
||||
com.tiedup.remake.client.animation.BondageAnimationManager
|
||||
.stopFurniture((Player) passenger);
|
||||
com.tiedup.remake.client.animation.BondageAnimationManager.stopFurniture(
|
||||
(Player) passenger
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,7 +652,9 @@ public class EntityFurniture extends Entity
|
||||
// Don't clobber active transitions — let the tick timer handle the reset
|
||||
if (this.transitionTicksLeft > 0) return;
|
||||
|
||||
byte state = this.getPassengers().isEmpty() ? STATE_IDLE : STATE_OCCUPIED;
|
||||
byte state = this.getPassengers().isEmpty()
|
||||
? STATE_IDLE
|
||||
: STATE_OCCUPIED;
|
||||
this.entityData.set(ANIM_STATE, state);
|
||||
}
|
||||
|
||||
@@ -628,11 +684,16 @@ public class EntityFurniture extends Entity
|
||||
// Priority 1: Force-mount a leashed captive onto an available seat.
|
||||
// The interacting player must be a captor with at least one leashed captive,
|
||||
// and the captive must wear a collar owned by this player (or player is OP).
|
||||
if (player instanceof ServerPlayer serverPlayer
|
||||
&& this.getPassengers().size() < def.seats().size()) {
|
||||
PlayerBindState ownerState = PlayerBindState.getInstance(serverPlayer);
|
||||
if (
|
||||
player instanceof ServerPlayer serverPlayer &&
|
||||
this.getPassengers().size() < def.seats().size()
|
||||
) {
|
||||
PlayerBindState ownerState = PlayerBindState.getInstance(
|
||||
serverPlayer
|
||||
);
|
||||
if (ownerState != null) {
|
||||
PlayerCaptorManager captorManager = ownerState.getCaptorManager();
|
||||
PlayerCaptorManager captorManager =
|
||||
ownerState.getCaptorManager();
|
||||
if (captorManager != null && captorManager.hasCaptives()) {
|
||||
for (IBondageState captive : captorManager.getCaptives()) {
|
||||
LivingEntity captiveEntity = captive.asLivingEntity();
|
||||
@@ -647,11 +708,18 @@ public class EntityFurniture extends Entity
|
||||
|
||||
// Verify collar ownership
|
||||
if (!captive.hasCollar()) continue;
|
||||
ItemStack collarStack = captive.getEquipment(BodyRegionV2.NECK);
|
||||
if (collarStack.isEmpty()
|
||||
|| !(collarStack.getItem() instanceof ItemCollar collar)) continue;
|
||||
if (!collar.isOwner(collarStack, serverPlayer)
|
||||
&& !serverPlayer.hasPermissions(2)) continue;
|
||||
ItemStack collarStack = captive.getEquipment(
|
||||
BodyRegionV2.NECK
|
||||
);
|
||||
if (
|
||||
collarStack.isEmpty() ||
|
||||
!(collarStack.getItem() instanceof
|
||||
ItemCollar collar)
|
||||
) continue;
|
||||
if (
|
||||
!collar.isOwner(collarStack, serverPlayer) &&
|
||||
!serverPlayer.hasPermissions(2)
|
||||
) continue;
|
||||
|
||||
// Detach leash only (drop the lead, keep tied-up status)
|
||||
captive.free(false);
|
||||
@@ -662,10 +730,18 @@ public class EntityFurniture extends Entity
|
||||
// Play mount sound
|
||||
FurnitureFeedback feedback = def.feedback();
|
||||
if (feedback.mountSound() != null) {
|
||||
this.level().playSound(null,
|
||||
this.getX(), this.getY(), this.getZ(),
|
||||
SoundEvent.createVariableRangeEvent(feedback.mountSound()),
|
||||
SoundSource.BLOCKS, 1.0f, 1.0f);
|
||||
this.level().playSound(
|
||||
null,
|
||||
this.getX(),
|
||||
this.getY(),
|
||||
this.getZ(),
|
||||
SoundEvent.createVariableRangeEvent(
|
||||
feedback.mountSound()
|
||||
),
|
||||
SoundSource.BLOCKS,
|
||||
1.0f,
|
||||
1.0f
|
||||
);
|
||||
}
|
||||
|
||||
// Broadcast updated state to tracking clients
|
||||
@@ -688,7 +764,10 @@ public class EntityFurniture extends Entity
|
||||
// Use look direction to pick the nearest occupied, lockable seat.
|
||||
ItemStack heldItem = player.getItemInHand(hand);
|
||||
if (isKeyItem(heldItem) && !this.getPassengers().isEmpty()) {
|
||||
SeatDefinition targetSeat = findNearestOccupiedLockableSeat(player, def);
|
||||
SeatDefinition targetSeat = findNearestOccupiedLockableSeat(
|
||||
player,
|
||||
def
|
||||
);
|
||||
if (targetSeat != null) {
|
||||
boolean wasLocked = isSeatLocked(targetSeat.id());
|
||||
setSeatLocked(targetSeat.id(), !wasLocked);
|
||||
@@ -699,15 +778,24 @@ public class EntityFurniture extends Entity
|
||||
? feedback.unlockSound()
|
||||
: feedback.lockSound();
|
||||
if (soundRL != null) {
|
||||
this.level().playSound(null, this.getX(), this.getY(), this.getZ(),
|
||||
this.level().playSound(
|
||||
null,
|
||||
this.getX(),
|
||||
this.getY(),
|
||||
this.getZ(),
|
||||
SoundEvent.createVariableRangeEvent(soundRL),
|
||||
SoundSource.BLOCKS, 1.0f, 1.0f);
|
||||
SoundSource.BLOCKS,
|
||||
1.0f,
|
||||
1.0f
|
||||
);
|
||||
}
|
||||
|
||||
// Set lock/unlock animation with transition timer
|
||||
boolean nowLocked = !wasLocked;
|
||||
this.entityData.set(ANIM_STATE,
|
||||
nowLocked ? STATE_LOCKING : STATE_UNLOCKING);
|
||||
this.entityData.set(
|
||||
ANIM_STATE,
|
||||
nowLocked ? STATE_LOCKING : STATE_UNLOCKING
|
||||
);
|
||||
this.transitionTicksLeft = 20;
|
||||
this.transitionTargetState = STATE_OCCUPIED;
|
||||
|
||||
@@ -723,7 +811,10 @@ public class EntityFurniture extends Entity
|
||||
}
|
||||
|
||||
// Priority 3: Empty hand + available seat -> self mount
|
||||
if (heldItem.isEmpty() && this.getPassengers().size() < def.seats().size()) {
|
||||
if (
|
||||
heldItem.isEmpty() &&
|
||||
this.getPassengers().size() < def.seats().size()
|
||||
) {
|
||||
player.startRiding(this);
|
||||
return InteractionResult.CONSUME;
|
||||
}
|
||||
@@ -731,9 +822,16 @@ public class EntityFurniture extends Entity
|
||||
// No valid action — play denied sound if configured
|
||||
FurnitureFeedback feedback = def.feedback();
|
||||
if (feedback != null && feedback.deniedSound() != null) {
|
||||
this.level().playSound(null, this.getX(), this.getY(), this.getZ(),
|
||||
this.level().playSound(
|
||||
null,
|
||||
this.getX(),
|
||||
this.getY(),
|
||||
this.getZ(),
|
||||
SoundEvent.createVariableRangeEvent(feedback.deniedSound()),
|
||||
SoundSource.BLOCKS, 0.5f, 1.0f);
|
||||
SoundSource.BLOCKS,
|
||||
0.5f,
|
||||
1.0f
|
||||
);
|
||||
}
|
||||
|
||||
return InteractionResult.PASS;
|
||||
@@ -775,10 +873,22 @@ public class EntityFurniture extends Entity
|
||||
// Creative mode bypasses this check (handled above with instant discard).
|
||||
if (!this.getPassengers().isEmpty()) {
|
||||
FurnitureDefinition occupiedDef = getDefinition();
|
||||
if (occupiedDef != null && occupiedDef.feedback().deniedSound() != null) {
|
||||
this.level().playSound(null, this.getX(), this.getY(), this.getZ(),
|
||||
SoundEvent.createVariableRangeEvent(occupiedDef.feedback().deniedSound()),
|
||||
SoundSource.BLOCKS, 0.5f, 1.0f);
|
||||
if (
|
||||
occupiedDef != null &&
|
||||
occupiedDef.feedback().deniedSound() != null
|
||||
) {
|
||||
this.level().playSound(
|
||||
null,
|
||||
this.getX(),
|
||||
this.getY(),
|
||||
this.getZ(),
|
||||
SoundEvent.createVariableRangeEvent(
|
||||
occupiedDef.feedback().deniedSound()
|
||||
),
|
||||
SoundSource.BLOCKS,
|
||||
0.5f,
|
||||
1.0f
|
||||
);
|
||||
}
|
||||
return true; // Consume the attack but accumulate no damage
|
||||
}
|
||||
@@ -790,7 +900,9 @@ public class EntityFurniture extends Entity
|
||||
if (this.currentDamage >= resistance) {
|
||||
this.ejectPassengers();
|
||||
if (def != null && def.dropOnBreak()) {
|
||||
ItemStack dropStack = FurniturePlacerItem.createStack(def.id());
|
||||
ItemStack dropStack = FurniturePlacerItem.createStack(
|
||||
def.id()
|
||||
);
|
||||
if (!dropStack.isEmpty()) {
|
||||
this.spawnAtLocation(dropStack);
|
||||
}
|
||||
@@ -850,9 +962,9 @@ public class EntityFurniture extends Entity
|
||||
passengerUuids.add(passenger.getUUID());
|
||||
}
|
||||
|
||||
boolean removed = seatAssignments.keySet().removeIf(
|
||||
uuid -> !passengerUuids.contains(uuid)
|
||||
);
|
||||
boolean removed = seatAssignments
|
||||
.keySet()
|
||||
.removeIf(uuid -> !passengerUuids.contains(uuid));
|
||||
|
||||
if (removed) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
@@ -888,7 +1000,8 @@ public class EntityFurniture extends Entity
|
||||
|
||||
@Override
|
||||
protected void readAdditionalSaveData(CompoundTag tag) {
|
||||
if (tag.contains(FurnitureRegistry.NBT_FURNITURE_ID, 8)) { // 8 = TAG_String
|
||||
if (tag.contains(FurnitureRegistry.NBT_FURNITURE_ID, 8)) {
|
||||
// 8 = TAG_String
|
||||
setFurnitureId(tag.getString(FurnitureRegistry.NBT_FURNITURE_ID));
|
||||
}
|
||||
if (tag.contains("facing")) {
|
||||
@@ -909,7 +1022,8 @@ public class EntityFurniture extends Entity
|
||||
|
||||
// Restore seat assignments
|
||||
seatAssignments.clear();
|
||||
if (tag.contains("seat_assignments", 10)) { // 10 = TAG_Compound
|
||||
if (tag.contains("seat_assignments", 10)) {
|
||||
// 10 = TAG_Compound
|
||||
CompoundTag assignments = tag.getCompound("seat_assignments");
|
||||
for (String key : assignments.getAllKeys()) {
|
||||
try {
|
||||
|
||||
@@ -31,6 +31,11 @@ public record FurnitureFeedback(
|
||||
) {
|
||||
/** Empty feedback -- all sounds null (use defaults). */
|
||||
public static final FurnitureFeedback EMPTY = new FurnitureFeedback(
|
||||
null, null, null, null, null, null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,11 +32,15 @@ import org.jetbrains.annotations.Nullable;
|
||||
*/
|
||||
public final class FurnitureParser {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger("TiedUpFurniture");
|
||||
private static final Logger LOGGER = LogManager.getLogger(
|
||||
"TiedUpFurniture"
|
||||
);
|
||||
private static final String TAG = "[FurnitureParser]";
|
||||
|
||||
/** Strict hex color pattern: # followed by exactly 6 hex digits. */
|
||||
private static final Pattern HEX_COLOR = Pattern.compile("^#[0-9A-Fa-f]{6}$");
|
||||
private static final Pattern HEX_COLOR = Pattern.compile(
|
||||
"^#[0-9A-Fa-f]{6}$"
|
||||
);
|
||||
|
||||
/** Maximum number of seats per furniture (bitmask limit: 8 bits). */
|
||||
private static final int MAX_SEATS = 8;
|
||||
@@ -51,7 +55,10 @@ public final class FurnitureParser {
|
||||
* @return the parsed definition, or null if the file is invalid
|
||||
*/
|
||||
@Nullable
|
||||
public static FurnitureDefinition parse(InputStream input, ResourceLocation fileId) {
|
||||
public static FurnitureDefinition parse(
|
||||
InputStream input,
|
||||
ResourceLocation fileId
|
||||
) {
|
||||
try {
|
||||
JsonObject root = JsonParser.parseReader(
|
||||
new InputStreamReader(input, StandardCharsets.UTF_8)
|
||||
@@ -59,7 +66,12 @@ public final class FurnitureParser {
|
||||
|
||||
return parseObject(root, fileId);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("{} Failed to parse JSON {}: {}", TAG, fileId, e.getMessage());
|
||||
LOGGER.error(
|
||||
"{} Failed to parse JSON {}: {}",
|
||||
TAG,
|
||||
fileId,
|
||||
e.getMessage()
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -72,7 +84,10 @@ public final class FurnitureParser {
|
||||
* @return the parsed definition, or null if validation fails
|
||||
*/
|
||||
@Nullable
|
||||
public static FurnitureDefinition parseObject(JsonObject root, ResourceLocation fileId) {
|
||||
public static FurnitureDefinition parseObject(
|
||||
JsonObject root,
|
||||
ResourceLocation fileId
|
||||
) {
|
||||
// --- Required: id ---
|
||||
String idStr = getStringOrNull(root, "id");
|
||||
if (idStr == null || idStr.isEmpty()) {
|
||||
@@ -81,7 +96,12 @@ public final class FurnitureParser {
|
||||
}
|
||||
ResourceLocation id = ResourceLocation.tryParse(idStr);
|
||||
if (id == null) {
|
||||
LOGGER.error("{} Skipping {}: invalid id ResourceLocation '{}'", TAG, fileId, idStr);
|
||||
LOGGER.error(
|
||||
"{} Skipping {}: invalid id ResourceLocation '{}'",
|
||||
TAG,
|
||||
fileId,
|
||||
idStr
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -103,7 +123,12 @@ public final class FurnitureParser {
|
||||
}
|
||||
ResourceLocation modelLocation = ResourceLocation.tryParse(modelStr);
|
||||
if (modelLocation == null) {
|
||||
LOGGER.error("{} Skipping {}: invalid model ResourceLocation '{}'", TAG, fileId, modelStr);
|
||||
LOGGER.error(
|
||||
"{} Skipping {}: invalid model ResourceLocation '{}'",
|
||||
TAG,
|
||||
fileId,
|
||||
modelStr
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -115,15 +140,27 @@ public final class FurnitureParser {
|
||||
}
|
||||
|
||||
// --- Optional: supports_color (default false) ---
|
||||
boolean supportsColor = getBooleanOrDefault(root, "supports_color", false);
|
||||
boolean supportsColor = getBooleanOrDefault(
|
||||
root,
|
||||
"supports_color",
|
||||
false
|
||||
);
|
||||
|
||||
// --- Optional: hitbox (defaults: 1.0 x 1.0, clamped [0.1, 5.0]) ---
|
||||
float hitboxWidth = 1.0f;
|
||||
float hitboxHeight = 1.0f;
|
||||
if (root.has("hitbox") && root.get("hitbox").isJsonObject()) {
|
||||
JsonObject hitbox = root.getAsJsonObject("hitbox");
|
||||
hitboxWidth = clamp(getFloatOrDefault(hitbox, "width", 1.0f), 0.1f, 5.0f);
|
||||
hitboxHeight = clamp(getFloatOrDefault(hitbox, "height", 1.0f), 0.1f, 5.0f);
|
||||
hitboxWidth = clamp(
|
||||
getFloatOrDefault(hitbox, "width", 1.0f),
|
||||
0.1f,
|
||||
5.0f
|
||||
);
|
||||
hitboxHeight = clamp(
|
||||
getFloatOrDefault(hitbox, "height", 1.0f),
|
||||
0.1f,
|
||||
5.0f
|
||||
);
|
||||
}
|
||||
|
||||
// --- Optional: placement ---
|
||||
@@ -139,14 +176,22 @@ public final class FurnitureParser {
|
||||
boolean lockable = getBooleanOrDefault(root, "lockable", false);
|
||||
|
||||
// --- Optional: break_resistance (default 100, clamped [1, 10000]) ---
|
||||
float breakResistance = clamp(getFloatOrDefault(root, "break_resistance", 100.0f), 1.0f, 10000.0f);
|
||||
float breakResistance = clamp(
|
||||
getFloatOrDefault(root, "break_resistance", 100.0f),
|
||||
1.0f,
|
||||
10000.0f
|
||||
);
|
||||
|
||||
// --- Optional: drop_on_break (default true) ---
|
||||
boolean dropOnBreak = getBooleanOrDefault(root, "drop_on_break", true);
|
||||
|
||||
// --- Required: seats (non-empty array, size [1, 8]) ---
|
||||
if (!root.has("seats") || !root.get("seats").isJsonArray()) {
|
||||
LOGGER.error("{} Skipping {}: missing or invalid 'seats' array", TAG, fileId);
|
||||
LOGGER.error(
|
||||
"{} Skipping {}: missing or invalid 'seats' array",
|
||||
TAG,
|
||||
fileId
|
||||
);
|
||||
return null;
|
||||
}
|
||||
JsonArray seatsArray = root.getAsJsonArray("seats");
|
||||
@@ -155,18 +200,33 @@ public final class FurnitureParser {
|
||||
return null;
|
||||
}
|
||||
if (seatsArray.size() > MAX_SEATS) {
|
||||
LOGGER.error("{} Skipping {}: 'seats' array has {} entries (max {})",
|
||||
TAG, fileId, seatsArray.size(), MAX_SEATS);
|
||||
LOGGER.error(
|
||||
"{} Skipping {}: 'seats' array has {} entries (max {})",
|
||||
TAG,
|
||||
fileId,
|
||||
seatsArray.size(),
|
||||
MAX_SEATS
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
List<SeatDefinition> seats = new ArrayList<>(seatsArray.size());
|
||||
for (int i = 0; i < seatsArray.size(); i++) {
|
||||
if (!seatsArray.get(i).isJsonObject()) {
|
||||
LOGGER.error("{} Skipping {}: seats[{}] is not a JSON object", TAG, fileId, i);
|
||||
LOGGER.error(
|
||||
"{} Skipping {}: seats[{}] is not a JSON object",
|
||||
TAG,
|
||||
fileId,
|
||||
i
|
||||
);
|
||||
return null;
|
||||
}
|
||||
SeatDefinition seat = parseSeat(seatsArray.get(i).getAsJsonObject(), i, lockable, fileId);
|
||||
SeatDefinition seat = parseSeat(
|
||||
seatsArray.get(i).getAsJsonObject(),
|
||||
i,
|
||||
lockable,
|
||||
fileId
|
||||
);
|
||||
if (seat == null) {
|
||||
// parseSeat already logged the error
|
||||
return null;
|
||||
@@ -184,15 +244,30 @@ public final class FurnitureParser {
|
||||
String category = getStringOrDefault(root, "category", "furniture");
|
||||
|
||||
// --- Optional: icon (item model ResourceLocation for inventory sprite) ---
|
||||
ResourceLocation icon = parseOptionalResourceLocation(root, "icon", fileId);
|
||||
ResourceLocation icon = parseOptionalResourceLocation(
|
||||
root,
|
||||
"icon",
|
||||
fileId
|
||||
);
|
||||
|
||||
return new FurnitureDefinition(
|
||||
id, displayName, translationKey, modelLocation,
|
||||
tintChannels, supportsColor,
|
||||
hitboxWidth, hitboxHeight,
|
||||
snapToWall, floorOnly,
|
||||
lockable, breakResistance, dropOnBreak,
|
||||
seats, feedback, category, icon
|
||||
id,
|
||||
displayName,
|
||||
translationKey,
|
||||
modelLocation,
|
||||
tintChannels,
|
||||
supportsColor,
|
||||
hitboxWidth,
|
||||
hitboxHeight,
|
||||
snapToWall,
|
||||
floorOnly,
|
||||
lockable,
|
||||
breakResistance,
|
||||
dropOnBreak,
|
||||
seats,
|
||||
feedback,
|
||||
category,
|
||||
icon
|
||||
);
|
||||
}
|
||||
|
||||
@@ -208,47 +283,85 @@ public final class FurnitureParser {
|
||||
* @return the parsed seat, or null on validation failure
|
||||
*/
|
||||
@Nullable
|
||||
private static SeatDefinition parseSeat(JsonObject obj, int index,
|
||||
boolean parentLockable,
|
||||
ResourceLocation fileId) {
|
||||
private static SeatDefinition parseSeat(
|
||||
JsonObject obj,
|
||||
int index,
|
||||
boolean parentLockable,
|
||||
ResourceLocation fileId
|
||||
) {
|
||||
// Required: id (must not contain ':')
|
||||
String seatId = getStringOrNull(obj, "id");
|
||||
if (seatId == null || seatId.isEmpty()) {
|
||||
LOGGER.error("{} Skipping {}: seats[{}] missing 'id'", TAG, fileId, index);
|
||||
LOGGER.error(
|
||||
"{} Skipping {}: seats[{}] missing 'id'",
|
||||
TAG,
|
||||
fileId,
|
||||
index
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if (seatId.contains(":")) {
|
||||
LOGGER.error("{} Skipping {}: seats[{}] id '{}' must not contain ':'",
|
||||
TAG, fileId, index, seatId);
|
||||
LOGGER.error(
|
||||
"{} Skipping {}: seats[{}] id '{}' must not contain ':'",
|
||||
TAG,
|
||||
fileId,
|
||||
index,
|
||||
seatId
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Required: armature
|
||||
String armature = getStringOrNull(obj, "armature");
|
||||
if (armature == null || armature.isEmpty()) {
|
||||
LOGGER.error("{} Skipping {}: seats[{}] missing 'armature'", TAG, fileId, index);
|
||||
LOGGER.error(
|
||||
"{} Skipping {}: seats[{}] missing 'armature'",
|
||||
TAG,
|
||||
fileId,
|
||||
index
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Optional: blocked_regions (unknown region = fatal for entire furniture)
|
||||
Set<BodyRegionV2> blockedRegions = parseBlockedRegions(obj, index, fileId);
|
||||
Set<BodyRegionV2> blockedRegions = parseBlockedRegions(
|
||||
obj,
|
||||
index,
|
||||
fileId
|
||||
);
|
||||
if (blockedRegions == null) {
|
||||
// parseBlockedRegions returns null ONLY on unknown region name (fatal)
|
||||
return null;
|
||||
}
|
||||
|
||||
// Optional: lockable (inherits from top-level)
|
||||
boolean seatLockable = getBooleanOrDefault(obj, "lockable", parentLockable);
|
||||
boolean seatLockable = getBooleanOrDefault(
|
||||
obj,
|
||||
"lockable",
|
||||
parentLockable
|
||||
);
|
||||
|
||||
// Optional: locked_difficulty (clamped [1, 10000], default 1)
|
||||
int lockedDifficulty = clampInt(getIntOrDefault(obj, "locked_difficulty", 1), 1, 10000);
|
||||
int lockedDifficulty = clampInt(
|
||||
getIntOrDefault(obj, "locked_difficulty", 1),
|
||||
1,
|
||||
10000
|
||||
);
|
||||
|
||||
// Optional: item_difficulty_bonus (default false)
|
||||
boolean itemDifficultyBonus = getBooleanOrDefault(obj, "item_difficulty_bonus", false);
|
||||
boolean itemDifficultyBonus = getBooleanOrDefault(
|
||||
obj,
|
||||
"item_difficulty_bonus",
|
||||
false
|
||||
);
|
||||
|
||||
return new SeatDefinition(
|
||||
seatId, armature, blockedRegions,
|
||||
seatLockable, lockedDifficulty, itemDifficultyBonus
|
||||
seatId,
|
||||
armature,
|
||||
blockedRegions,
|
||||
seatLockable,
|
||||
lockedDifficulty,
|
||||
itemDifficultyBonus
|
||||
);
|
||||
}
|
||||
|
||||
@@ -257,15 +370,25 @@ public final class FurnitureParser {
|
||||
* Returns null (fatal) if any region name is unknown.
|
||||
*/
|
||||
@Nullable
|
||||
private static Set<BodyRegionV2> parseBlockedRegions(JsonObject obj, int seatIndex,
|
||||
ResourceLocation fileId) {
|
||||
if (!obj.has("blocked_regions") || !obj.get("blocked_regions").isJsonArray()) {
|
||||
return Collections.unmodifiableSet(EnumSet.noneOf(BodyRegionV2.class));
|
||||
private static Set<BodyRegionV2> parseBlockedRegions(
|
||||
JsonObject obj,
|
||||
int seatIndex,
|
||||
ResourceLocation fileId
|
||||
) {
|
||||
if (
|
||||
!obj.has("blocked_regions") ||
|
||||
!obj.get("blocked_regions").isJsonArray()
|
||||
) {
|
||||
return Collections.unmodifiableSet(
|
||||
EnumSet.noneOf(BodyRegionV2.class)
|
||||
);
|
||||
}
|
||||
|
||||
JsonArray arr = obj.getAsJsonArray("blocked_regions");
|
||||
if (arr.isEmpty()) {
|
||||
return Collections.unmodifiableSet(EnumSet.noneOf(BodyRegionV2.class));
|
||||
return Collections.unmodifiableSet(
|
||||
EnumSet.noneOf(BodyRegionV2.class)
|
||||
);
|
||||
}
|
||||
|
||||
EnumSet<BodyRegionV2> regions = EnumSet.noneOf(BodyRegionV2.class);
|
||||
@@ -274,15 +397,25 @@ public final class FurnitureParser {
|
||||
try {
|
||||
name = elem.getAsString().toUpperCase();
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("{} Skipping {}: seats[{}] invalid element in 'blocked_regions': {}",
|
||||
TAG, fileId, seatIndex, e.getMessage());
|
||||
LOGGER.error(
|
||||
"{} Skipping {}: seats[{}] invalid element in 'blocked_regions': {}",
|
||||
TAG,
|
||||
fileId,
|
||||
seatIndex,
|
||||
e.getMessage()
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
BodyRegionV2 region = BodyRegionV2.fromName(name);
|
||||
if (region == null) {
|
||||
LOGGER.error("{} Skipping {}: seats[{}] unknown body region '{}'",
|
||||
TAG, fileId, seatIndex, name);
|
||||
LOGGER.error(
|
||||
"{} Skipping {}: seats[{}] unknown body region '{}'",
|
||||
TAG,
|
||||
fileId,
|
||||
seatIndex,
|
||||
name
|
||||
);
|
||||
return null;
|
||||
}
|
||||
regions.add(region);
|
||||
@@ -293,7 +426,10 @@ public final class FurnitureParser {
|
||||
|
||||
// ===== Feedback Parsing =====
|
||||
|
||||
private static FurnitureFeedback parseFeedback(JsonObject obj, ResourceLocation fileId) {
|
||||
private static FurnitureFeedback parseFeedback(
|
||||
JsonObject obj,
|
||||
ResourceLocation fileId
|
||||
) {
|
||||
return new FurnitureFeedback(
|
||||
parseOptionalResourceLocation(obj, "mount_sound", fileId),
|
||||
parseOptionalResourceLocation(obj, "lock_sound", fileId),
|
||||
@@ -311,8 +447,14 @@ public final class FurnitureParser {
|
||||
* Returns empty map if field is absent. Returns null if any value is invalid hex.
|
||||
*/
|
||||
@Nullable
|
||||
private static Map<String, Integer> parseTintChannels(JsonObject root, ResourceLocation fileId) {
|
||||
if (!root.has("tint_channels") || !root.get("tint_channels").isJsonObject()) {
|
||||
private static Map<String, Integer> parseTintChannels(
|
||||
JsonObject root,
|
||||
ResourceLocation fileId
|
||||
) {
|
||||
if (
|
||||
!root.has("tint_channels") ||
|
||||
!root.get("tint_channels").isJsonObject()
|
||||
) {
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
@@ -324,15 +466,24 @@ public final class FurnitureParser {
|
||||
try {
|
||||
hex = entry.getValue().getAsString();
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("{} Skipping {}: tint_channels '{}' value is not a string",
|
||||
TAG, fileId, entry.getKey());
|
||||
LOGGER.error(
|
||||
"{} Skipping {}: tint_channels '{}' value is not a string",
|
||||
TAG,
|
||||
fileId,
|
||||
entry.getKey()
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!HEX_COLOR.matcher(hex).matches()) {
|
||||
LOGGER.error("{} Skipping {}: tint_channels '{}' has invalid hex color '{}' "
|
||||
+ "(expected '#' followed by 6 hex digits)",
|
||||
TAG, fileId, entry.getKey(), hex);
|
||||
LOGGER.error(
|
||||
"{} Skipping {}: tint_channels '{}' has invalid hex color '{}' " +
|
||||
"(expected '#' followed by 6 hex digits)",
|
||||
TAG,
|
||||
fileId,
|
||||
entry.getKey(),
|
||||
hex
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -355,12 +506,20 @@ public final class FurnitureParser {
|
||||
}
|
||||
}
|
||||
|
||||
private static String getStringOrDefault(JsonObject obj, String key, String defaultValue) {
|
||||
private static String getStringOrDefault(
|
||||
JsonObject obj,
|
||||
String key,
|
||||
String defaultValue
|
||||
) {
|
||||
String value = getStringOrNull(obj, key);
|
||||
return (value != null && !value.isEmpty()) ? value : defaultValue;
|
||||
}
|
||||
|
||||
private static int getIntOrDefault(JsonObject obj, String key, int defaultValue) {
|
||||
private static int getIntOrDefault(
|
||||
JsonObject obj,
|
||||
String key,
|
||||
int defaultValue
|
||||
) {
|
||||
if (!obj.has(key) || obj.get(key).isJsonNull()) return defaultValue;
|
||||
try {
|
||||
return obj.get(key).getAsInt();
|
||||
@@ -369,7 +528,11 @@ public final class FurnitureParser {
|
||||
}
|
||||
}
|
||||
|
||||
private static float getFloatOrDefault(JsonObject obj, String key, float defaultValue) {
|
||||
private static float getFloatOrDefault(
|
||||
JsonObject obj,
|
||||
String key,
|
||||
float defaultValue
|
||||
) {
|
||||
if (!obj.has(key) || obj.get(key).isJsonNull()) return defaultValue;
|
||||
try {
|
||||
return obj.get(key).getAsFloat();
|
||||
@@ -378,7 +541,11 @@ public final class FurnitureParser {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean getBooleanOrDefault(JsonObject obj, String key, boolean defaultValue) {
|
||||
private static boolean getBooleanOrDefault(
|
||||
JsonObject obj,
|
||||
String key,
|
||||
boolean defaultValue
|
||||
) {
|
||||
if (!obj.has(key) || obj.get(key).isJsonNull()) return defaultValue;
|
||||
try {
|
||||
return obj.get(key).getAsBoolean();
|
||||
@@ -389,13 +556,21 @@ public final class FurnitureParser {
|
||||
|
||||
@Nullable
|
||||
private static ResourceLocation parseOptionalResourceLocation(
|
||||
JsonObject obj, String key, ResourceLocation fileId
|
||||
JsonObject obj,
|
||||
String key,
|
||||
ResourceLocation fileId
|
||||
) {
|
||||
String value = getStringOrNull(obj, key);
|
||||
if (value == null || value.isEmpty()) return null;
|
||||
ResourceLocation loc = ResourceLocation.tryParse(value);
|
||||
if (loc == null) {
|
||||
LOGGER.warn("{} In {}: invalid ResourceLocation for '{}': '{}'", TAG, fileId, key, value);
|
||||
LOGGER.warn(
|
||||
"{} In {}: invalid ResourceLocation for '{}': '{}'",
|
||||
TAG,
|
||||
fileId,
|
||||
key,
|
||||
value
|
||||
);
|
||||
}
|
||||
return loc;
|
||||
}
|
||||
|
||||
@@ -86,7 +86,8 @@ public class FurniturePlacerItem extends Item {
|
||||
|
||||
// Spawn the furniture entity
|
||||
EntityFurniture furniture = new EntityFurniture(
|
||||
ModEntities.FURNITURE.get(), level
|
||||
ModEntities.FURNITURE.get(),
|
||||
level
|
||||
);
|
||||
furniture.setFurnitureId(furnitureIdStr);
|
||||
furniture.moveTo(spawnPos.x, spawnPos.y, spawnPos.z);
|
||||
@@ -101,10 +102,19 @@ public class FurniturePlacerItem extends Item {
|
||||
// Snap to wall: if enabled, check 4 cardinal directions for an adjacent wall
|
||||
// and rotate the furniture to face it (back against wall), overriding player yaw.
|
||||
if (def.snapToWall()) {
|
||||
Direction[] directions = {Direction.NORTH, Direction.SOUTH, Direction.EAST, Direction.WEST};
|
||||
Direction[] directions = {
|
||||
Direction.NORTH,
|
||||
Direction.SOUTH,
|
||||
Direction.EAST,
|
||||
Direction.WEST,
|
||||
};
|
||||
for (Direction dir : directions) {
|
||||
BlockPos wallPos = spawnBlockPos.relative(dir);
|
||||
if (level.getBlockState(wallPos).isFaceSturdy(level, wallPos, dir.getOpposite())) {
|
||||
if (
|
||||
level
|
||||
.getBlockState(wallPos)
|
||||
.isFaceSturdy(level, wallPos, dir.getOpposite())
|
||||
) {
|
||||
yaw = dir.toYRot();
|
||||
break;
|
||||
}
|
||||
@@ -155,9 +165,12 @@ public class FurniturePlacerItem extends Item {
|
||||
public static ItemStack createStack(ResourceLocation furnitureId) {
|
||||
if (V2BondageItems.FURNITURE_PLACER == null) return ItemStack.EMPTY;
|
||||
ItemStack stack = new ItemStack(V2BondageItems.FURNITURE_PLACER.get());
|
||||
stack.getOrCreateTag().putString(
|
||||
FurnitureRegistry.NBT_FURNITURE_ID, furnitureId.toString()
|
||||
);
|
||||
stack
|
||||
.getOrCreateTag()
|
||||
.putString(
|
||||
FurnitureRegistry.NBT_FURNITURE_ID,
|
||||
furnitureId.toString()
|
||||
);
|
||||
return stack;
|
||||
}
|
||||
|
||||
@@ -173,8 +186,12 @@ public class FurniturePlacerItem extends Item {
|
||||
public static String getFurnitureIdFromStack(ItemStack stack) {
|
||||
if (stack.isEmpty() || !stack.hasTag()) return null;
|
||||
// noinspection DataFlowIssue -- hasTag() guarantees non-null
|
||||
if (!stack.getTag().contains(FurnitureRegistry.NBT_FURNITURE_ID, 8)) return null;
|
||||
String id = stack.getTag().getString(FurnitureRegistry.NBT_FURNITURE_ID);
|
||||
if (
|
||||
!stack.getTag().contains(FurnitureRegistry.NBT_FURNITURE_ID, 8)
|
||||
) return null;
|
||||
String id = stack
|
||||
.getTag()
|
||||
.getString(FurnitureRegistry.NBT_FURNITURE_ID);
|
||||
return id.isEmpty() ? null : id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,10 @@ public final class FurnitureRegistry {
|
||||
* Volatile reference to an unmodifiable map. {@link #reload} builds a new map
|
||||
* and swaps atomically; consumer threads always see a consistent snapshot.
|
||||
*/
|
||||
private static volatile Map<ResourceLocation, FurnitureDefinition> DEFINITIONS = Map.of();
|
||||
private static volatile Map<
|
||||
ResourceLocation,
|
||||
FurnitureDefinition
|
||||
> DEFINITIONS = Map.of();
|
||||
|
||||
private FurnitureRegistry() {}
|
||||
|
||||
@@ -41,7 +44,9 @@ public final class FurnitureRegistry {
|
||||
*
|
||||
* @param newDefs the new definitions map (will be defensively copied)
|
||||
*/
|
||||
public static void reload(Map<ResourceLocation, FurnitureDefinition> newDefs) {
|
||||
public static void reload(
|
||||
Map<ResourceLocation, FurnitureDefinition> newDefs
|
||||
) {
|
||||
DEFINITIONS = Collections.unmodifiableMap(new HashMap<>(newDefs));
|
||||
}
|
||||
|
||||
|
||||
@@ -22,30 +22,45 @@ import org.apache.logging.log4j.Logger;
|
||||
* <p>Registered via {@link net.minecraftforge.event.AddReloadListenerEvent} in
|
||||
* {@link com.tiedup.remake.core.TiedUpMod.ForgeEvents}.</p>
|
||||
*/
|
||||
public class FurnitureServerReloadListener extends SimplePreparableReloadListener<Void> {
|
||||
public class FurnitureServerReloadListener
|
||||
extends SimplePreparableReloadListener<Void>
|
||||
{
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger("TiedUpFurniture");
|
||||
private static final Logger LOGGER = LogManager.getLogger(
|
||||
"TiedUpFurniture"
|
||||
);
|
||||
|
||||
/** Resource directory containing furniture definition JSON files (under data/). */
|
||||
private static final String DIRECTORY = "tiedup_furniture";
|
||||
|
||||
@Override
|
||||
protected Void prepare(ResourceManager resourceManager, ProfilerFiller profiler) {
|
||||
protected Void prepare(
|
||||
ResourceManager resourceManager,
|
||||
ProfilerFiller profiler
|
||||
) {
|
||||
// No preparation needed -- parsing happens in apply phase
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void apply(Void nothing, ResourceManager resourceManager, ProfilerFiller profiler) {
|
||||
protected void apply(
|
||||
Void nothing,
|
||||
ResourceManager resourceManager,
|
||||
ProfilerFiller profiler
|
||||
) {
|
||||
Map<ResourceLocation, FurnitureDefinition> newDefs = new HashMap<>();
|
||||
|
||||
Map<ResourceLocation, Resource> resources = resourceManager.listResources(
|
||||
DIRECTORY, loc -> loc.getPath().endsWith(".json")
|
||||
);
|
||||
Map<ResourceLocation, Resource> resources =
|
||||
resourceManager.listResources(DIRECTORY, loc ->
|
||||
loc.getPath().endsWith(".json")
|
||||
);
|
||||
|
||||
int skipped = 0;
|
||||
|
||||
for (Map.Entry<ResourceLocation, Resource> entry : resources.entrySet()) {
|
||||
for (Map.Entry<
|
||||
ResourceLocation,
|
||||
Resource
|
||||
> entry : resources.entrySet()) {
|
||||
ResourceLocation fileId = entry.getKey();
|
||||
Resource resource = entry.getValue();
|
||||
|
||||
@@ -55,17 +70,28 @@ public class FurnitureServerReloadListener extends SimplePreparableReloadListene
|
||||
if (def != null) {
|
||||
// Check for duplicate IDs
|
||||
if (newDefs.containsKey(def.id())) {
|
||||
LOGGER.warn("[TiedUpFurniture] Server: Duplicate furniture ID '{}' from file '{}' -- overwriting previous definition",
|
||||
def.id(), fileId);
|
||||
LOGGER.warn(
|
||||
"[TiedUpFurniture] Server: Duplicate furniture ID '{}' from file '{}' -- overwriting previous definition",
|
||||
def.id(),
|
||||
fileId
|
||||
);
|
||||
}
|
||||
|
||||
newDefs.put(def.id(), def);
|
||||
LOGGER.debug("[TiedUpFurniture] Server loaded: {} -> '{}'", def.id(), def.displayName());
|
||||
LOGGER.debug(
|
||||
"[TiedUpFurniture] Server loaded: {} -> '{}'",
|
||||
def.id(),
|
||||
def.displayName()
|
||||
);
|
||||
} else {
|
||||
skipped++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("[TiedUpFurniture] Server: Failed to read resource {}: {}", fileId, e.getMessage());
|
||||
LOGGER.error(
|
||||
"[TiedUpFurniture] Server: Failed to read resource {}: {}",
|
||||
fileId,
|
||||
e.getMessage()
|
||||
);
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
@@ -77,12 +103,20 @@ public class FurnitureServerReloadListener extends SimplePreparableReloadListene
|
||||
net.minecraft.server.MinecraftServer server =
|
||||
net.minecraftforge.server.ServerLifecycleHooks.getCurrentServer();
|
||||
if (server != null) {
|
||||
for (net.minecraft.server.level.ServerPlayer p : server.getPlayerList().getPlayers()) {
|
||||
com.tiedup.remake.v2.furniture.network.PacketSyncFurnitureDefinitions.sendToPlayer(p);
|
||||
for (net.minecraft.server.level.ServerPlayer p : server
|
||||
.getPlayerList()
|
||||
.getPlayers()) {
|
||||
com.tiedup.remake.v2.furniture.network.PacketSyncFurnitureDefinitions.sendToPlayer(
|
||||
p
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.info("[TiedUpFurniture] Server loaded {} furniture definitions ({} skipped) from {} JSON files",
|
||||
newDefs.size(), skipped, resources.size());
|
||||
LOGGER.info(
|
||||
"[TiedUpFurniture] Server loaded {} furniture definitions ({} skipped) from {} JSON files",
|
||||
newDefs.size(),
|
||||
skipped,
|
||||
resources.size()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import org.jetbrains.annotations.Nullable;
|
||||
* never EntityFurniture directly.</p>
|
||||
*/
|
||||
public interface ISeatProvider {
|
||||
|
||||
/** All seat definitions for this entity. */
|
||||
List<SeatDefinition> getSeats();
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import dev.kosmx.playerAnim.core.data.KeyframeAnimation;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Builds a {@link KeyframeAnimation} for a player seated on furniture.
|
||||
@@ -43,7 +43,9 @@ import org.apache.logging.log4j.Logger;
|
||||
@OnlyIn(Dist.CLIENT)
|
||||
public final class FurnitureAnimationContext {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger("FurnitureAnimation");
|
||||
private static final Logger LOGGER = LogManager.getLogger(
|
||||
"FurnitureAnimation"
|
||||
);
|
||||
|
||||
private FurnitureAnimationContext() {}
|
||||
|
||||
@@ -61,24 +63,30 @@ public final class FurnitureAnimationContext {
|
||||
*/
|
||||
@Nullable
|
||||
public static KeyframeAnimation create(
|
||||
GltfData.AnimationClip seatClip,
|
||||
@Nullable GltfData seatSkeleton,
|
||||
Set<BodyRegionV2> blockedRegions) {
|
||||
|
||||
GltfData.AnimationClip seatClip,
|
||||
@Nullable GltfData seatSkeleton,
|
||||
Set<BodyRegionV2> blockedRegions
|
||||
) {
|
||||
if (seatClip == null) {
|
||||
LOGGER.warn("[FurnitureAnim] Cannot create animation: seatClip is null");
|
||||
LOGGER.warn(
|
||||
"[FurnitureAnim] Cannot create animation: seatClip is null"
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (seatSkeleton == null) {
|
||||
// V1: skeleton parsing not yet implemented. Furniture animation requires
|
||||
// rest pose data for glTF-to-PlayerAnimator conversion.
|
||||
LOGGER.debug("[FurnitureAnim] Seat skeleton unavailable (V1), skipping animation");
|
||||
LOGGER.debug(
|
||||
"[FurnitureAnim] Seat skeleton unavailable (V1), skipping animation"
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (blockedRegions == null || blockedRegions.isEmpty()) {
|
||||
LOGGER.debug("[FurnitureAnim] No blocked regions, skipping animation");
|
||||
LOGGER.debug(
|
||||
"[FurnitureAnim] No blocked regions, skipping animation"
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -87,9 +95,15 @@ public final class FurnitureAnimationContext {
|
||||
KeyframeAnimation fullAnim;
|
||||
try {
|
||||
fullAnim = GltfPoseConverter.convertWithSkeleton(
|
||||
seatSkeleton, seatClip, "furniture_seat");
|
||||
seatSkeleton,
|
||||
seatClip,
|
||||
"furniture_seat"
|
||||
);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("[FurnitureAnim] Failed to convert seat animation clip", e);
|
||||
LOGGER.error(
|
||||
"[FurnitureAnim] Failed to convert seat animation clip",
|
||||
e
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -101,7 +115,10 @@ public final class FurnitureAnimationContext {
|
||||
|
||||
if (blockedParts.isEmpty()) {
|
||||
// Blocked regions don't map to any animation bones (e.g., only NECK/FINGERS/TAIL/WINGS)
|
||||
LOGGER.debug("[FurnitureAnim] Blocked regions {} map to zero bones, skipping", blockedRegions);
|
||||
LOGGER.debug(
|
||||
"[FurnitureAnim] Blocked regions {} map to zero bones, skipping",
|
||||
blockedRegions
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -119,8 +136,12 @@ public final class FurnitureAnimationContext {
|
||||
disableParts(builder, disabledParts);
|
||||
|
||||
KeyframeAnimation result = builder.build();
|
||||
LOGGER.debug("[FurnitureAnim] Created animation: blocked={}, enabled={}, disabled={}",
|
||||
blockedRegions, blockedParts, disabledParts);
|
||||
LOGGER.debug(
|
||||
"[FurnitureAnim] Created animation: blocked={}, enabled={}, disabled={}",
|
||||
blockedRegions,
|
||||
blockedParts,
|
||||
disabledParts
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -135,7 +156,9 @@ public final class FurnitureAnimationContext {
|
||||
* @param disabledParts set of PlayerAnimator part names to disable
|
||||
*/
|
||||
private static void disableParts(
|
||||
KeyframeAnimation.AnimationBuilder builder, Set<String> disabledParts) {
|
||||
KeyframeAnimation.AnimationBuilder builder,
|
||||
Set<String> disabledParts
|
||||
) {
|
||||
for (String partName : disabledParts) {
|
||||
KeyframeAnimation.StateCollection part = builder.getPart(partName);
|
||||
if (part != null) {
|
||||
|
||||
@@ -64,11 +64,18 @@ public class FurnitureEntityRenderer extends EntityRenderer<EntityFurniture> {
|
||||
GltfData meshData = data.furnitureMesh();
|
||||
|
||||
// Compute joint matrices: animated if there is an active clip, static otherwise
|
||||
GltfData.AnimationClip activeClip = resolveActiveAnimation(entity, meshData);
|
||||
GltfData.AnimationClip activeClip = resolveActiveAnimation(
|
||||
entity,
|
||||
meshData
|
||||
);
|
||||
Matrix4f[] joints;
|
||||
if (activeClip != null) {
|
||||
float time = computeAnimationTime(entity, activeClip, partialTick);
|
||||
joints = GltfSkinningEngine.computeJointMatricesAnimated(meshData, activeClip, time);
|
||||
joints = GltfSkinningEngine.computeJointMatricesAnimated(
|
||||
meshData,
|
||||
activeClip,
|
||||
time
|
||||
);
|
||||
} else {
|
||||
joints = GltfSkinningEngine.computeJointMatrices(meshData);
|
||||
}
|
||||
@@ -88,15 +95,26 @@ public class FurnitureEntityRenderer extends EntityRenderer<EntityFurniture> {
|
||||
// has multiple primitives (tintable and non-tintable parts).
|
||||
Map<String, Integer> tintColors = def.tintChannels();
|
||||
if (!tintColors.isEmpty() && meshData.primitives().size() > 1) {
|
||||
RenderType renderType = GltfMeshRenderer.getRenderTypeForDefaultTexture();
|
||||
RenderType renderType =
|
||||
GltfMeshRenderer.getRenderTypeForDefaultTexture();
|
||||
GltfMeshRenderer.renderSkinnedTinted(
|
||||
meshData, joints, poseStack, buffer,
|
||||
packedLight, packedOverlay, renderType, tintColors
|
||||
meshData,
|
||||
joints,
|
||||
poseStack,
|
||||
buffer,
|
||||
packedLight,
|
||||
packedOverlay,
|
||||
renderType,
|
||||
tintColors
|
||||
);
|
||||
} else {
|
||||
GltfMeshRenderer.renderSkinned(
|
||||
meshData, joints, poseStack, buffer,
|
||||
packedLight, packedOverlay
|
||||
meshData,
|
||||
joints,
|
||||
poseStack,
|
||||
buffer,
|
||||
packedLight,
|
||||
packedOverlay
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,19 +135,22 @@ public class FurnitureEntityRenderer extends EntityRenderer<EntityFurniture> {
|
||||
* @return the resolved animation clip, or null for static rendering
|
||||
*/
|
||||
private GltfData.AnimationClip resolveActiveAnimation(
|
||||
EntityFurniture entity, GltfData meshData
|
||||
EntityFurniture entity,
|
||||
GltfData meshData
|
||||
) {
|
||||
String animName = switch (entity.getAnimState()) {
|
||||
case EntityFurniture.STATE_OCCUPIED -> "Occupied";
|
||||
case EntityFurniture.STATE_LOCKING -> "LockClose";
|
||||
case EntityFurniture.STATE_STRUGGLE -> "Shake";
|
||||
case EntityFurniture.STATE_UNLOCKING -> "LockOpen";
|
||||
case EntityFurniture.STATE_ENTERING -> "Occupied"; // furniture plays Occupied during player enter transition
|
||||
case EntityFurniture.STATE_EXITING -> "Idle"; // furniture transitions to Idle during player exit
|
||||
case EntityFurniture.STATE_ENTERING -> "Occupied"; // furniture plays Occupied during player enter transition
|
||||
case EntityFurniture.STATE_EXITING -> "Idle"; // furniture transitions to Idle during player exit
|
||||
default -> "Idle";
|
||||
};
|
||||
GltfData.AnimationClip clip = meshData.getAnimation(animName);
|
||||
if (clip == null && entity.getAnimState() != EntityFurniture.STATE_IDLE) {
|
||||
if (
|
||||
clip == null && entity.getAnimState() != EntityFurniture.STATE_IDLE
|
||||
) {
|
||||
// Specific state animation missing: fall back to Idle
|
||||
clip = meshData.getAnimation("Idle");
|
||||
}
|
||||
@@ -150,7 +171,9 @@ public class FurnitureEntityRenderer extends EntityRenderer<EntityFurniture> {
|
||||
* @return time in frame-space, suitable for {@link GltfSkinningEngine#computeJointMatricesAnimated}
|
||||
*/
|
||||
private float computeAnimationTime(
|
||||
EntityFurniture entity, GltfData.AnimationClip clip, float partialTick
|
||||
EntityFurniture entity,
|
||||
GltfData.AnimationClip clip,
|
||||
float partialTick
|
||||
) {
|
||||
int frameCount = clip.frameCount();
|
||||
if (frameCount <= 1) return 0f;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,6 @@ package com.tiedup.remake.v2.furniture.client;
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.packs.resources.Resource;
|
||||
@@ -11,6 +10,7 @@ import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Lazy-loading cache for parsed multi-armature furniture GLB data.
|
||||
@@ -31,14 +31,11 @@ public final class FurnitureGltfCache {
|
||||
* Sentinel value stored in the cache when loading fails, to avoid retrying
|
||||
* broken resources on every frame.
|
||||
*/
|
||||
private static final FurnitureGltfData FAILED_SENTINEL = new FurnitureGltfData(
|
||||
null,
|
||||
Map.of(),
|
||||
Map.of(),
|
||||
Map.of()
|
||||
);
|
||||
private static final FurnitureGltfData FAILED_SENTINEL =
|
||||
new FurnitureGltfData(null, Map.of(), Map.of(), Map.of());
|
||||
|
||||
private static final Map<ResourceLocation, FurnitureGltfData> CACHE = new ConcurrentHashMap<>();
|
||||
private static final Map<ResourceLocation, FurnitureGltfData> CACHE =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
private FurnitureGltfCache() {}
|
||||
|
||||
@@ -51,7 +48,10 @@ public final class FurnitureGltfCache {
|
||||
*/
|
||||
@Nullable
|
||||
public static FurnitureGltfData get(ResourceLocation modelLocation) {
|
||||
FurnitureGltfData cached = CACHE.computeIfAbsent(modelLocation, FurnitureGltfCache::load);
|
||||
FurnitureGltfData cached = CACHE.computeIfAbsent(
|
||||
modelLocation,
|
||||
FurnitureGltfCache::load
|
||||
);
|
||||
return cached == FAILED_SENTINEL ? null : cached;
|
||||
}
|
||||
|
||||
@@ -72,12 +72,19 @@ public final class FurnitureGltfCache {
|
||||
}
|
||||
|
||||
try (InputStream is = resource.open()) {
|
||||
FurnitureGltfData data = FurnitureGlbParser.parse(is, loc.toString());
|
||||
FurnitureGltfData data = FurnitureGlbParser.parse(
|
||||
is,
|
||||
loc.toString()
|
||||
);
|
||||
LOGGER.debug("[FurnitureGltf] Cached: {}", loc);
|
||||
return data;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("[FurnitureGltf] Failed to load furniture GLB: {}", loc, e);
|
||||
LOGGER.error(
|
||||
"[FurnitureGltf] Failed to load furniture GLB: {}",
|
||||
loc,
|
||||
e
|
||||
);
|
||||
return FAILED_SENTINEL;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,9 @@ public final class FurnitureSeatPositionHelper {
|
||||
public static double[] getSeatWorldPosition(
|
||||
FurnitureDefinition def,
|
||||
String seatId,
|
||||
double furnitureX, double furnitureY, double furnitureZ,
|
||||
double furnitureX,
|
||||
double furnitureY,
|
||||
double furnitureZ,
|
||||
float furnitureYRot
|
||||
) {
|
||||
ResourceLocation modelLoc = def.modelLocation();
|
||||
@@ -47,7 +49,9 @@ public final class FurnitureSeatPositionHelper {
|
||||
FurnitureGltfData gltfData = FurnitureGltfCache.get(modelLoc);
|
||||
if (gltfData == null) return null;
|
||||
|
||||
FurnitureGltfData.SeatTransform transform = gltfData.seatTransforms().get(seatId);
|
||||
FurnitureGltfData.SeatTransform transform = gltfData
|
||||
.seatTransforms()
|
||||
.get(seatId);
|
||||
if (transform == null) return null;
|
||||
|
||||
// The seat transform position is in Minecraft model space (post-conversion):
|
||||
@@ -67,7 +71,7 @@ public final class FurnitureSeatPositionHelper {
|
||||
return new double[] {
|
||||
furnitureX + rx,
|
||||
furnitureY + sy,
|
||||
furnitureZ + rz
|
||||
furnitureZ + rz,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,10 @@ public class PacketFurnitureEscape {
|
||||
ctx.setPacketHandled(true);
|
||||
}
|
||||
|
||||
private static void handleOnServer(PacketFurnitureEscape msg, NetworkEvent.Context ctx) {
|
||||
private static void handleOnServer(
|
||||
PacketFurnitureEscape msg,
|
||||
NetworkEvent.Context ctx
|
||||
) {
|
||||
ServerPlayer sender = ctx.getSender();
|
||||
if (sender == null) return;
|
||||
|
||||
@@ -103,10 +106,14 @@ public class PacketFurnitureEscape {
|
||||
if (!entity.isAlive() || entity.isRemoved()) return;
|
||||
|
||||
// Validate escape method
|
||||
if (msg.escapeMethod != METHOD_STRUGGLE && msg.escapeMethod != METHOD_LOCKPICK) {
|
||||
if (
|
||||
msg.escapeMethod != METHOD_STRUGGLE &&
|
||||
msg.escapeMethod != METHOD_LOCKPICK
|
||||
) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[PacketFurnitureEscape] Invalid escape method {} from {}",
|
||||
msg.escapeMethod, sender.getName().getString()
|
||||
msg.escapeMethod,
|
||||
sender.getName().getString()
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -133,7 +140,8 @@ public class PacketFurnitureEscape {
|
||||
if (!furnitureEntity.hasPassenger(sender)) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PacketFurnitureEscape] Struggle: {} is not a passenger of furniture {}",
|
||||
sender.getName().getString(), furnitureEntity.getId()
|
||||
sender.getName().getString(),
|
||||
furnitureEntity.getId()
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -160,12 +168,18 @@ public class PacketFurnitureEscape {
|
||||
// Compute difficulty
|
||||
int baseDifficulty = provider.getLockedDifficulty(seat.id());
|
||||
int itemBonus = computeItemDifficultyBonus(sender, provider, seat);
|
||||
int totalDifficulty = Math.min(baseDifficulty + itemBonus, MAX_DIFFICULTY);
|
||||
int totalDifficulty = Math.min(
|
||||
baseDifficulty + itemBonus,
|
||||
MAX_DIFFICULTY
|
||||
);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PacketFurnitureEscape] Struggle: {} on seat '{}' — difficulty {} (base {} + items {})",
|
||||
sender.getName().getString(), seat.id(),
|
||||
totalDifficulty, baseDifficulty, itemBonus
|
||||
sender.getName().getString(),
|
||||
seat.id(),
|
||||
totalDifficulty,
|
||||
baseDifficulty,
|
||||
itemBonus
|
||||
);
|
||||
|
||||
// Difficulty 0: immediate escape (no minigame needed)
|
||||
@@ -181,13 +195,16 @@ public class PacketFurnitureEscape {
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[PacketFurnitureEscape] {} escaped furniture {} (difficulty was 0)",
|
||||
sender.getName().getString(), furnitureEntity.getId()
|
||||
sender.getName().getString(),
|
||||
furnitureEntity.getId()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Respect server config: if struggle minigame is disabled, skip
|
||||
if (!com.tiedup.remake.core.ModConfig.SERVER.struggleMiniGameEnabled.get()) {
|
||||
if (
|
||||
!com.tiedup.remake.core.ModConfig.SERVER.struggleMiniGameEnabled.get()
|
||||
) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PacketFurnitureEscape] Struggle minigame disabled by server config"
|
||||
);
|
||||
@@ -196,9 +213,13 @@ public class PacketFurnitureEscape {
|
||||
|
||||
// Launch struggle minigame session via StruggleSessionManager
|
||||
StruggleSessionManager manager = StruggleSessionManager.getInstance();
|
||||
ContinuousStruggleMiniGameState session = manager.startFurnitureStruggleSession(
|
||||
sender, furnitureEntity.getId(), seat.id(), totalDifficulty
|
||||
);
|
||||
ContinuousStruggleMiniGameState session =
|
||||
manager.startFurnitureStruggleSession(
|
||||
sender,
|
||||
furnitureEntity.getId(),
|
||||
seat.id(),
|
||||
totalDifficulty
|
||||
);
|
||||
|
||||
if (session != null) {
|
||||
// Send START packet to open the struggle GUI on the client
|
||||
@@ -255,7 +276,11 @@ public class PacketFurnitureEscape {
|
||||
// Use look-direction-based seat targeting (same vector math as
|
||||
// EntityFurniture.findNearestOccupiedLockableSeat) instead of
|
||||
// blindly picking the first locked seat.
|
||||
SeatDefinition targetSeat = findNearestLockedOccupiedSeat(sender, provider, furnitureEntity);
|
||||
SeatDefinition targetSeat = findNearestLockedOccupiedSeat(
|
||||
sender,
|
||||
provider,
|
||||
furnitureEntity
|
||||
);
|
||||
if (targetSeat == null) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PacketFurnitureEscape] Lockpick: no locked occupied seat found"
|
||||
@@ -264,30 +289,52 @@ public class PacketFurnitureEscape {
|
||||
}
|
||||
|
||||
// Find the passenger in this seat (needed for item bonus computation)
|
||||
Entity passenger = findPassengerInSeat(provider, furnitureEntity, targetSeat.id());
|
||||
Entity passenger = findPassengerInSeat(
|
||||
provider,
|
||||
furnitureEntity,
|
||||
targetSeat.id()
|
||||
);
|
||||
|
||||
// Compute difficulty
|
||||
int baseDifficulty = provider.getLockedDifficulty(targetSeat.id());
|
||||
int itemBonus = 0;
|
||||
if (passenger instanceof LivingEntity livingPassenger) {
|
||||
itemBonus = computeItemDifficultyBonus(livingPassenger, provider, targetSeat);
|
||||
itemBonus = computeItemDifficultyBonus(
|
||||
livingPassenger,
|
||||
provider,
|
||||
targetSeat
|
||||
);
|
||||
}
|
||||
int totalDifficulty = Math.min(baseDifficulty + itemBonus, MAX_DIFFICULTY);
|
||||
int totalDifficulty = Math.min(
|
||||
baseDifficulty + itemBonus,
|
||||
MAX_DIFFICULTY
|
||||
);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PacketFurnitureEscape] Lockpick: {} on seat '{}' -- difficulty {} (base {} + items {})",
|
||||
sender.getName().getString(), targetSeat.id(),
|
||||
totalDifficulty, baseDifficulty, itemBonus
|
||||
sender.getName().getString(),
|
||||
targetSeat.id(),
|
||||
totalDifficulty,
|
||||
baseDifficulty,
|
||||
itemBonus
|
||||
);
|
||||
|
||||
// Difficulty 0: immediate success — unlock + dismount + consume lockpick
|
||||
if (totalDifficulty == 0) {
|
||||
completeLockpickSuccess(sender, furnitureEntity, provider, targetSeat, passenger);
|
||||
completeLockpickSuccess(
|
||||
sender,
|
||||
furnitureEntity,
|
||||
provider,
|
||||
targetSeat,
|
||||
passenger
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Respect server config: if lockpick minigame is disabled, skip
|
||||
if (!com.tiedup.remake.core.ModConfig.SERVER.lockpickMiniGameEnabled.get()) {
|
||||
if (
|
||||
!com.tiedup.remake.core.ModConfig.SERVER.lockpickMiniGameEnabled.get()
|
||||
) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PacketFurnitureEscape] Lockpick minigame disabled by server config"
|
||||
);
|
||||
@@ -298,11 +345,15 @@ public class PacketFurnitureEscape {
|
||||
ItemStack lockpickStack = ItemLockpick.findLockpickInInventory(sender);
|
||||
if (lockpickStack.isEmpty()) return; // double-check; hasLockpickInInventory passed above
|
||||
|
||||
int remainingUses = lockpickStack.getMaxDamage() - lockpickStack.getDamageValue();
|
||||
int remainingUses =
|
||||
lockpickStack.getMaxDamage() - lockpickStack.getDamageValue();
|
||||
|
||||
// Sweet spot width scales inversely with difficulty: harder locks = narrower sweet spot.
|
||||
// Base width 0.15 at difficulty 1, down to 0.03 at MAX_DIFFICULTY.
|
||||
float sweetSpotWidth = Math.max(0.03f, 0.15f - (totalDifficulty / (float) MAX_DIFFICULTY) * 0.12f);
|
||||
float sweetSpotWidth = Math.max(
|
||||
0.03f,
|
||||
0.15f - (totalDifficulty / (float) MAX_DIFFICULTY) * 0.12f
|
||||
);
|
||||
|
||||
// Start lockpick session via LockpickSessionManager.
|
||||
// The existing lockpick session uses a targetSlot (BodyRegionV2 ordinal) for
|
||||
@@ -311,7 +362,8 @@ public class PacketFurnitureEscape {
|
||||
// For now, we use the simplified approach: start the session and let the existing
|
||||
// PacketLockpickAttempt handler manage the sweet-spot interaction. On success,
|
||||
// the furniture-specific completion is handled by a post-session check.
|
||||
LockpickSessionManager lockpickManager = LockpickSessionManager.getInstance();
|
||||
LockpickSessionManager lockpickManager =
|
||||
LockpickSessionManager.getInstance();
|
||||
LockpickMiniGameState session = lockpickManager.startLockpickSession(
|
||||
sender,
|
||||
furnitureEntity.getId(), // repurpose targetSlot as entity ID
|
||||
@@ -350,8 +402,11 @@ public class PacketFurnitureEscape {
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[PacketFurnitureEscape] {} started lockpick on seat '{}' of furniture {} (difficulty {}, sweet spot width {})",
|
||||
sender.getName().getString(), targetSeat.id(),
|
||||
furnitureEntity.getId(), totalDifficulty, sweetSpotWidth
|
||||
sender.getName().getString(),
|
||||
targetSeat.id(),
|
||||
furnitureEntity.getId(),
|
||||
totalDifficulty,
|
||||
sweetSpotWidth
|
||||
);
|
||||
}
|
||||
|
||||
@@ -368,7 +423,9 @@ public class PacketFurnitureEscape {
|
||||
) {
|
||||
provider.setSeatLocked(targetSeat.id(), false);
|
||||
if (passenger instanceof ServerPlayer passengerPlayer) {
|
||||
passengerPlayer.getPersistentData().remove("tiedup_locked_furniture");
|
||||
passengerPlayer
|
||||
.getPersistentData()
|
||||
.remove("tiedup_locked_furniture");
|
||||
}
|
||||
if (passenger != null) {
|
||||
passenger.stopRiding();
|
||||
@@ -378,7 +435,9 @@ public class PacketFurnitureEscape {
|
||||
ItemStack lockpickStack = ItemLockpick.findLockpickInInventory(sender);
|
||||
if (!lockpickStack.isEmpty()) {
|
||||
lockpickStack.setDamageValue(lockpickStack.getDamageValue() + 1);
|
||||
if (lockpickStack.getDamageValue() >= lockpickStack.getMaxDamage()) {
|
||||
if (
|
||||
lockpickStack.getDamageValue() >= lockpickStack.getMaxDamage()
|
||||
) {
|
||||
lockpickStack.shrink(1);
|
||||
}
|
||||
}
|
||||
@@ -390,7 +449,9 @@ public class PacketFurnitureEscape {
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[PacketFurnitureEscape] {} lockpicked seat '{}' on furniture {} (difficulty was 0)",
|
||||
sender.getName().getString(), targetSeat.id(), furnitureEntity.getId()
|
||||
sender.getName().getString(),
|
||||
targetSeat.id(),
|
||||
furnitureEntity.getId()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -411,8 +472,11 @@ public class PacketFurnitureEscape {
|
||||
return 0;
|
||||
}
|
||||
|
||||
Set<BodyRegionV2> blockedRegions = provider.getBlockedRegions(seat.id());
|
||||
Map<BodyRegionV2, ItemStack> equipped = V2EquipmentHelper.getAllEquipped(passenger);
|
||||
Set<BodyRegionV2> blockedRegions = provider.getBlockedRegions(
|
||||
seat.id()
|
||||
);
|
||||
Map<BodyRegionV2, ItemStack> equipped =
|
||||
V2EquipmentHelper.getAllEquipped(passenger);
|
||||
|
||||
int bonus = 0;
|
||||
for (Map.Entry<BodyRegionV2, ItemStack> entry : equipped.entrySet()) {
|
||||
@@ -436,7 +500,9 @@ public class PacketFurnitureEscape {
|
||||
if (stack.getItem() instanceof ItemLockpick) return true;
|
||||
}
|
||||
// Also check offhand
|
||||
if (player.getOffhandItem().getItem() instanceof ItemLockpick) return true;
|
||||
if (
|
||||
player.getOffhandItem().getItem() instanceof ItemLockpick
|
||||
) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -526,7 +592,9 @@ public class PacketFurnitureEscape {
|
||||
String seatId
|
||||
) {
|
||||
for (Entity passenger : furnitureEntity.getPassengers()) {
|
||||
SeatDefinition passengerSeat = provider.getSeatForPassenger(passenger);
|
||||
SeatDefinition passengerSeat = provider.getSeatForPassenger(
|
||||
passenger
|
||||
);
|
||||
if (passengerSeat != null && passengerSeat.id().equals(seatId)) {
|
||||
return passenger;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.tiedup.remake.v2.furniture.network;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.items.base.ItemCollar;
|
||||
import com.tiedup.remake.network.PacketRateLimiter;
|
||||
import com.tiedup.remake.state.IBondageState;
|
||||
import com.tiedup.remake.util.KidnappedHelper;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.v2.furniture.EntityFurniture;
|
||||
import com.tiedup.remake.v2.furniture.FurnitureDefinition;
|
||||
import com.tiedup.remake.v2.furniture.FurnitureFeedback;
|
||||
@@ -48,7 +48,10 @@ public class PacketFurnitureForcemount {
|
||||
|
||||
// ==================== Codec ====================
|
||||
|
||||
public static void encode(PacketFurnitureForcemount msg, FriendlyByteBuf buf) {
|
||||
public static void encode(
|
||||
PacketFurnitureForcemount msg,
|
||||
FriendlyByteBuf buf
|
||||
) {
|
||||
buf.writeInt(msg.furnitureEntityId);
|
||||
buf.writeUUID(msg.captiveUUID);
|
||||
}
|
||||
@@ -68,7 +71,10 @@ public class PacketFurnitureForcemount {
|
||||
ctx.setPacketHandled(true);
|
||||
}
|
||||
|
||||
private static void handleOnServer(PacketFurnitureForcemount msg, NetworkEvent.Context ctx) {
|
||||
private static void handleOnServer(
|
||||
PacketFurnitureForcemount msg,
|
||||
NetworkEvent.Context ctx
|
||||
) {
|
||||
ServerPlayer sender = ctx.getSender();
|
||||
if (sender == null) return;
|
||||
|
||||
@@ -126,8 +132,10 @@ public class PacketFurnitureForcemount {
|
||||
}
|
||||
|
||||
ItemStack collarStack = captiveState.getEquipment(BodyRegionV2.NECK);
|
||||
if (collarStack.isEmpty()
|
||||
|| !(collarStack.getItem() instanceof ItemCollar collar)) {
|
||||
if (
|
||||
collarStack.isEmpty() ||
|
||||
!(collarStack.getItem() instanceof ItemCollar collar)
|
||||
) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PacketFurnitureForcemount] Invalid collar item on captive"
|
||||
);
|
||||
@@ -174,13 +182,21 @@ public class PacketFurnitureForcemount {
|
||||
FurnitureFeedback feedback = def.feedback();
|
||||
ResourceLocation mountSoundRL = feedback.mountSound();
|
||||
if (mountSoundRL != null) {
|
||||
SoundEvent sound = SoundEvent.createVariableRangeEvent(mountSoundRL);
|
||||
entity.level().playSound(
|
||||
null,
|
||||
entity.getX(), entity.getY(), entity.getZ(),
|
||||
sound, SoundSource.BLOCKS,
|
||||
1.0f, 1.0f
|
||||
SoundEvent sound = SoundEvent.createVariableRangeEvent(
|
||||
mountSoundRL
|
||||
);
|
||||
entity
|
||||
.level()
|
||||
.playSound(
|
||||
null,
|
||||
entity.getX(),
|
||||
entity.getY(),
|
||||
entity.getZ(),
|
||||
sound,
|
||||
SoundSource.BLOCKS,
|
||||
1.0f,
|
||||
1.0f
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,15 +218,20 @@ public class PacketFurnitureForcemount {
|
||||
* Find a living entity by UUID within a reasonable range of the sender.
|
||||
* Checks players first (O(1) lookup), then falls back to entity search.
|
||||
*/
|
||||
private static LivingEntity findCaptiveByUUID(ServerPlayer sender, UUID uuid) {
|
||||
private static LivingEntity findCaptiveByUUID(
|
||||
ServerPlayer sender,
|
||||
UUID uuid
|
||||
) {
|
||||
// Try player lookup first (fast)
|
||||
net.minecraft.world.entity.player.Player player =
|
||||
sender.level().getPlayerByUUID(uuid);
|
||||
net.minecraft.world.entity.player.Player player = sender
|
||||
.level()
|
||||
.getPlayerByUUID(uuid);
|
||||
if (player != null) return player;
|
||||
|
||||
// Search nearby entities (64 block radius)
|
||||
AABB searchBox = sender.getBoundingBox().inflate(64);
|
||||
for (LivingEntity nearby : sender.level()
|
||||
for (LivingEntity nearby : sender
|
||||
.level()
|
||||
.getEntitiesOfClass(LivingEntity.class, searchBox)) {
|
||||
if (nearby.getUUID().equals(uuid)) {
|
||||
return nearby;
|
||||
@@ -224,12 +245,20 @@ public class PacketFurnitureForcemount {
|
||||
*
|
||||
* @return the seat ID, or null if all seats are occupied
|
||||
*/
|
||||
private static String findFirstAvailableSeat(ISeatProvider provider, Entity furniture) {
|
||||
private static String findFirstAvailableSeat(
|
||||
ISeatProvider provider,
|
||||
Entity furniture
|
||||
) {
|
||||
for (SeatDefinition seat : provider.getSeats()) {
|
||||
boolean occupied = false;
|
||||
for (Entity passenger : furniture.getPassengers()) {
|
||||
SeatDefinition passengerSeat = provider.getSeatForPassenger(passenger);
|
||||
if (passengerSeat != null && passengerSeat.id().equals(seat.id())) {
|
||||
SeatDefinition passengerSeat = provider.getSeatForPassenger(
|
||||
passenger
|
||||
);
|
||||
if (
|
||||
passengerSeat != null &&
|
||||
passengerSeat.id().equals(seat.id())
|
||||
) {
|
||||
occupied = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -63,7 +63,10 @@ public class PacketFurnitureLock {
|
||||
ctx.setPacketHandled(true);
|
||||
}
|
||||
|
||||
private static void handleOnServer(PacketFurnitureLock msg, NetworkEvent.Context ctx) {
|
||||
private static void handleOnServer(
|
||||
PacketFurnitureLock msg,
|
||||
NetworkEvent.Context ctx
|
||||
) {
|
||||
ServerPlayer sender = ctx.getSender();
|
||||
if (sender == null) return;
|
||||
|
||||
@@ -78,8 +81,9 @@ public class PacketFurnitureLock {
|
||||
if (!entity.isAlive() || entity.isRemoved()) return;
|
||||
|
||||
// Sender must hold a key item in either hand
|
||||
boolean hasKey = (sender.getMainHandItem().getItem() instanceof ItemMasterKey)
|
||||
|| (sender.getOffhandItem().getItem() instanceof ItemMasterKey);
|
||||
boolean hasKey =
|
||||
(sender.getMainHandItem().getItem() instanceof ItemMasterKey) ||
|
||||
(sender.getOffhandItem().getItem() instanceof ItemMasterKey);
|
||||
if (!hasKey) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PacketFurnitureLock] {} does not hold a key item in either hand",
|
||||
@@ -93,7 +97,8 @@ public class PacketFurnitureLock {
|
||||
if (seat == null) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PacketFurnitureLock] Seat '{}' not found on entity {}",
|
||||
msg.seatId, msg.entityId
|
||||
msg.seatId,
|
||||
msg.entityId
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -135,22 +140,32 @@ public class PacketFurnitureLock {
|
||||
? feedback.unlockSound()
|
||||
: feedback.lockSound();
|
||||
if (soundRL != null) {
|
||||
SoundEvent sound = SoundEvent.createVariableRangeEvent(soundRL);
|
||||
entity.level().playSound(
|
||||
null, // null = play for all nearby players
|
||||
entity.getX(), entity.getY(), entity.getZ(),
|
||||
sound, SoundSource.BLOCKS,
|
||||
1.0f, 1.0f
|
||||
SoundEvent sound = SoundEvent.createVariableRangeEvent(
|
||||
soundRL
|
||||
);
|
||||
entity
|
||||
.level()
|
||||
.playSound(
|
||||
null, // null = play for all nearby players
|
||||
entity.getX(),
|
||||
entity.getY(),
|
||||
entity.getZ(),
|
||||
sound,
|
||||
SoundSource.BLOCKS,
|
||||
1.0f,
|
||||
1.0f
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Set lock/unlock animation state. The next updateAnimState() call
|
||||
// (from tick or passenger change) will reset it to OCCUPIED/IDLE.
|
||||
boolean nowLocked = !wasLocked;
|
||||
furniture.setAnimState(nowLocked
|
||||
? EntityFurniture.STATE_LOCKING
|
||||
: EntityFurniture.STATE_UNLOCKING);
|
||||
furniture.setAnimState(
|
||||
nowLocked
|
||||
? EntityFurniture.STATE_LOCKING
|
||||
: EntityFurniture.STATE_UNLOCKING
|
||||
);
|
||||
|
||||
// Broadcast updated state to all tracking clients
|
||||
PacketSyncFurnitureState.sendToTracking(furniture);
|
||||
@@ -162,7 +177,10 @@ public class PacketFurnitureLock {
|
||||
/**
|
||||
* Find a SeatDefinition by ID from the provider's seat list.
|
||||
*/
|
||||
private static SeatDefinition findSeatById(ISeatProvider provider, String seatId) {
|
||||
private static SeatDefinition findSeatById(
|
||||
ISeatProvider provider,
|
||||
String seatId
|
||||
) {
|
||||
for (SeatDefinition seat : provider.getSeats()) {
|
||||
if (seat.id().equals(seatId)) return seat;
|
||||
}
|
||||
@@ -178,7 +196,9 @@ public class PacketFurnitureLock {
|
||||
String seatId
|
||||
) {
|
||||
for (Entity passenger : furnitureEntity.getPassengers()) {
|
||||
SeatDefinition passengerSeat = provider.getSeatForPassenger(passenger);
|
||||
SeatDefinition passengerSeat = provider.getSeatForPassenger(
|
||||
passenger
|
||||
);
|
||||
if (passengerSeat != null && passengerSeat.id().equals(seatId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,9 @@ import org.apache.logging.log4j.Logger;
|
||||
*/
|
||||
public class PacketSyncFurnitureDefinitions {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger("PacketSyncFurnitureDefinitions");
|
||||
private static final Logger LOGGER = LogManager.getLogger(
|
||||
"PacketSyncFurnitureDefinitions"
|
||||
);
|
||||
|
||||
/**
|
||||
* Safety cap on the number of definitions to prevent memory exhaustion
|
||||
@@ -65,13 +67,18 @@ public class PacketSyncFurnitureDefinitions {
|
||||
|
||||
private final Map<ResourceLocation, FurnitureDefinition> definitions;
|
||||
|
||||
public PacketSyncFurnitureDefinitions(Map<ResourceLocation, FurnitureDefinition> definitions) {
|
||||
public PacketSyncFurnitureDefinitions(
|
||||
Map<ResourceLocation, FurnitureDefinition> definitions
|
||||
) {
|
||||
this.definitions = definitions;
|
||||
}
|
||||
|
||||
// ==================== Codec ====================
|
||||
|
||||
public static void encode(PacketSyncFurnitureDefinitions msg, FriendlyByteBuf buf) {
|
||||
public static void encode(
|
||||
PacketSyncFurnitureDefinitions msg,
|
||||
FriendlyByteBuf buf
|
||||
) {
|
||||
buf.writeVarInt(msg.definitions.size());
|
||||
|
||||
for (FurnitureDefinition def : msg.definitions.values()) {
|
||||
@@ -91,7 +98,9 @@ public class PacketSyncFurnitureDefinitions {
|
||||
|
||||
// Tint channels
|
||||
buf.writeVarInt(def.tintChannels().size());
|
||||
for (Map.Entry<String, Integer> entry : def.tintChannels().entrySet()) {
|
||||
for (Map.Entry<String, Integer> entry : def
|
||||
.tintChannels()
|
||||
.entrySet()) {
|
||||
buf.writeUtf(entry.getKey());
|
||||
buf.writeInt(entry.getValue());
|
||||
}
|
||||
@@ -138,7 +147,10 @@ public class PacketSyncFurnitureDefinitions {
|
||||
buf.writeBoolean(seat.itemDifficultyBonus());
|
||||
}
|
||||
|
||||
private static void encodeFeedback(FurnitureFeedback feedback, FriendlyByteBuf buf) {
|
||||
private static void encodeFeedback(
|
||||
FurnitureFeedback feedback,
|
||||
FriendlyByteBuf buf
|
||||
) {
|
||||
writeOptionalRL(buf, feedback.mountSound());
|
||||
writeOptionalRL(buf, feedback.lockSound());
|
||||
writeOptionalRL(buf, feedback.unlockSound());
|
||||
@@ -147,7 +159,10 @@ public class PacketSyncFurnitureDefinitions {
|
||||
writeOptionalRL(buf, feedback.deniedSound());
|
||||
}
|
||||
|
||||
private static void writeOptionalRL(FriendlyByteBuf buf, ResourceLocation rl) {
|
||||
private static void writeOptionalRL(
|
||||
FriendlyByteBuf buf,
|
||||
ResourceLocation rl
|
||||
) {
|
||||
buf.writeBoolean(rl != null);
|
||||
if (rl != null) {
|
||||
buf.writeResourceLocation(rl);
|
||||
@@ -203,11 +218,23 @@ public class PacketSyncFurnitureDefinitions {
|
||||
ResourceLocation icon = readOptionalRL(buf);
|
||||
|
||||
FurnitureDefinition def = new FurnitureDefinition(
|
||||
id, displayName, translationKey, modelLocation,
|
||||
Map.copyOf(tintChannels), supportsColor,
|
||||
hitboxWidth, hitboxHeight, snapToWall, floorOnly,
|
||||
lockable, breakResistance, dropOnBreak,
|
||||
List.copyOf(seats), feedback, category, icon
|
||||
id,
|
||||
displayName,
|
||||
translationKey,
|
||||
modelLocation,
|
||||
Map.copyOf(tintChannels),
|
||||
supportsColor,
|
||||
hitboxWidth,
|
||||
hitboxHeight,
|
||||
snapToWall,
|
||||
floorOnly,
|
||||
lockable,
|
||||
breakResistance,
|
||||
dropOnBreak,
|
||||
List.copyOf(seats),
|
||||
feedback,
|
||||
category,
|
||||
icon
|
||||
);
|
||||
|
||||
defs.put(id, def);
|
||||
@@ -235,8 +262,12 @@ public class PacketSyncFurnitureDefinitions {
|
||||
boolean itemDifficultyBonus = buf.readBoolean();
|
||||
|
||||
return new SeatDefinition(
|
||||
id, armatureName, Set.copyOf(blockedRegions),
|
||||
lockable, lockedDifficulty, itemDifficultyBonus
|
||||
id,
|
||||
armatureName,
|
||||
Set.copyOf(blockedRegions),
|
||||
lockable,
|
||||
lockedDifficulty,
|
||||
itemDifficultyBonus
|
||||
);
|
||||
}
|
||||
|
||||
@@ -249,8 +280,12 @@ public class PacketSyncFurnitureDefinitions {
|
||||
ResourceLocation deniedSound = readOptionalRL(buf);
|
||||
|
||||
return new FurnitureFeedback(
|
||||
mountSound, lockSound, unlockSound,
|
||||
struggleLoopSound, escapeSound, deniedSound
|
||||
mountSound,
|
||||
lockSound,
|
||||
unlockSound,
|
||||
struggleLoopSound,
|
||||
escapeSound,
|
||||
deniedSound
|
||||
);
|
||||
}
|
||||
|
||||
@@ -276,8 +311,10 @@ public class PacketSyncFurnitureDefinitions {
|
||||
@OnlyIn(Dist.CLIENT)
|
||||
private static void handleOnClient(PacketSyncFurnitureDefinitions msg) {
|
||||
FurnitureRegistry.reload(msg.definitions);
|
||||
LOGGER.debug("Client received {} furniture definitions from server",
|
||||
msg.definitions.size());
|
||||
LOGGER.debug(
|
||||
"Client received {} furniture definitions from server",
|
||||
msg.definitions.size()
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== Server-side Helpers ====================
|
||||
|
||||
@@ -33,7 +33,11 @@ public class PacketSyncFurnitureState {
|
||||
private final byte lockBits;
|
||||
private final byte animState;
|
||||
|
||||
public PacketSyncFurnitureState(int entityId, byte lockBits, byte animState) {
|
||||
public PacketSyncFurnitureState(
|
||||
int entityId,
|
||||
byte lockBits,
|
||||
byte animState
|
||||
) {
|
||||
this.entityId = entityId;
|
||||
this.lockBits = lockBits;
|
||||
this.animState = animState;
|
||||
@@ -41,7 +45,10 @@ public class PacketSyncFurnitureState {
|
||||
|
||||
// ==================== Codec ====================
|
||||
|
||||
public static void encode(PacketSyncFurnitureState msg, FriendlyByteBuf buf) {
|
||||
public static void encode(
|
||||
PacketSyncFurnitureState msg,
|
||||
FriendlyByteBuf buf
|
||||
) {
|
||||
buf.writeInt(msg.entityId);
|
||||
buf.writeByte(msg.lockBits);
|
||||
buf.writeByte(msg.animState);
|
||||
|
||||
Reference in New Issue
Block a user