Files
TiedUp-/src/main/java/com/tiedup/remake/client/gltf/GltfClientSetup.java
NotEvil 355e2936c9 Refactor V2 animation, furniture, and GLTF rendering
Broad consolidation of the V2 bondage-item, furniture-entity, and
client-side GLTF pipeline.

Parsing and rendering
  - Shared GLB parsing helpers consolidated into GlbParserUtils
    (accessor reads, weight normalization, joint-index clamping,
    coordinate-space conversion, animation parse, primitive loop).
  - Grow-on-demand Matrix4f[] scratch pool in GltfSkinningEngine and
    GltfLiveBoneReader — removes per-frame joint-matrix allocation
    from the render hot path.
  - emitVertex helper dedups three parallel loops in GltfMeshRenderer.
  - TintColorResolver.resolve has a zero-alloc path when the item
    declares no tint channels.
  - itemAnimCache bounded to 256 entries (access-order LRU) with
    atomic get-or-compute under the map's monitor.

Animation correctness
  - First-in-joint-order wins when body and torso both map to the
    same PlayerAnimator slot; duplicate writes log a single WARN.
  - Multi-item composites honor the FullX / FullHeadX opt-in that
    the single-item path already recognized.
  - Seat transforms converted to Minecraft model-def space so
    asymmetric furniture renders passengers at the correct offset.
  - GlbValidator: IBM count / type / presence, JOINTS_0 presence,
    animation channel target validation, multi-skin support.

Furniture correctness and anti-exploit
  - Seat assignment synced via SynchedEntityData (server is
    authoritative; eliminates client-server divergence on multi-seat).
  - Force-mount authorization requires same dimension and a free
    seat; cross-dimension distance checks rejected.
  - Reconnection on login checks for seat takeover before re-mount
    and force-loads the target chunk for cross-dimension cases.
  - tiedup_furniture_lockpick_ctx carries a session UUID nonce so
    stale context can't misroute a body-item lockpick.
  - tiedup_locked_furniture survives death without keepInventory
    (Forge 1.20.1 does not auto-copy persistent data on respawn).

Lifecycle and memory
  - EntityCleanupHandler fans EntityLeaveLevelEvent out to every
    per-entity state map on the client.
  - DogPoseRenderHandler re-keyed by UUID (stable across dimension
    change; entity int ids are recycled).
  - PetBedRenderHandler, PlayerArmHideEventHandler, and
    HeldItemHideHandler use receiveCanceled + sentinel sets so
    Pre-time mutations are restored even when a downstream handler
    cancels the render.

Tests
  - JUnit harness with 76+ tests across GlbParserUtils, GltfPoseConverter,
    FurnitureSeatGeometry, and FurnitureAuthPredicate.
2026-04-18 17:34:03 +02:00

195 lines
8.5 KiB
Java

package com.tiedup.remake.client.gltf;
import com.tiedup.remake.client.animation.context.ContextAnimationFactory;
import com.tiedup.remake.client.animation.context.ContextGlbRegistry;
import com.tiedup.remake.v2.bondage.client.V2BondageRenderLayer;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemReloadListener;
import net.minecraft.client.renderer.entity.player.PlayerRenderer;
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.client.event.EntityRenderersEvent;
import net.minecraftforge.client.event.RegisterClientReloadListenersEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Forge event registration for the glTF pipeline.
* Registers render layers and animation factory.
*/
public final class GltfClientSetup {
private static final Logger LOGGER = LogManager.getLogger("GltfPipeline");
private GltfClientSetup() {}
/**
* MOD bus event subscribers (FMLClientSetupEvent, AddLayers).
*/
@Mod.EventBusSubscriber(
modid = "tiedup",
bus = Mod.EventBusSubscriber.Bus.MOD,
value = Dist.CLIENT
)
public static class ModBusEvents {
@SubscribeEvent
public static void onClientSetup(FMLClientSetupEvent event) {
event.enqueueWork(() -> {
GltfCache.init();
GltfAnimationApplier.init();
LOGGER.info("[GltfPipeline] Client setup complete");
});
}
@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)
);
LOGGER.info(
"[GltfPipeline] Render layers added to 'default' player renderer"
);
}
// Add V2 layer to slim player renderer (Alex)
var slimRenderer = event.getSkin("slim");
if (slimRenderer instanceof PlayerRenderer playerRenderer) {
playerRenderer.addLayer(
new V2BondageRenderLayer<>(playerRenderer)
);
LOGGER.info(
"[GltfPipeline] Render layers added to 'slim' player renderer"
);
}
}
/**
* Register resource reload listeners in the order required by the
* cache/consumer dependency graph.
*
* <p><b>ORDER MATTERS — do not rearrange without checking the
* invariants below.</b> Forge does not guarantee parallel-safe
* ordering between listeners registered on the same event; we rely
* on {@code apply()} running sequentially in the order of
* {@code registerReloadListener} calls.</p>
*
* <ol>
* <li><b>GLB cache clear</b> (inline listener below) — must run
* first. Inside this single listener's {@code apply()}:
* <ol type="a">
* <li>Blow away the raw GLB byte caches
* ({@code GltfCache.clearCache},
* {@code GltfAnimationApplier.invalidateCache},
* {@code GltfMeshRenderer.clearRenderTypeCache}).
* These three caches are mutually independent — none
* of them reads from the others — so their relative
* order is not load-bearing.</li>
* <li>Reload {@code ContextGlbRegistry} from the new
* resource packs <i>before</i> clearing
* {@code ContextAnimationFactory.clearCache()} — if
* the order is swapped, the next factory lookup will
* lazily rebuild clips against the stale registry
* (which is still populated at that moment), cache
* them, and keep serving old data until the next
* reload.</li>
* <li>Clear {@code FurnitureGltfCache} last, after the GLB
* layer has repopulated its registry but before any
* downstream item listener queries furniture models.</li>
* </ol>
* </li>
* <li><b>Data-driven item reload</b>
* ({@code DataDrivenItemReloadListener}) — consumes the
* reloaded GLB registry indirectly via item JSON references.
* Must run <i>after</i> the GLB cache clear so any item that
* reaches into the GLB layer during load picks up fresh data.</li>
* <li><b>GLB validation</b>
* ({@code GlbValidationReloadListener}) — runs last. It walks
* both the item registry and the GLB cache to surface
* authoring issues via toast. If it ran earlier, missing
* items would falsely trip the "referenced but not found"
* diagnostic.</li>
* </ol>
*
* <p>When adding a new listener: decide where it sits in this
* producer/consumer chain. If you're not sure, add it at the end
* (the safest position — the rest of the graph is already built).</p>
*/
@SubscribeEvent
public static void onRegisterReloadListeners(
RegisterClientReloadListenersEvent event
) {
event.registerReloadListener(
new SimplePreparableReloadListener<Void>() {
@Override
protected Void prepare(
ResourceManager resourceManager,
ProfilerFiller profiler
) {
return null;
}
@Override
protected void apply(
Void nothing,
ResourceManager resourceManager,
ProfilerFiller profiler
) {
GltfCache.clearCache();
GltfAnimationApplier.invalidateCache();
GltfMeshRenderer.clearRenderTypeCache();
// Reload context GLB animations from resource packs FIRST,
// then clear the factory cache so it rebuilds against the
// new GLB registry (prevents stale JSON fallback caching).
ContextGlbRegistry.reload(resourceManager);
ContextAnimationFactory.clearCache();
com.tiedup.remake.v2.furniture.client.FurnitureGltfCache.clear();
LOGGER.info(
"[GltfPipeline] Caches cleared on resource reload"
);
}
}
);
LOGGER.info("[GltfPipeline] Resource reload listener registered");
// Data-driven bondage item definitions (tiedup_items/*.json)
event.registerReloadListener(new DataDrivenItemReloadListener());
LOGGER.info(
"[GltfPipeline] Data-driven item reload listener registered"
);
// GLB structural validation (runs after item definitions are loaded)
event.registerReloadListener(new com.tiedup.remake.client.gltf.diagnostic.GlbValidationReloadListener());
LOGGER.info("[GltfPipeline] GLB validation reload listener registered");
}
}
/** FORGE bus event subscribers (client-side commands). */
@Mod.EventBusSubscriber(
modid = "tiedup",
bus = Mod.EventBusSubscriber.Bus.FORGE,
value = Dist.CLIENT
)
public static class ForgeBusEvents {
@SubscribeEvent
public static void onRegisterClientCommands(
net.minecraftforge.client.event.RegisterClientCommandsEvent event
) {
com.tiedup.remake.commands.ValidateGlbCommand.register(
event.getDispatcher()
);
LOGGER.info(
"[GltfPipeline] Client command /tiedup validate registered"
);
}
}
}