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

@@ -6,6 +6,7 @@ import com.tiedup.remake.items.base.ILockable;
import com.tiedup.remake.minigame.LockpickMiniGameState;
import com.tiedup.remake.minigame.LockpickMiniGameState.PickAttemptResult;
import com.tiedup.remake.minigame.LockpickSessionManager;
import com.tiedup.remake.minigame.LockpickTargetKind;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.PacketRateLimiter;
import com.tiedup.remake.network.sync.SyncManager;
@@ -115,41 +116,40 @@ public class PacketLockpickAttempt {
ServerPlayer player,
LockpickMiniGameState session
) {
// Furniture seat lockpick path: presence of furniture_id AND a
// session_id matching the current session. A ctx without the nonce
// (or with a foreign nonce) is rejected — this is the branch a
// stale-ctx bug could otherwise mis-route into.
CompoundTag furnitureCtx = player
.getPersistentData()
.getCompound("tiedup_furniture_lockpick_ctx");
boolean ctxValid =
furnitureCtx != null
&& furnitureCtx.contains("furniture_id")
&& furnitureCtx.hasUUID("session_id")
&& furnitureCtx.getUUID("session_id").equals(session.getSessionId());
if (ctxValid) {
// Distance check BEFORE endLockpickSession — consuming a
// session without applying the reward (player walked away)
// would burn the session with no visible effect.
int furnitureId = furnitureCtx.getInt("furniture_id");
Entity furnitureEntity = player.level().getEntity(furnitureId);
if (session.getTargetKind() == LockpickTargetKind.FURNITURE_SEAT) {
// Validate EVERY input before any side effect. Consuming the
// session and damaging the lockpick before verifying that the
// unlock will succeed would show "Lock picked!" to the player
// while nothing actually unlocks.
Entity furnitureEntity = player.level().getEntity(session.getTargetData());
if (
furnitureEntity == null ||
player.distanceTo(furnitureEntity) > 10.0
) {
return;
}
CompoundTag furnitureCtx = player
.getPersistentData()
.getCompound("tiedup_furniture_lockpick_ctx");
String seatId = furnitureCtx.contains("seat_id", net.minecraft.nbt.Tag.TAG_STRING)
? furnitureCtx.getString("seat_id")
: "";
if (seatId.isEmpty()) {
TiedUpMod.LOGGER.warn(
"[PacketLockpickAttempt] Furniture lockpick ctx missing seat_id for {} — aborting without consuming session",
player.getName().getString()
);
return;
}
// Session validated — now end it
LockpickSessionManager.getInstance().endLockpickSession(
player.getUUID(),
true
);
handleFurnitureLockpickSuccess(player, furnitureCtx);
handleFurnitureLockpickSuccess(player, furnitureEntity, seatId);
player.getPersistentData().remove("tiedup_furniture_lockpick_ctx");
damageLockpick(player);
// Send result to client
ModNetwork.sendToPlayer(
new PacketLockpickMiniGameResult(
session.getSessionId(),
@@ -167,9 +167,8 @@ public class PacketLockpickAttempt {
true
);
// Body item lockpick path: targetSlot stores BodyRegionV2 ordinal
BodyRegionV2 targetRegion =
BodyRegionV2.values()[session.getTargetSlot()];
BodyRegionV2.values()[session.getTargetData()];
ItemStack targetStack = V2EquipmentHelper.getInRegion(
player,
targetRegion
@@ -212,24 +211,21 @@ public class PacketLockpickAttempt {
/**
* Handle a successful furniture seat lockpick: unlock the seat, dismount
* the passenger, play the unlock sound, and broadcast the updated state.
* Caller is responsible for validating inputs before firing side effects.
*/
private void handleFurnitureLockpickSuccess(
ServerPlayer player,
CompoundTag ctx
Entity furnitureEntity,
String seatId
) {
int furnitureEntityId = ctx.getInt("furniture_id");
String seatId = ctx.getString("seat_id");
Entity entity = player.level().getEntity(furnitureEntityId);
if (!(entity instanceof EntityFurniture furniture)) {
if (!(furnitureEntity instanceof EntityFurniture furniture)) {
TiedUpMod.LOGGER.warn(
"[PacketLockpickAttempt] Furniture entity {} not found or wrong type for lockpick success",
furnitureEntityId
"[PacketLockpickAttempt] Lockpick target {} is not an EntityFurniture",
furnitureEntity.getId()
);
return;
}
// Unlock the seat
furniture.setSeatLocked(seatId, false);
// Dismount the passenger in that seat
@@ -244,16 +240,15 @@ public class PacketLockpickAttempt {
passenger.stopRiding();
}
// Play unlock sound from the furniture definition
FurnitureDefinition def = furniture.getDefinition();
if (def != null && def.feedback().unlockSound() != null) {
player
.level()
.playSound(
null,
entity.getX(),
entity.getY(),
entity.getZ(),
furniture.getX(),
furniture.getY(),
furniture.getZ(),
SoundEvent.createVariableRangeEvent(
def.feedback().unlockSound()
),
@@ -263,13 +258,12 @@ public class PacketLockpickAttempt {
);
}
// Broadcast updated lock/anim state to all tracking clients
PacketSyncFurnitureState.sendToTracking(furniture);
TiedUpMod.LOGGER.info(
"[PacketLockpickAttempt] Player {} picked furniture lock on entity {} seat '{}'",
player.getName().getString(),
furnitureEntityId,
furniture.getId(),
seatId
);
}
@@ -330,18 +324,13 @@ public class PacketLockpickAttempt {
// Jam mechanic (5%) only applies to body-item sessions — seat locks
// have no ILockable stack to jam.
boolean jammed = false;
CompoundTag sessionCtx = player
.getPersistentData()
.getCompound("tiedup_furniture_lockpick_ctx");
boolean isFurnitureSession =
sessionCtx.contains("furniture_id")
&& sessionCtx.hasUUID("session_id")
&& sessionCtx.getUUID("session_id").equals(session.getSessionId());
boolean isBodySession =
session.getTargetKind() == LockpickTargetKind.BODY_REGION;
if (!isFurnitureSession && player.getRandom().nextFloat() < 0.05f) {
int targetSlot = session.getTargetSlot();
if (targetSlot >= 0 && targetSlot < BodyRegionV2.values().length) {
BodyRegionV2 targetRegion = BodyRegionV2.values()[targetSlot];
if (isBodySession && player.getRandom().nextFloat() < 0.05f) {
int targetOrdinal = session.getTargetData();
if (targetOrdinal >= 0 && targetOrdinal < BodyRegionV2.values().length) {
BodyRegionV2 targetRegion = BodyRegionV2.values()[targetOrdinal];
ItemStack targetStack = V2EquipmentHelper.getInRegion(
player,
targetRegion

View File

@@ -122,6 +122,7 @@ public class PacketLockpickMiniGameStart {
LockpickSessionManager manager = LockpickSessionManager.getInstance();
LockpickMiniGameState session = manager.startLockpickSession(
player,
com.tiedup.remake.minigame.LockpickTargetKind.BODY_REGION,
targetRegion.ordinal(),
sweetSpotWidth
);