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.
195 lines
8.5 KiB
Java
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"
|
|
);
|
|
}
|
|
}
|
|
}
|