diff --git a/src/main/java/com/tiedup/remake/rig/anim/action/AnimationAction.java b/src/main/java/com/tiedup/remake/rig/anim/action/AnimationAction.java index 29f3e8b..7c2bbef 100644 --- a/src/main/java/com/tiedup/remake/rig/anim/action/AnimationAction.java +++ b/src/main/java/com/tiedup/remake/rig/anim/action/AnimationAction.java @@ -11,6 +11,7 @@ import net.minecraft.resources.ResourceLocation; import com.tiedup.remake.rig.anim.types.DynamicAnimation; import com.tiedup.remake.rig.asset.AssetAccessor; import com.tiedup.remake.rig.patch.LivingEntityPatch; +import com.tiedup.remake.rig.util.CodecDispatchRegistry; /** * Phase 3 D2 — biggest artist unlock : an {@code AnimationAction} is a @@ -39,7 +40,7 @@ import com.tiedup.remake.rig.patch.LivingEntityPatch; * trigger frame. Today's core actions ignore them — they execute atomically * on trigger. */ -public interface AnimationAction { +public interface AnimationAction extends CodecDispatchRegistry.Typed { /** * Dispatch codec — reads the {@code "type"} field of the JSON object and @@ -53,12 +54,13 @@ public interface AnimationAction { * Codec}, which cannot express «unknown * type» without throwing. */ - Codec CODEC = AnimationActionRegistry.dispatchCodec(); + Codec CODEC = AnimationActionRegistry.INSTANCE.dispatchCodec(); /** * The registered type id of this action (e.g. {@code tiedup:play_sound}). * Used by the dispatch codec to serialize back to JSON. */ + @Override ResourceLocation type(); /** diff --git a/src/main/java/com/tiedup/remake/rig/anim/action/AnimationActionRegistry.java b/src/main/java/com/tiedup/remake/rig/anim/action/AnimationActionRegistry.java index bcfa4d8..51ab0b7 100644 --- a/src/main/java/com/tiedup/remake/rig/anim/action/AnimationActionRegistry.java +++ b/src/main/java/com/tiedup/remake/rig/anim/action/AnimationActionRegistry.java @@ -4,19 +4,11 @@ package com.tiedup.remake.rig.anim.action; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import com.mojang.serialization.Codec; -import com.mojang.serialization.DataResult; - -import net.minecraft.resources.ResourceLocation; - import com.tiedup.remake.rig.anim.action.impl.ApplyEffectAction; import com.tiedup.remake.rig.anim.action.impl.DamageEntityAction; import com.tiedup.remake.rig.anim.action.impl.PlaySoundAction; import com.tiedup.remake.rig.anim.action.impl.SpawnParticleAction; +import com.tiedup.remake.rig.util.CodecDispatchRegistry; /** * Registry of {@link AnimationAction} type ids → codecs, used by the dispatch @@ -29,72 +21,28 @@ import com.tiedup.remake.rig.anim.action.impl.SpawnParticleAction; * full dispatch table. * *

Third-party mods may register additional action types by calling - * {@link #register(ResourceLocation, Codec)} from their common setup event + * {@link #register} on {@link #INSTANCE} from their common setup event * (post-{@code FMLCommonSetup} to avoid class-loading order surprises with * the static init of this class). + * + *

Plumbing (map, register, dispatchCodec) lives in + * {@link CodecDispatchRegistry} since {@code SMELL-CODEC-01}. */ -public final class AnimationActionRegistry { +public final class AnimationActionRegistry extends CodecDispatchRegistry { - private static final Map> TYPES = new HashMap<>(); + public static final AnimationActionRegistry INSTANCE = new AnimationActionRegistry(); private AnimationActionRegistry() {} - /** - * Register a new action type. Throws {@link IllegalStateException} if the - * id is already registered — this is intentional : silent shadowing of a - * built-in action type would be a nasty debugging experience for artists. - * - * @throws IllegalStateException if {@code id} is already registered - */ - public static void register(ResourceLocation id, Codec codec) { - if (TYPES.containsKey(id)) { - throw new IllegalStateException("Animation action type " + id + " is already registered."); - } - - TYPES.put(id, codec); - } - - /** - * Unmodifiable view of all registered action types (for debug / introspection). - */ - public static Map> types() { - return Collections.unmodifiableMap(TYPES); - } - - /** - * Look up the codec for a given type id. Returns {@code null} if the id is - * not registered. - */ - public static Codec getCodec(ResourceLocation id) { - return TYPES.get(id); - } - - /** - * Build the dispatch codec used by {@link AnimationAction#CODEC}. - * - *

Uses {@link Codec#partialDispatch} so that unknown types surface as a - * {@link DataResult} error rather than a thrown exception. The error is - * bubbled up by the standard {@link com.tiedup.remake.rig.anim.property.AnimationProperty#parseFrom} - * pipeline (WARN log + {@code orElseThrow}). - */ - public static Codec dispatchCodec() { - return ResourceLocation.CODEC.partialDispatch( - "type", - action -> DataResult.success(action.type()), - id -> { - Codec codec = TYPES.get(id); - if (codec == null) { - return DataResult.error(() -> "Unknown animation action type: " + id); - } - return DataResult.success(codec); - } - ); + @Override + protected String registryName() { + return "AnimationAction"; } static { - register(PlaySoundAction.ID, PlaySoundAction.CODEC); - register(SpawnParticleAction.ID, SpawnParticleAction.CODEC); - register(ApplyEffectAction.ID, ApplyEffectAction.CODEC); - register(DamageEntityAction.ID, DamageEntityAction.CODEC); + INSTANCE.register(PlaySoundAction.ID, PlaySoundAction.CODEC); + INSTANCE.register(SpawnParticleAction.ID, SpawnParticleAction.CODEC); + INSTANCE.register(ApplyEffectAction.ID, ApplyEffectAction.CODEC); + INSTANCE.register(DamageEntityAction.ID, DamageEntityAction.CODEC); } } diff --git a/src/main/java/com/tiedup/remake/rig/anim/modifier/PlaybackSpeedModifierImpl.java b/src/main/java/com/tiedup/remake/rig/anim/modifier/PlaybackSpeedModifierImpl.java index 30eae2c..7e0e277 100644 --- a/src/main/java/com/tiedup/remake/rig/anim/modifier/PlaybackSpeedModifierImpl.java +++ b/src/main/java/com/tiedup/remake/rig/anim/modifier/PlaybackSpeedModifierImpl.java @@ -11,6 +11,7 @@ import net.minecraft.resources.ResourceLocation; import com.tiedup.remake.rig.anim.property.AnimationProperty.PlaybackSpeedModifier; import com.tiedup.remake.rig.anim.types.DynamicAnimation; import com.tiedup.remake.rig.patch.LivingEntityPatch; +import com.tiedup.remake.rig.util.CodecDispatchRegistry; /** * Phase 3 D1 — data-driven concrete implementation of @@ -28,10 +29,11 @@ import com.tiedup.remake.rig.patch.LivingEntityPatch; * the modifier returns each tick — so a modifier that returns {@code 0.5F} * halves the animation speed. */ -public interface PlaybackSpeedModifierImpl extends PlaybackSpeedModifier { +public interface PlaybackSpeedModifierImpl extends PlaybackSpeedModifier, CodecDispatchRegistry.Typed { - Codec CODEC = PlaybackSpeedModifierRegistry.dispatchCodec(); + Codec CODEC = PlaybackSpeedModifierRegistry.INSTANCE.dispatchCodec(); + @Override ResourceLocation type(); @Override diff --git a/src/main/java/com/tiedup/remake/rig/anim/modifier/PlaybackSpeedModifierRegistry.java b/src/main/java/com/tiedup/remake/rig/anim/modifier/PlaybackSpeedModifierRegistry.java index ac79b8e..5bc3213 100644 --- a/src/main/java/com/tiedup/remake/rig/anim/modifier/PlaybackSpeedModifierRegistry.java +++ b/src/main/java/com/tiedup/remake/rig/anim/modifier/PlaybackSpeedModifierRegistry.java @@ -4,17 +4,9 @@ package com.tiedup.remake.rig.anim.modifier; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import com.mojang.serialization.Codec; -import com.mojang.serialization.DataResult; - -import net.minecraft.resources.ResourceLocation; - import com.tiedup.remake.rig.anim.modifier.impl.ConstantFactorSpeedModifier; import com.tiedup.remake.rig.anim.modifier.impl.LinearRampSpeedModifier; +import com.tiedup.remake.rig.util.CodecDispatchRegistry; /** * Registry of {@link PlaybackSpeedModifierImpl} type ids → codecs. Same @@ -26,45 +18,22 @@ import com.tiedup.remake.rig.anim.modifier.impl.LinearRampSpeedModifier; *

  • {@code tiedup:linear_ramp} — linear interpolation between a start and * end factor over {@code elapsedTime} from 0 to {@code duration}
  • * + * + *

    Plumbing lives in {@link CodecDispatchRegistry} since {@code SMELL-CODEC-01}. */ -public final class PlaybackSpeedModifierRegistry { +public final class PlaybackSpeedModifierRegistry extends CodecDispatchRegistry { - private static final Map> TYPES = new HashMap<>(); + public static final PlaybackSpeedModifierRegistry INSTANCE = new PlaybackSpeedModifierRegistry(); private PlaybackSpeedModifierRegistry() {} - public static void register(ResourceLocation id, Codec codec) { - if (TYPES.containsKey(id)) { - throw new IllegalStateException("Playback speed modifier type " + id + " is already registered."); - } - - TYPES.put(id, codec); - } - - public static Map> types() { - return Collections.unmodifiableMap(TYPES); - } - - public static Codec getCodec(ResourceLocation id) { - return TYPES.get(id); - } - - public static Codec dispatchCodec() { - return ResourceLocation.CODEC.partialDispatch( - "type", - mod -> DataResult.success(mod.type()), - id -> { - Codec codec = TYPES.get(id); - if (codec == null) { - return DataResult.error(() -> "Unknown playback speed modifier type: " + id); - } - return DataResult.success(codec); - } - ); + @Override + protected String registryName() { + return "PlaybackSpeedModifier"; } static { - register(ConstantFactorSpeedModifier.ID, ConstantFactorSpeedModifier.CODEC); - register(LinearRampSpeedModifier.ID, LinearRampSpeedModifier.CODEC); + INSTANCE.register(ConstantFactorSpeedModifier.ID, ConstantFactorSpeedModifier.CODEC); + INSTANCE.register(LinearRampSpeedModifier.ID, LinearRampSpeedModifier.CODEC); } } diff --git a/src/main/java/com/tiedup/remake/rig/anim/modifier/PlaybackTimeModifierImpl.java b/src/main/java/com/tiedup/remake/rig/anim/modifier/PlaybackTimeModifierImpl.java index d391ebd..e987880 100644 --- a/src/main/java/com/tiedup/remake/rig/anim/modifier/PlaybackTimeModifierImpl.java +++ b/src/main/java/com/tiedup/remake/rig/anim/modifier/PlaybackTimeModifierImpl.java @@ -12,6 +12,7 @@ import net.minecraft.resources.ResourceLocation; import com.tiedup.remake.rig.anim.property.AnimationProperty.PlaybackTimeModifier; import com.tiedup.remake.rig.anim.types.DynamicAnimation; import com.tiedup.remake.rig.patch.LivingEntityPatch; +import com.tiedup.remake.rig.util.CodecDispatchRegistry; /** * Phase 3 D1 — data-driven concrete implementation of @@ -26,10 +27,11 @@ import com.tiedup.remake.rig.patch.LivingEntityPatch; * "loop_start": 0.3, "loop_end": 0.8 } * } */ -public interface PlaybackTimeModifierImpl extends PlaybackTimeModifier { +public interface PlaybackTimeModifierImpl extends PlaybackTimeModifier, CodecDispatchRegistry.Typed { - Codec CODEC = PlaybackTimeModifierRegistry.dispatchCodec(); + Codec CODEC = PlaybackTimeModifierRegistry.INSTANCE.dispatchCodec(); + @Override ResourceLocation type(); @Override diff --git a/src/main/java/com/tiedup/remake/rig/anim/modifier/PlaybackTimeModifierRegistry.java b/src/main/java/com/tiedup/remake/rig/anim/modifier/PlaybackTimeModifierRegistry.java index 1a8da04..222fcf1 100644 --- a/src/main/java/com/tiedup/remake/rig/anim/modifier/PlaybackTimeModifierRegistry.java +++ b/src/main/java/com/tiedup/remake/rig/anim/modifier/PlaybackTimeModifierRegistry.java @@ -4,16 +4,8 @@ package com.tiedup.remake.rig.anim.modifier; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import com.mojang.serialization.Codec; -import com.mojang.serialization.DataResult; - -import net.minecraft.resources.ResourceLocation; - import com.tiedup.remake.rig.anim.modifier.impl.LoopSectionTimeModifier; +import com.tiedup.remake.rig.util.CodecDispatchRegistry; /** * Registry of {@link PlaybackTimeModifierImpl} type ids → codecs. @@ -27,44 +19,21 @@ import com.tiedup.remake.rig.anim.modifier.impl.LoopSectionTimeModifier; *

    Not a ton of base impls because the common «replay this window» * behaviour is what 95% of bondage loops need ; more exotic time warps * (ping-pong, jitter) can be added later without a schema break. + * + *

    Plumbing lives in {@link CodecDispatchRegistry} since {@code SMELL-CODEC-01}. */ -public final class PlaybackTimeModifierRegistry { +public final class PlaybackTimeModifierRegistry extends CodecDispatchRegistry { - private static final Map> TYPES = new HashMap<>(); + public static final PlaybackTimeModifierRegistry INSTANCE = new PlaybackTimeModifierRegistry(); private PlaybackTimeModifierRegistry() {} - public static void register(ResourceLocation id, Codec codec) { - if (TYPES.containsKey(id)) { - throw new IllegalStateException("Playback time modifier type " + id + " is already registered."); - } - - TYPES.put(id, codec); - } - - public static Map> types() { - return Collections.unmodifiableMap(TYPES); - } - - public static Codec getCodec(ResourceLocation id) { - return TYPES.get(id); - } - - public static Codec dispatchCodec() { - return ResourceLocation.CODEC.partialDispatch( - "type", - mod -> DataResult.success(mod.type()), - id -> { - Codec codec = TYPES.get(id); - if (codec == null) { - return DataResult.error(() -> "Unknown playback time modifier type: " + id); - } - return DataResult.success(codec); - } - ); + @Override + protected String registryName() { + return "PlaybackTimeModifier"; } static { - register(LoopSectionTimeModifier.ID, LoopSectionTimeModifier.CODEC); + INSTANCE.register(LoopSectionTimeModifier.ID, LoopSectionTimeModifier.CODEC); } } diff --git a/src/main/java/com/tiedup/remake/rig/anim/modifier/PoseModifierImpl.java b/src/main/java/com/tiedup/remake/rig/anim/modifier/PoseModifierImpl.java index 50e4680..49bef9e 100644 --- a/src/main/java/com/tiedup/remake/rig/anim/modifier/PoseModifierImpl.java +++ b/src/main/java/com/tiedup/remake/rig/anim/modifier/PoseModifierImpl.java @@ -12,6 +12,7 @@ import com.tiedup.remake.rig.anim.Pose; import com.tiedup.remake.rig.anim.property.AnimationProperty.PoseModifier; import com.tiedup.remake.rig.anim.types.DynamicAnimation; import com.tiedup.remake.rig.patch.LivingEntityPatch; +import com.tiedup.remake.rig.util.CodecDispatchRegistry; /** * Phase 3 D1 — data-driven concrete implementation of the functional @@ -41,18 +42,19 @@ import com.tiedup.remake.rig.patch.LivingEntityPatch; * mutate the pose must do so deterministically from the inputs only, no IO or * world state. */ -public interface PoseModifierImpl extends PoseModifier { +public interface PoseModifierImpl extends PoseModifier, CodecDispatchRegistry.Typed { /** * Dispatch codec — reads the {@code "type"} field and delegates to the * codec registered for that {@link ResourceLocation}. */ - Codec CODEC = PoseModifierRegistry.dispatchCodec(); + Codec CODEC = PoseModifierRegistry.INSTANCE.dispatchCodec(); /** * The registered type id of this modifier (e.g. * {@code tiedup:joint_rotation_offset}). */ + @Override ResourceLocation type(); /** diff --git a/src/main/java/com/tiedup/remake/rig/anim/modifier/PoseModifierRegistry.java b/src/main/java/com/tiedup/remake/rig/anim/modifier/PoseModifierRegistry.java index f75d8db..b705cf0 100644 --- a/src/main/java/com/tiedup/remake/rig/anim/modifier/PoseModifierRegistry.java +++ b/src/main/java/com/tiedup/remake/rig/anim/modifier/PoseModifierRegistry.java @@ -4,18 +4,10 @@ package com.tiedup.remake.rig.anim.modifier; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import com.mojang.serialization.Codec; -import com.mojang.serialization.DataResult; - -import net.minecraft.resources.ResourceLocation; - import com.tiedup.remake.rig.anim.modifier.impl.ChainedPoseModifier; import com.tiedup.remake.rig.anim.modifier.impl.JointRotationOffsetModifier; import com.tiedup.remake.rig.anim.modifier.impl.JointTranslationOffsetModifier; +import com.tiedup.remake.rig.util.CodecDispatchRegistry; /** * Registry of {@link PoseModifierImpl} type ids → codecs. Mirrors the design @@ -31,52 +23,23 @@ import com.tiedup.remake.rig.anim.modifier.impl.JointTranslationOffsetModifier; *

    Registering via the static init means a single reference to * {@link PoseModifierImpl#CODEC} in a parse path is enough to bootstrap the * dispatch table, same bootstrap contract as the action registry. + * + *

    Plumbing lives in {@link CodecDispatchRegistry} since {@code SMELL-CODEC-01}. */ -public final class PoseModifierRegistry { +public final class PoseModifierRegistry extends CodecDispatchRegistry { - private static final Map> TYPES = new HashMap<>(); + public static final PoseModifierRegistry INSTANCE = new PoseModifierRegistry(); private PoseModifierRegistry() {} - /** - * @throws IllegalStateException if {@code id} is already registered - */ - public static void register(ResourceLocation id, Codec codec) { - if (TYPES.containsKey(id)) { - throw new IllegalStateException("Pose modifier type " + id + " is already registered."); - } - - TYPES.put(id, codec); - } - - /** - * Unmodifiable view of all registered modifier types. - */ - public static Map> types() { - return Collections.unmodifiableMap(TYPES); - } - - public static Codec getCodec(ResourceLocation id) { - return TYPES.get(id); - } - - public static Codec dispatchCodec() { - return ResourceLocation.CODEC.partialDispatch( - "type", - mod -> DataResult.success(mod.type()), - id -> { - Codec codec = TYPES.get(id); - if (codec == null) { - return DataResult.error(() -> "Unknown pose modifier type: " + id); - } - return DataResult.success(codec); - } - ); + @Override + protected String registryName() { + return "PoseModifier"; } static { - register(JointRotationOffsetModifier.ID, JointRotationOffsetModifier.CODEC); - register(JointTranslationOffsetModifier.ID, JointTranslationOffsetModifier.CODEC); - register(ChainedPoseModifier.ID, ChainedPoseModifier.CODEC); + INSTANCE.register(JointRotationOffsetModifier.ID, JointRotationOffsetModifier.CODEC); + INSTANCE.register(JointTranslationOffsetModifier.ID, JointTranslationOffsetModifier.CODEC); + INSTANCE.register(ChainedPoseModifier.ID, ChainedPoseModifier.CODEC); } } diff --git a/src/main/java/com/tiedup/remake/rig/util/CodecDispatchRegistry.java b/src/main/java/com/tiedup/remake/rig/util/CodecDispatchRegistry.java new file mode 100644 index 0000000..a0666d2 --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/util/CodecDispatchRegistry.java @@ -0,0 +1,133 @@ +/* + * © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.util; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; + +import net.minecraft.resources.ResourceLocation; + +/** + * Generic dispatch-codec registry — holds a {@code ResourceLocation → Codec} + * table and exposes a {@link #dispatchCodec()} that reads a {@code "type"} + * discriminator field, looks up the matching codec, and delegates parsing. + * + *

    Extracted in {@code SMELL-CODEC-01} : prior to this, four registries + * ({@code AnimationActionRegistry}, {@code PoseModifierRegistry}, + * {@code PlaybackSpeedModifierRegistry}, {@code PlaybackTimeModifierRegistry}) + * had structurally identical implementations — a private static + * {@code TYPES} map, a {@code register} that throws on duplicate ids, getters, + * and a hand-rolled {@code partialDispatch} block. ~50 LOC each, ~200 LOC of + * pure copy-paste. A bug fix in one had to be ported to the other three. + * + *

    Subclasses are expected to be singletons : declare a public static + * {@code INSTANCE} field, a private constructor, and a {@code static {}} block + * that calls {@link #register} for each built-in type. The interface they + * dispatch on must extend {@link Typed} so that the dispatch codec can read + * back the type id during encode. + * + *

    Init-order contract : when the dispatched interface declares its + * {@code CODEC} as {@code Subclass.INSTANCE.dispatchCodec()}, referencing the + * codec triggers the subclass's class-init, which (a) constructs + * {@code INSTANCE} (initializing the empty type map) and (b) runs the + * {@code static {}} block (populating the map). The codec returned by + * {@code dispatchCodec()} performs map lookups at parse time, never + * at codec construction, so the order in which built-ins register is + * irrelevant. + */ +public abstract class CodecDispatchRegistry { + + /** + * Marker for types that can be dispatched on. Each implementation reports + * its type id, which is the same id under which its {@link Codec} was + * registered. Used by {@link #dispatchCodec()} to embed the discriminator + * during encode. + */ + public interface Typed { + ResourceLocation type(); + } + + private final Map> types = new HashMap<>(); + + /** + * Subclass-specific human-readable name — used in the duplicate + * registration exception and in the «unknown type» dispatch error so the + * caller can tell which registry rejected the input. + */ + protected abstract String registryName(); + + /** + * Register a new type. Throws on duplicate id : silent shadowing of a + * built-in would be a nasty debugging experience for datapack authors and + * is never the intended behaviour. + * + * @throws IllegalArgumentException if {@code id} is already registered + */ + public final void register(ResourceLocation id, Codec codec) { + Codec previous = this.types.put(id, codec); + if (previous != null) { + // Re-insert the previous mapping so a thrown registration does not + // corrupt state for callers that catch the exception. + this.types.put(id, previous); + throw new IllegalArgumentException( + "Duplicate registration in " + this.registryName() + ": " + id + ); + } + } + + /** + * Look up the codec for a given type id. Returns {@code null} if the id is + * not registered — callers that need a {@code DataResult} should use + * {@link #dispatchCodec()}. + */ + public final Codec getCodec(ResourceLocation id) { + return this.types.get(id); + } + + /** + * Unmodifiable view of all registered type ids (debug / introspection). + */ + public final Set types() { + return Collections.unmodifiableSet(this.types.keySet()); + } + + /** + * Build the dispatch codec. + * + *

    Uses {@link Codec#partialDispatch} rather than {@link Codec#dispatch} + * so that unknown types surface as a {@link DataResult} error instead of + * an unchecked exception — the upstream parser logs and drops the + * offending entry instead of crashing the load. + */ + public final Codec dispatchCodec() { + return ResourceLocation.CODEC.partialDispatch( + "type", + value -> DataResult.success(value.type()), + id -> { + Codec codec = this.types.get(id); + if (codec == null) { + return DataResult.error( + () -> "Unknown " + this.registryName() + " type: " + id + ); + } + return DataResult.success(codec); + } + ); + } + + /** + * Test-only state reset — clears the type map. Public so JUnit + * fixtures across packages can call it but named to make production usage + * obviously wrong. + */ + public final void clearForTests() { + this.types.clear(); + } +} diff --git a/src/test/java/com/tiedup/remake/rig/anim/action/AnimationActionRegistryTest.java b/src/test/java/com/tiedup/remake/rig/anim/action/AnimationActionRegistryTest.java index d6533c3..94577ef 100644 --- a/src/test/java/com/tiedup/remake/rig/anim/action/AnimationActionRegistryTest.java +++ b/src/test/java/com/tiedup/remake/rig/anim/action/AnimationActionRegistryTest.java @@ -53,30 +53,30 @@ class AnimationActionRegistryTest { @Test void coreActions_registered() { - assertNotNull(AnimationActionRegistry.getCodec(PlaySoundAction.ID)); - assertNotNull(AnimationActionRegistry.getCodec(SpawnParticleAction.ID)); - assertNotNull(AnimationActionRegistry.getCodec(ApplyEffectAction.ID)); - assertNotNull(AnimationActionRegistry.getCodec(DamageEntityAction.ID)); + assertNotNull(AnimationActionRegistry.INSTANCE.getCodec(PlaySoundAction.ID)); + assertNotNull(AnimationActionRegistry.INSTANCE.getCodec(SpawnParticleAction.ID)); + assertNotNull(AnimationActionRegistry.INSTANCE.getCodec(ApplyEffectAction.ID)); + assertNotNull(AnimationActionRegistry.INSTANCE.getCodec(DamageEntityAction.ID)); } @Test void types_returnsFourEntries_atMinimum() { // At least 4 (there could be additional registrations by other tests, // but we should have our 4 core). - assertTrue(AnimationActionRegistry.types().size() >= 4); + assertTrue(AnimationActionRegistry.INSTANCE.types().size() >= 4); } @Test void getCodec_unknownType_returnsNull() { - assertNull(AnimationActionRegistry.getCodec(new ResourceLocation("foo", "bar"))); + assertNull(AnimationActionRegistry.INSTANCE.getCodec(new ResourceLocation("foo", "bar"))); } @Test void register_duplicateId_throws() { // Attempt to re-register PlaySoundAction id → must throw, never // silently shadow. Uses an arbitrary no-op codec for the retry. - assertThrows(IllegalStateException.class, () -> - AnimationActionRegistry.register(PlaySoundAction.ID, PlaySoundAction.CODEC) + assertThrows(IllegalArgumentException.class, () -> + AnimationActionRegistry.INSTANCE.register(PlaySoundAction.ID, PlaySoundAction.CODEC) ); } diff --git a/src/test/java/com/tiedup/remake/rig/anim/modifier/PlaybackSpeedModifierRegistryTest.java b/src/test/java/com/tiedup/remake/rig/anim/modifier/PlaybackSpeedModifierRegistryTest.java index 7c2d026..325566c 100644 --- a/src/test/java/com/tiedup/remake/rig/anim/modifier/PlaybackSpeedModifierRegistryTest.java +++ b/src/test/java/com/tiedup/remake/rig/anim/modifier/PlaybackSpeedModifierRegistryTest.java @@ -36,19 +36,19 @@ class PlaybackSpeedModifierRegistryTest { @Test void baseImpls_registered() { - assertNotNull(PlaybackSpeedModifierRegistry.getCodec(ConstantFactorSpeedModifier.ID)); - assertNotNull(PlaybackSpeedModifierRegistry.getCodec(LinearRampSpeedModifier.ID)); + assertNotNull(PlaybackSpeedModifierRegistry.INSTANCE.getCodec(ConstantFactorSpeedModifier.ID)); + assertNotNull(PlaybackSpeedModifierRegistry.INSTANCE.getCodec(LinearRampSpeedModifier.ID)); } @Test void getCodec_unknownType_returnsNull() { - assertNull(PlaybackSpeedModifierRegistry.getCodec(new ResourceLocation("foo", "bar"))); + assertNull(PlaybackSpeedModifierRegistry.INSTANCE.getCodec(new ResourceLocation("foo", "bar"))); } @Test void register_duplicateId_throws() { - assertThrows(IllegalStateException.class, () -> - PlaybackSpeedModifierRegistry.register(ConstantFactorSpeedModifier.ID, ConstantFactorSpeedModifier.CODEC) + assertThrows(IllegalArgumentException.class, () -> + PlaybackSpeedModifierRegistry.INSTANCE.register(ConstantFactorSpeedModifier.ID, ConstantFactorSpeedModifier.CODEC) ); } diff --git a/src/test/java/com/tiedup/remake/rig/anim/modifier/PlaybackTimeModifierRegistryTest.java b/src/test/java/com/tiedup/remake/rig/anim/modifier/PlaybackTimeModifierRegistryTest.java index a0640e3..b399f59 100644 --- a/src/test/java/com/tiedup/remake/rig/anim/modifier/PlaybackTimeModifierRegistryTest.java +++ b/src/test/java/com/tiedup/remake/rig/anim/modifier/PlaybackTimeModifierRegistryTest.java @@ -35,18 +35,18 @@ class PlaybackTimeModifierRegistryTest { @Test void baseImpl_registered() { - assertNotNull(PlaybackTimeModifierRegistry.getCodec(LoopSectionTimeModifier.ID)); + assertNotNull(PlaybackTimeModifierRegistry.INSTANCE.getCodec(LoopSectionTimeModifier.ID)); } @Test void getCodec_unknownType_returnsNull() { - assertNull(PlaybackTimeModifierRegistry.getCodec(new ResourceLocation("foo", "bar"))); + assertNull(PlaybackTimeModifierRegistry.INSTANCE.getCodec(new ResourceLocation("foo", "bar"))); } @Test void register_duplicateId_throws() { - assertThrows(IllegalStateException.class, () -> - PlaybackTimeModifierRegistry.register(LoopSectionTimeModifier.ID, LoopSectionTimeModifier.CODEC) + assertThrows(IllegalArgumentException.class, () -> + PlaybackTimeModifierRegistry.INSTANCE.register(LoopSectionTimeModifier.ID, LoopSectionTimeModifier.CODEC) ); } diff --git a/src/test/java/com/tiedup/remake/rig/anim/modifier/PoseModifierRegistryTest.java b/src/test/java/com/tiedup/remake/rig/anim/modifier/PoseModifierRegistryTest.java index 32998d3..d56de78 100644 --- a/src/test/java/com/tiedup/remake/rig/anim/modifier/PoseModifierRegistryTest.java +++ b/src/test/java/com/tiedup/remake/rig/anim/modifier/PoseModifierRegistryTest.java @@ -44,25 +44,25 @@ class PoseModifierRegistryTest { @Test void baseImpls_registered() { - assertNotNull(PoseModifierRegistry.getCodec(JointRotationOffsetModifier.ID)); - assertNotNull(PoseModifierRegistry.getCodec(JointTranslationOffsetModifier.ID)); - assertNotNull(PoseModifierRegistry.getCodec(ChainedPoseModifier.ID)); + assertNotNull(PoseModifierRegistry.INSTANCE.getCodec(JointRotationOffsetModifier.ID)); + assertNotNull(PoseModifierRegistry.INSTANCE.getCodec(JointTranslationOffsetModifier.ID)); + assertNotNull(PoseModifierRegistry.INSTANCE.getCodec(ChainedPoseModifier.ID)); } @Test void types_returnsThreeEntries_atMinimum() { - assertTrue(PoseModifierRegistry.types().size() >= 3); + assertTrue(PoseModifierRegistry.INSTANCE.types().size() >= 3); } @Test void getCodec_unknownType_returnsNull() { - assertNull(PoseModifierRegistry.getCodec(new ResourceLocation("foo", "bar"))); + assertNull(PoseModifierRegistry.INSTANCE.getCodec(new ResourceLocation("foo", "bar"))); } @Test void register_duplicateId_throws() { - assertThrows(IllegalStateException.class, () -> - PoseModifierRegistry.register(JointRotationOffsetModifier.ID, JointRotationOffsetModifier.CODEC) + assertThrows(IllegalArgumentException.class, () -> + PoseModifierRegistry.INSTANCE.register(JointRotationOffsetModifier.ID, JointRotationOffsetModifier.CODEC) ); } diff --git a/src/test/java/com/tiedup/remake/rig/util/CodecDispatchRegistryTest.java b/src/test/java/com/tiedup/remake/rig/util/CodecDispatchRegistryTest.java new file mode 100644 index 0000000..fb866ca --- /dev/null +++ b/src/test/java/com/tiedup/remake/rig/util/CodecDispatchRegistryTest.java @@ -0,0 +1,172 @@ +/* + * © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.JsonOps; +import com.mojang.serialization.codecs.RecordCodecBuilder; + +import net.minecraft.resources.ResourceLocation; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link CodecDispatchRegistry} — covers the four behaviours + * subclasses inherit : duplicate-id rejection, getCodec lookup, dispatch + * round-trip, and unknown-type error message. + * + *

    Uses a minimal pure-Java fixture {@link FakeRegistry} + + * {@link FakeTyped} so the test does not depend on the rig anim package. + */ +class CodecDispatchRegistryTest { + + // ===== Fixture ===== + + private interface FakeTyped extends CodecDispatchRegistry.Typed { + String value(); + } + + private record FakeImpl(String value) implements FakeTyped { + static final ResourceLocation ID = new ResourceLocation("test", "fake"); + static final Codec CODEC = RecordCodecBuilder.create(inst -> inst.group( + Codec.STRING.fieldOf("value").forGetter(FakeImpl::value) + ).apply(inst, FakeImpl::new)); + + @Override + public ResourceLocation type() { + return ID; + } + } + + private record OtherImpl(String value) implements FakeTyped { + static final ResourceLocation ID = new ResourceLocation("test", "other"); + static final Codec CODEC = RecordCodecBuilder.create(inst -> inst.group( + Codec.STRING.fieldOf("value").forGetter(OtherImpl::value) + ).apply(inst, OtherImpl::new)); + + @Override + public ResourceLocation type() { + return ID; + } + } + + private static final class FakeRegistry extends CodecDispatchRegistry { + @Override + protected String registryName() { + return "FakeRegistry"; + } + } + + private FakeRegistry registry; + + @BeforeEach + void setup() { + this.registry = new FakeRegistry(); + } + + // ===== Behaviour ===== + + @Test + void register_thenGetCodec_returnsRegisteredCodec() { + this.registry.register(FakeImpl.ID, FakeImpl.CODEC); + assertNotNull(this.registry.getCodec(FakeImpl.ID)); + assertTrue(this.registry.types().contains(FakeImpl.ID)); + } + + @Test + void getCodec_unregisteredId_returnsNull() { + assertNull(this.registry.getCodec(new ResourceLocation("nope", "nope"))); + } + + @Test + void register_duplicateId_throwsAndPreservesPreviousMapping() { + this.registry.register(FakeImpl.ID, FakeImpl.CODEC); + // Duplicate registration must throw, and must NOT corrupt the existing + // mapping (regression guard : the naive map.put-then-check would have + // overwritten the original codec before throwing). + IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> this.registry.register(FakeImpl.ID, FakeImpl.CODEC) + ); + assertTrue(ex.getMessage().contains("FakeRegistry")); + assertTrue(ex.getMessage().contains(FakeImpl.ID.toString())); + // Mapping intact : + assertNotNull(this.registry.getCodec(FakeImpl.ID)); + } + + @Test + void dispatchCodec_roundtrip_knownType() { + this.registry.register(FakeImpl.ID, FakeImpl.CODEC); + Codec dispatch = this.registry.dispatchCodec(); + + FakeImpl original = new FakeImpl("hello"); + DataResult encoded = dispatch.encodeStart(JsonOps.INSTANCE, original); + assertTrue(encoded.result().isPresent(), "encode error : " + encoded.error()); + + JsonObject json = encoded.result().get().getAsJsonObject(); + assertEquals("test:fake", json.get("type").getAsString()); + assertEquals("hello", json.get("value").getAsString()); + + DataResult decoded = dispatch.parse(JsonOps.INSTANCE, json); + assertTrue(decoded.result().isPresent(), "decode error : " + decoded.error()); + assertEquals(original, decoded.result().get()); + } + + @Test + void dispatchCodec_unknownType_returnsErrorWithRegistryName() { + this.registry.register(FakeImpl.ID, FakeImpl.CODEC); + Codec dispatch = this.registry.dispatchCodec(); + + JsonObject json = new JsonObject(); + json.add("type", new JsonPrimitive("foo:bar")); + + DataResult decoded = dispatch.parse(JsonOps.INSTANCE, json); + assertFalse(decoded.result().isPresent()); + String message = decoded.error().get().message(); + assertTrue(message.contains("FakeRegistry"), "expected registry name in error, got : " + message); + assertTrue(message.contains("foo:bar"), "expected unknown id in error, got : " + message); + } + + @Test + void dispatchCodec_lookupHappensAtParseTime_notConstructionTime() { + // Build the codec BEFORE registering anything — codec must still work + // once the registry is populated, because partialDispatch defers map + // reads to the parse-time lambda. This is the property that makes + // «interface CODEC = INSTANCE.dispatchCodec()» init-order-safe. + Codec dispatch = this.registry.dispatchCodec(); + + this.registry.register(FakeImpl.ID, FakeImpl.CODEC); + this.registry.register(OtherImpl.ID, OtherImpl.CODEC); + + JsonObject json = new JsonObject(); + json.add("type", new JsonPrimitive("test:other")); + json.add("value", new JsonPrimitive("late")); + + DataResult decoded = dispatch.parse(JsonOps.INSTANCE, json); + assertTrue(decoded.result().isPresent(), "decode error : " + decoded.error()); + assertEquals(new OtherImpl("late"), decoded.result().get()); + } + + @Test + void clearForTests_emptiesTheMap() { + this.registry.register(FakeImpl.ID, FakeImpl.CODEC); + assertEquals(1, this.registry.types().size()); + this.registry.clearForTests(); + assertEquals(0, this.registry.types().size()); + assertNull(this.registry.getCodec(FakeImpl.ID)); + } +}