Polish V2 subsystem: lockpick kinds, package boundaries, client extractions

Architectural debt cleanup on top of the earlier V2 hardening pass.

Minigame:
  - LockpickMiniGameState splits the overloaded targetSlot int into a
    LockpickTargetKind enum + targetData int. Body-vs-furniture
    dispatch is now a simple enum check; the NBT-tag nonce it
    previously depended on is gone, along with the AIOOBE risk at
    BodyRegionV2.values()[targetSlot].
  - PacketLockpickAttempt.handleFurnitureLockpickSuccess takes the
    entity and seat id as explicit parameters. Caller pre-validates
    both before any side effect, so a corrupted ctx tag can no longer
    produce a "Lock picked!" UI with a used lockpick and nothing
    unlocked.

Package boundaries:
  - client.gltf no longer imports v2.bondage. Render-layer attachment,
    DataDrivenItemReloadListener, and GlbValidationReloadListener all
    live in v2.client.V2ClientSetup.
  - GlbValidationReloadListener moved to v2.bondage.client.diagnostic.
  - Reload-listener ordering is preserved via EventPriority (HIGH for
    the generic GLB cache clear in GltfClientSetup, LOW for bondage
    consumers in V2ClientSetup).
  - Removed the unused validateAgainstDefinition stub on GlbValidator.

Extractions from EntityFurniture:
  - FurnitureSeatSyncCodec (pipe/semicolon serialization for the
    SEAT_ASSIGNMENTS_SYNC entity data field), with 8 unit tests.
  - FurnitureClientAnimator (client-only seat-pose kickoff, moved out
    of the dual-side entity class).
  - EntityFurniture drops ~100 lines with no behavior change.

Interface docs:
  - ISeatProvider Javadoc narrowed to reflect that EntityFurniture is
    the only implementation; callers that need animation state or
    definition reference still downcast.
  - FurnitureAuthPredicate.findOccupant uses the interface only.
  - AnimationIdBuilder flagged as legacy JSON-era utility (NPC
    fallback + MCA mixin).

Artist guide: corrected the "Monster Seat System (Planned)" section
to match the ISeatProvider single-impl reality.
This commit is contained in:
NotEvil
2026-04-19 02:06:02 +02:00
parent d391b892aa
commit cc6a62a6e5
19 changed files with 442 additions and 362 deletions

View File

@@ -0,0 +1,160 @@
package com.tiedup.remake.v2.bondage.client.diagnostic;
import com.tiedup.remake.client.gltf.diagnostic.GlbDiagnostic;
import com.tiedup.remake.client.gltf.diagnostic.GlbDiagnosticRegistry;
import com.tiedup.remake.client.gltf.diagnostic.GlbValidationResult;
import com.tiedup.remake.client.gltf.diagnostic.GlbValidator;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry;
import java.io.InputStream;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.toasts.SystemToast;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.Resource;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.server.packs.resources.SimplePreparableReloadListener;
import net.minecraft.util.profiling.ProfilerFiller;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Reload listener that validates all data-driven item GLB models on resource
* reload (F3+T or startup).
*
* <p>Must be registered AFTER {@link com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemReloadListener}
* so that the item registry is populated before validation runs.</p>
*
* <p>On each reload:
* <ol>
* <li>Clears the {@link GlbDiagnosticRegistry}</li>
* <li>Iterates all {@link DataDrivenItemDefinition}s</li>
* <li>For each definition with a model location, attempts to open the GLB
* resource and runs {@link GlbValidator#validate} on it</li>
* <li>Records results into the diagnostic registry and logs a summary</li>
* </ol>
*/
@OnlyIn(Dist.CLIENT)
public class GlbValidationReloadListener
extends SimplePreparableReloadListener<Void>
{
private static final Logger LOGGER = LogManager.getLogger("GltfValidation");
@Override
protected Void prepare(
ResourceManager resourceManager,
ProfilerFiller profiler
) {
return null;
}
@Override
protected void apply(
Void nothing,
ResourceManager resourceManager,
ProfilerFiller profiler
) {
GlbDiagnosticRegistry.clear();
Collection<DataDrivenItemDefinition> definitions =
DataDrivenItemRegistry.getAll();
if (definitions.isEmpty()) {
LOGGER.warn(
"[GltfValidation] No data-driven item definitions found — skipping GLB validation"
);
return;
}
int passed = 0;
int withWarnings = 0;
int withErrors = 0;
for (DataDrivenItemDefinition def : definitions) {
ResourceLocation modelLoc = def.modelLocation();
if (modelLoc == null) {
continue;
}
Optional<Resource> resourceOpt =
resourceManager.getResource(modelLoc);
if (resourceOpt.isEmpty()) {
// GLB file not found in any resource pack
GlbValidationResult missingResult = GlbValidationResult.of(
modelLoc,
List.of(new GlbDiagnostic(
modelLoc,
def.id(),
GlbDiagnostic.Severity.ERROR,
"MISSING_GLB",
"GLB file not found: " + modelLoc
+ " (referenced by item " + def.id() + ")"
))
);
GlbDiagnosticRegistry.addResult(missingResult);
withErrors++;
continue;
}
try (InputStream stream = resourceOpt.get().open()) {
GlbValidationResult result =
GlbValidator.validate(stream, modelLoc);
GlbDiagnosticRegistry.addResult(result);
if (!result.passed()) {
withErrors++;
} else if (hasWarnings(result)) {
withWarnings++;
} else {
passed++;
}
} catch (Exception e) {
GlbValidationResult errorResult = GlbValidationResult.of(
modelLoc,
List.of(new GlbDiagnostic(
modelLoc,
def.id(),
GlbDiagnostic.Severity.ERROR,
"GLB_READ_ERROR",
"Failed to read GLB file: " + e.getMessage()
))
);
GlbDiagnosticRegistry.addResult(errorResult);
withErrors++;
}
}
int total = passed + withWarnings + withErrors;
LOGGER.info(
"[GltfValidation] Validated {} GLBs: {} passed, {} with warnings, {} with errors",
total, passed, withWarnings, withErrors
);
// Show toast notification for errors so artists don't have to check logs
if (withErrors > 0) {
int errorCount = withErrors;
Minecraft.getInstance().tell(() ->
Minecraft.getInstance().getToasts().addToast(
SystemToast.multiline(
Minecraft.getInstance(),
SystemToast.SystemToastIds.PACK_LOAD_FAILURE,
Component.literal("TiedUp! GLB Validation"),
Component.literal(errorCount + " model(s) have errors. Run /tiedup validate")
)
)
);
}
}
private static boolean hasWarnings(GlbValidationResult result) {
return result.diagnostics().stream()
.anyMatch(d -> d.severity() == GlbDiagnostic.Severity.WARNING);
}
}

View File

@@ -5,19 +5,25 @@ import com.tiedup.remake.client.model.CellCoreBakedModel;
import com.tiedup.remake.client.renderer.CellCoreRenderer;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.v2.V2BlockEntities;
import com.tiedup.remake.v2.bondage.client.V2BondageRenderLayer;
import com.tiedup.remake.v2.bondage.client.diagnostic.GlbValidationReloadListener;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemReloadListener;
import com.tiedup.remake.v2.furniture.FurnitureDefinition;
import com.tiedup.remake.v2.furniture.FurnitureRegistry;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import net.minecraft.client.renderer.entity.player.PlayerRenderer;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.EntityRenderersEvent;
import net.minecraftforge.client.event.ModelEvent;
import net.minecraftforge.client.event.RegisterClientReloadListenersEvent;
import net.minecraftforge.eventbus.api.EventPriority;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.registries.ForgeRegistries;
@@ -72,6 +78,49 @@ public class V2ClientSetup {
);
}
/**
* Attach the V2 bondage item render layer to both player model variants.
* The layer queries the equipment capability on every player render, so
* registering it once covers all players (local + remote).
*/
@SuppressWarnings("unchecked")
@SubscribeEvent
public static void onAddLayers(EntityRenderersEvent.AddLayers event) {
var defaultRenderer = event.getSkin("default");
if (defaultRenderer instanceof PlayerRenderer playerRenderer) {
playerRenderer.addLayer(
new V2BondageRenderLayer<>(playerRenderer)
);
}
var slimRenderer = event.getSkin("slim");
if (slimRenderer instanceof PlayerRenderer playerRenderer) {
playerRenderer.addLayer(
new V2BondageRenderLayer<>(playerRenderer)
);
}
TiedUpMod.LOGGER.info(
"[V2ClientSetup] V2 bondage render layer attached to player renderers"
);
}
/**
* Register bondage-specific reload listeners. Uses {@code LOW} priority so
* it fires after {@link com.tiedup.remake.client.gltf.GltfClientSetup}
* registers the generic GLB cache clear — item definitions must not load
* against a stale GLB cache, and the validator must run against a warm
* item registry.
*/
@SubscribeEvent(priority = EventPriority.LOW)
public static void onRegisterReloadListeners(
RegisterClientReloadListenersEvent event
) {
event.registerReloadListener(new DataDrivenItemReloadListener());
event.registerReloadListener(new GlbValidationReloadListener());
TiedUpMod.LOGGER.info(
"[V2ClientSetup] Data-driven item + GLB validation reload listeners registered"
);
}
/**
* Register custom icon models for baking.
*

View File

@@ -232,42 +232,19 @@ public class EntityFurniture
syncSeatAssignmentsIfServer();
}
/**
* Serialize {@link #seatAssignments} into {@link #SEAT_ASSIGNMENTS_SYNC}
* so tracking clients see the authoritative mapping. No-op on client.
*/
/** Push seatAssignments into SEAT_ASSIGNMENTS_SYNC. No-op on client. */
private void syncSeatAssignmentsIfServer() {
if (this.level().isClientSide) return;
StringBuilder sb = new StringBuilder(seatAssignments.size() * 40);
boolean first = true;
for (Map.Entry<UUID, String> entry : seatAssignments.entrySet()) {
if (!first) sb.append('|');
sb.append(entry.getKey()).append(';').append(entry.getValue());
first = false;
}
this.entityData.set(SEAT_ASSIGNMENTS_SYNC, sb.toString());
this.entityData.set(
SEAT_ASSIGNMENTS_SYNC,
FurnitureSeatSyncCodec.encode(seatAssignments)
);
}
/**
* Parse {@link #SEAT_ASSIGNMENTS_SYNC} back into {@link #seatAssignments}.
* Called on the client when the server's broadcast arrives. Malformed
* entries (bad UUID, empty seat id) are skipped silently; we don't want
* to throw on a packet from a future protocol version.
*/
/** Rebuild seatAssignments from the server's broadcast. */
private void applySyncedSeatAssignments(String serialized) {
seatAssignments.clear();
if (serialized.isEmpty()) return;
for (String entry : serialized.split("\\|")) {
int sep = entry.indexOf(';');
if (sep <= 0 || sep == entry.length() - 1) continue;
try {
UUID uuid = UUID.fromString(entry.substring(0, sep));
String seatId = entry.substring(sep + 1);
seatAssignments.put(uuid, seatId);
} catch (IllegalArgumentException ignored) {
// Corrupt UUID — skip this entry, preserve the rest.
}
}
seatAssignments.putAll(FurnitureSeatSyncCodec.decode(serialized));
}
@Override
@@ -474,98 +451,18 @@ public class EntityFurniture
this.transitionTargetState = STATE_OCCUPIED;
}
// Note: the previous client-side startFurnitureAnimationClient call is
// no longer needed here — the animation is kicked off by
// onSyncedDataUpdated when SEAT_ASSIGNMENTS_SYNC arrives (~1 tick
// after mount). The cold-cache retry in AnimationTickHandler still
// covers the case where the GLB hasn't parsed yet.
// Seat-pose animation is kicked off from onSyncedDataUpdated once
// SEAT_ASSIGNMENTS_SYNC arrives (~1 tick after mount). The cold-
// cache retry in AnimationTickHandler covers the case where the
// GLB hasn't parsed yet.
}
/**
* Client-only: (re)start the seat pose animation for a player mounting this
* furniture. Safe to call repeatedly — {@link BondageAnimationManager#playFurniture}
* replaces any active animation. No-op if the GLB isn't loaded (cold cache),
* the seat has no authored clip, or the animation context rejects the setup.
*
* <p>Called from three sites:</p>
* <ul>
* <li>{@link #addPassenger} on mount</li>
* <li>{@link #onSyncedDataUpdated} when server-side {@code ANIM_STATE} changes</li>
* <li>The per-tick retry in {@code AnimationTickHandler} (for cold-cache recovery)</li>
* </ul>
*
* <p>Clip selection mirrors {@link com.tiedup.remake.v2.furniture.client.FurnitureEntityRenderer#resolveActiveAnimation}
* for mesh/pose coherence. Fallback chain: state-specific clip → {@code "Occupied"}
* → first available clip. This lets artists author optional state-specific poses
* ({@code Entering}, {@code Exiting}, {@code Shake}) without requiring all of them.</p>
*/
public static void startFurnitureAnimationClient(
EntityFurniture furniture,
Player player
) {
if (!furniture.level().isClientSide) return;
SeatDefinition seat = furniture.getSeatForPassenger(player);
if (seat == null) return;
FurnitureDefinition def = furniture.getDefinition();
if (def == null) return;
com.tiedup.remake.v2.furniture.client.FurnitureGltfData gltfData =
com.tiedup.remake.v2.furniture.client.FurnitureGltfCache.get(
def.modelLocation()
);
if (gltfData == null) return;
Map<
String,
com.tiedup.remake.client.gltf.GltfData.AnimationClip
> seatClips = gltfData.seatAnimations().get(seat.id());
if (seatClips == null || seatClips.isEmpty()) return;
com.tiedup.remake.client.gltf.GltfData seatSkeleton =
gltfData.seatSkeletons().get(seat.id());
// State-driven clip selection for the player seat armature. Names match
// the ARTIST_GUIDE.md "Player Seat Animations" section so artists can
// author matching clips. The fallback chain handles missing clips
// (state-specific → "Occupied" → first available), so artists only need
// to author what they want to customize.
String stateClipName = switch (furniture.getAnimState()) {
case STATE_OCCUPIED -> "Occupied";
case STATE_STRUGGLE -> "Struggle";
case STATE_ENTERING -> "Enter";
case STATE_EXITING -> "Exit";
case STATE_LOCKING -> "LockClose";
case STATE_UNLOCKING -> "LockOpen";
default -> "Idle";
};
com.tiedup.remake.client.gltf.GltfData.AnimationClip clip =
seatClips.get(stateClipName);
if (clip == null) clip = seatClips.get("Occupied");
if (clip == null) clip = seatClips.values().iterator().next();
if (clip == null) return;
dev.kosmx.playerAnim.core.data.KeyframeAnimation anim =
com.tiedup.remake.v2.furniture.client.FurnitureAnimationContext.create(
clip,
seatSkeleton,
seat.blockedRegions()
);
if (anim != null) {
com.tiedup.remake.client.animation.BondageAnimationManager.playFurniture(
player,
anim
);
}
}
/**
* Client-side: when the synched {@code ANIM_STATE} changes, re-play the seat
* pose for each seated player so the authored state-specific clip kicks in.
* Without this, a server-side transition (mount entering → occupied, lock
* close, struggle start) never propagates to the player's pose.
* Client-side: when the synched {@code ANIM_STATE} or seat assignments
* change, re-play the seat pose for each seated player so the authored
* state-specific clip kicks in. Without this, a server-side transition
* (mount entering → occupied, lock close, struggle start) never
* propagates to the player's pose.
*/
@Override
public void onSyncedDataUpdated(
@@ -574,22 +471,21 @@ public class EntityFurniture
super.onSyncedDataUpdated(key);
if (!this.level().isClientSide) return;
if (ANIM_STATE.equals(key)) {
for (Entity passenger : this.getPassengers()) {
if (passenger instanceof Player player) {
startFurnitureAnimationClient(this, player);
}
}
restartAllSeatAnimations();
} else if (SEAT_ASSIGNMENTS_SYNC.equals(key)) {
applySyncedSeatAssignments(
this.entityData.get(SEAT_ASSIGNMENTS_SYNC)
);
// Re-play animations for passengers whose seat id just changed.
// Without this the client could keep rendering a passenger with
// the previous seat's blockedRegions until some other trigger.
for (Entity passenger : this.getPassengers()) {
if (passenger instanceof Player player) {
startFurnitureAnimationClient(this, player);
}
restartAllSeatAnimations();
}
}
/** Client-only — caller guards via level().isClientSide. */
private void restartAllSeatAnimations() {
for (Entity passenger : this.getPassengers()) {
if (passenger instanceof Player player) {
com.tiedup.remake.v2.furniture.client.FurnitureClientAnimator
.start(this, player);
}
}
}

View File

@@ -242,18 +242,9 @@ public final class FurnitureAuthPredicate {
*/
@Nullable
private static LivingEntity findOccupant(Entity furniture, String seatId) {
if (!(furniture instanceof EntityFurniture ef)) return null;
for (Entity passenger : ef.getPassengers()) {
SeatDefinition assigned = ef.getSeatForPassenger(passenger);
if (
assigned != null &&
assigned.id().equals(seatId) &&
passenger instanceof LivingEntity living
) {
return living;
}
}
return null;
if (!(furniture instanceof ISeatProvider provider)) return null;
Entity passenger = provider.findPassengerInSeat(seatId);
return passenger instanceof LivingEntity living ? living : null;
}
/**

View File

@@ -0,0 +1,54 @@
package com.tiedup.remake.v2.furniture;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
/**
* Wire-format codec for the {@code seatAssignments} map synced to clients
* via the SEAT_ASSIGNMENTS_SYNC entity data accessor.
*
* <p>Format: {@code uuid;seatId|uuid;seatId|…} (empty string = no
* assignments). Seat ids cannot contain {@code |} or {@code ;} — enforced
* at parse time in {@link FurnitureParser}.</p>
*/
public final class FurnitureSeatSyncCodec {
private FurnitureSeatSyncCodec() {}
/** Serialize an assignments map to its wire form. */
public static String encode(Map<UUID, String> assignments) {
if (assignments.isEmpty()) return "";
StringBuilder sb = new StringBuilder(assignments.size() * 40);
boolean first = true;
for (Map.Entry<UUID, String> entry : assignments.entrySet()) {
if (!first) sb.append('|');
sb.append(entry.getKey()).append(';').append(entry.getValue());
first = false;
}
return sb.toString();
}
/**
* Parse a wire-form string back into an assignments map. Malformed
* entries (invalid UUID, missing separator, empty seat id) are skipped
* silently so a packet from a future protocol version can still be
* consumed at worst-effort without throwing.
*/
public static Map<UUID, String> decode(String serialized) {
Map<UUID, String> result = new LinkedHashMap<>();
if (serialized == null || serialized.isEmpty()) return result;
for (String entry : serialized.split("\\|")) {
int sep = entry.indexOf(';');
if (sep <= 0 || sep == entry.length() - 1) continue;
try {
UUID uuid = UUID.fromString(entry.substring(0, sep));
String seatId = entry.substring(sep + 1);
result.put(uuid, seatId);
} catch (IllegalArgumentException ignored) {
// Corrupt UUID — skip this entry, preserve the rest.
}
}
return result;
}
}

View File

@@ -7,11 +7,15 @@ import net.minecraft.world.entity.Entity;
import org.jetbrains.annotations.Nullable;
/**
* Universal interface for entities that hold players in constrained poses.
* Contract for entities that hold passengers in named seats.
*
* <p>Implemented by EntityFurniture (static) and optionally by monsters/NPCs.
* All downstream systems (packets, animation, rendering) check ISeatProvider,
* never EntityFurniture directly.</p>
* <p>{@link EntityFurniture} is currently the only implementation. The
* interface covers the seat-lookup / lock / region surface but NOT
* animation state or definition reference — callers that need those
* still downcast to {@code EntityFurniture}. Adding a second
* implementation (NPC carrying a captive, a block acting as a chair,
* …) would require extending this interface first; the existing
* consumers aren't polymorphic over those methods today.</p>
*/
public interface ISeatProvider {
/** All seat definitions for this entity. */

View File

@@ -0,0 +1,83 @@
package com.tiedup.remake.v2.furniture.client;
import com.tiedup.remake.client.animation.BondageAnimationManager;
import com.tiedup.remake.client.gltf.GltfData;
import com.tiedup.remake.v2.furniture.EntityFurniture;
import com.tiedup.remake.v2.furniture.FurnitureDefinition;
import com.tiedup.remake.v2.furniture.SeatDefinition;
import dev.kosmx.playerAnim.core.data.KeyframeAnimation;
import java.util.Map;
import net.minecraft.world.entity.player.Player;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
/**
* Client-only: start the seat-pose animation for a player on a furniture
* entity. Selects the authored clip for the current animation state, falls
* back through {@code "Occupied"} → first available, and dispatches the
* result to {@link BondageAnimationManager#playFurniture}.
*
* <p>Safe to call repeatedly — {@code playFurniture} replaces any active
* animation. No-op if the GLB isn't loaded yet (cold cache) or the seat
* has no authored clip.</p>
*/
@OnlyIn(Dist.CLIENT)
public final class FurnitureClientAnimator {
private FurnitureClientAnimator() {}
/**
* Kick off (or restart) the seat-pose animation. Callers:
* <ul>
* <li>{@code EntityFurniture.addPassenger} — on mount</li>
* <li>{@code EntityFurniture.onSyncedDataUpdated} — on
* {@code ANIM_STATE} / {@code SEAT_ASSIGNMENTS_SYNC} change</li>
* <li>{@code AnimationTickHandler} cold-cache retry</li>
* </ul>
*
* <p>Clip selection matches {@code FurnitureEntityRenderer.resolveActiveAnimation}
* to keep mesh and pose in lockstep.</p>
*/
public static void start(EntityFurniture furniture, Player player) {
if (!furniture.level().isClientSide) return;
SeatDefinition seat = furniture.getSeatForPassenger(player);
if (seat == null) return;
FurnitureDefinition def = furniture.getDefinition();
if (def == null) return;
FurnitureGltfData gltfData = FurnitureGltfCache.get(def.modelLocation());
if (gltfData == null) return;
Map<String, GltfData.AnimationClip> seatClips =
gltfData.seatAnimations().get(seat.id());
if (seatClips == null || seatClips.isEmpty()) return;
GltfData seatSkeleton = gltfData.seatSkeletons().get(seat.id());
String stateClipName = switch (furniture.getAnimState()) {
case EntityFurniture.STATE_OCCUPIED -> "Occupied";
case EntityFurniture.STATE_STRUGGLE -> "Struggle";
case EntityFurniture.STATE_ENTERING -> "Enter";
case EntityFurniture.STATE_EXITING -> "Exit";
case EntityFurniture.STATE_LOCKING -> "LockClose";
case EntityFurniture.STATE_UNLOCKING -> "LockOpen";
default -> "Idle";
};
GltfData.AnimationClip clip = seatClips.get(stateClipName);
if (clip == null) clip = seatClips.get("Occupied");
if (clip == null) clip = seatClips.values().iterator().next();
if (clip == null) return;
KeyframeAnimation anim = FurnitureAnimationContext.create(
clip,
seatSkeleton,
seat.blockedRegions()
);
if (anim != null) {
BondageAnimationManager.playFurniture(player, anim);
}
}
}

View File

@@ -363,18 +363,14 @@ public class PacketFurnitureEscape {
0.15f - (totalDifficulty / (float) MAX_DIFFICULTY) * 0.12f
);
// Start lockpick session via LockpickSessionManager.
// The existing lockpick session uses a targetSlot (BodyRegionV2 ordinal) for
// bondage items. For furniture, we repurpose targetSlot as the furniture entity ID
// and store the seat ID in a context tag so the completion callback can find it.
// 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.
// Start a FURNITURE_SEAT-kind session; PacketLockpickAttempt branches
// on the kind to run the furniture-specific completion.
LockpickSessionManager lockpickManager =
LockpickSessionManager.getInstance();
LockpickMiniGameState session = lockpickManager.startLockpickSession(
sender,
furnitureEntity.getId(), // repurpose targetSlot as entity ID
com.tiedup.remake.minigame.LockpickTargetKind.FURNITURE_SEAT,
furnitureEntity.getId(),
sweetSpotWidth
);
@@ -388,16 +384,12 @@ public class PacketFurnitureEscape {
session.setRemainingUses(remainingUses);
// Store furniture context in the sender's persistent data so the
// lockpick attempt handler can resolve the furniture on success.
// This is cleaned up when the session ends.
// Store seat_id in persistent data so the completion callback can
// find it (the session carries furniture id via targetData, but
// seat id has nowhere else to live yet).
net.minecraft.nbt.CompoundTag ctx = new net.minecraft.nbt.CompoundTag();
ctx.putInt("furniture_id", furnitureEntity.getId());
ctx.putString("seat_id", targetSeat.id());
// Nonce: the handler accepts this ctx only when session_id matches
// the active session. Prevents stale ctx from mis-routing a later
// body-item lockpick.
ctx.putUUID("session_id", session.getSessionId());
sender.getPersistentData().put("tiedup_furniture_lockpick_ctx", ctx);
// Send initial lockpick state to open the minigame GUI on the client