diff --git a/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemRegistry.java b/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemRegistry.java index 070ef7f..e0e25ec 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemRegistry.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemRegistry.java @@ -18,9 +18,9 @@ import org.jetbrains.annotations.Nullable; * Thread-safe registry for data-driven bondage item definitions. * *
Populated by the reload listener that scans {@code tiedup_items/} JSON files. - * Uses volatile atomic swap (same pattern as {@link - * com.tiedup.remake.client.animation.context.ContextGlbRegistry}) to ensure - * the render thread always sees a consistent snapshot.
+ * Uses a single volatile snapshot reference to ensure readers always see a + * consistent pair of definitions + component holders. This prevents torn reads + * where one map is updated but the other is stale. * *Lookup methods accept either a {@link ResourceLocation} ID directly * or an {@link ItemStack} (reads the {@code tiedup_item_id} NBT tag).
@@ -31,20 +31,22 @@ public final class DataDrivenItemRegistry { public static final String NBT_ITEM_ID = "tiedup_item_id"; /** - * Volatile reference to an unmodifiable map. Reload builds a new map - * and swaps atomically; consumer threads always see a consistent snapshot. + * Immutable snapshot combining definitions and their component holders. + * Swapped atomically via a single volatile write to prevent torn reads. */ - private static volatile Map< - ResourceLocation, - DataDrivenItemDefinition - > DEFINITIONS = Map.of(); + private record RegistrySnapshot( + MapOn an integrated server, both the client (assets/) and server (data/) reload * listeners populate this registry. Using {@link #reload} would cause the second * listener to overwrite the first's definitions. This method builds a new map - * from the existing snapshot + the new entries, then swaps atomically.
+ * from the existing snapshot + the new entries, then swaps atomically as a + * single snapshot to prevent torn reads. * * @param newDefs the definitions to merge (will overwrite existing entries with same key) */ @@ -75,11 +79,12 @@ public final class DataDrivenItemRegistry { MapCaptures the snapshot reference once to ensure consistent reads + * between the definition lookup and the holder lookup.
+ * * @param stack the ItemStack to inspect * @return the holder, or null if the stack is not data-driven or has no definition */ @Nullable public static ComponentHolder getComponents(ItemStack stack) { - DataDrivenItemDefinition def = get(stack); + 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) + ); + if (id == null) return null; + // Capture snapshot once to ensure definition and holder come from + // the same atomic snapshot, preventing torn reads. + RegistrySnapshot snap = SNAPSHOT; + DataDrivenItemDefinition def = snap.definitions.get(id); if (def == null) return null; - return COMPONENT_HOLDERS.get(def.id()); + return snap.holders.get(def.id()); } /** @@ -151,7 +168,7 @@ public final class DataDrivenItemRegistry { */ @Nullable public static ComponentHolder getComponents(ResourceLocation id) { - return COMPONENT_HOLDERS.get(id); + return SNAPSHOT.holders.get(id); } /**