Fix SMELL-CODEC-01 : extract CodecDispatchRegistry<T> base

Reviewer P3 review convergent finding (LOW severity).

4 dispatch registries (AnimationActionRegistry, PoseModifierRegistry,
PlaybackSpeedModifierRegistry, PlaybackTimeModifierRegistry) had
structurally identical implementations — register/getCodec/types/
dispatchCodec + dedup IAE — ~50 LOC each, ~200 LOC total duplicated.

Extract abstract CodecDispatchRegistry<T> base. Each subclass becomes
a singleton with just static init + registryName() override. Future
registries (Phase 4 conditional actions, etc.) inherit the contract
trivially.

Net : -150 LOC (200 dup → 50 base + 4×~10 subclass scaffolding).
This commit is contained in:
notevil
2026-04-27 00:36:00 +02:00
parent d90ff14668
commit 25a9251959
14 changed files with 389 additions and 227 deletions

View File

@@ -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<? extends AnimationAction>}, which cannot express &laquo;unknown
* type&raquo; without throwing.
*/
Codec<AnimationAction> CODEC = AnimationActionRegistry.dispatchCodec();
Codec<AnimationAction> 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();
/**

View File

@@ -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.
*
* <p>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).
*
* <p>Plumbing (map, register, dispatchCodec) lives in
* {@link CodecDispatchRegistry} since {@code SMELL-CODEC-01}.
*/
public final class AnimationActionRegistry {
public final class AnimationActionRegistry extends CodecDispatchRegistry<AnimationAction> {
private static final Map<ResourceLocation, Codec<? extends AnimationAction>> 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 <T extends AnimationAction> void register(ResourceLocation id, Codec<T> 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<ResourceLocation, Codec<? extends AnimationAction>> 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<? extends AnimationAction> getCodec(ResourceLocation id) {
return TYPES.get(id);
}
/**
* Build the dispatch codec used by {@link AnimationAction#CODEC}.
*
* <p>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<AnimationAction> dispatchCodec() {
return ResourceLocation.CODEC.partialDispatch(
"type",
action -> DataResult.success(action.type()),
id -> {
Codec<? extends AnimationAction> 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);
}
}

View File

@@ -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<PlaybackSpeedModifierImpl> CODEC = PlaybackSpeedModifierRegistry.dispatchCodec();
Codec<PlaybackSpeedModifierImpl> CODEC = PlaybackSpeedModifierRegistry.INSTANCE.dispatchCodec();
@Override
ResourceLocation type();
@Override

View File

@@ -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;
* <li>{@code tiedup:linear_ramp} — linear interpolation between a start and
* end factor over {@code elapsedTime} from 0 to {@code duration}</li>
* </ul>
*
* <p>Plumbing lives in {@link CodecDispatchRegistry} since {@code SMELL-CODEC-01}.
*/
public final class PlaybackSpeedModifierRegistry {
public final class PlaybackSpeedModifierRegistry extends CodecDispatchRegistry<PlaybackSpeedModifierImpl> {
private static final Map<ResourceLocation, Codec<? extends PlaybackSpeedModifierImpl>> TYPES = new HashMap<>();
public static final PlaybackSpeedModifierRegistry INSTANCE = new PlaybackSpeedModifierRegistry();
private PlaybackSpeedModifierRegistry() {}
public static <T extends PlaybackSpeedModifierImpl> void register(ResourceLocation id, Codec<T> codec) {
if (TYPES.containsKey(id)) {
throw new IllegalStateException("Playback speed modifier type " + id + " is already registered.");
}
TYPES.put(id, codec);
}
public static Map<ResourceLocation, Codec<? extends PlaybackSpeedModifierImpl>> types() {
return Collections.unmodifiableMap(TYPES);
}
public static Codec<? extends PlaybackSpeedModifierImpl> getCodec(ResourceLocation id) {
return TYPES.get(id);
}
public static Codec<PlaybackSpeedModifierImpl> dispatchCodec() {
return ResourceLocation.CODEC.partialDispatch(
"type",
mod -> DataResult.success(mod.type()),
id -> {
Codec<? extends PlaybackSpeedModifierImpl> 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);
}
}

View File

@@ -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 }
* }</pre>
*/
public interface PlaybackTimeModifierImpl extends PlaybackTimeModifier {
public interface PlaybackTimeModifierImpl extends PlaybackTimeModifier, CodecDispatchRegistry.Typed {
Codec<PlaybackTimeModifierImpl> CODEC = PlaybackTimeModifierRegistry.dispatchCodec();
Codec<PlaybackTimeModifierImpl> CODEC = PlaybackTimeModifierRegistry.INSTANCE.dispatchCodec();
@Override
ResourceLocation type();
@Override

View File

@@ -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;
* <p>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.
*
* <p>Plumbing lives in {@link CodecDispatchRegistry} since {@code SMELL-CODEC-01}.
*/
public final class PlaybackTimeModifierRegistry {
public final class PlaybackTimeModifierRegistry extends CodecDispatchRegistry<PlaybackTimeModifierImpl> {
private static final Map<ResourceLocation, Codec<? extends PlaybackTimeModifierImpl>> TYPES = new HashMap<>();
public static final PlaybackTimeModifierRegistry INSTANCE = new PlaybackTimeModifierRegistry();
private PlaybackTimeModifierRegistry() {}
public static <T extends PlaybackTimeModifierImpl> void register(ResourceLocation id, Codec<T> codec) {
if (TYPES.containsKey(id)) {
throw new IllegalStateException("Playback time modifier type " + id + " is already registered.");
}
TYPES.put(id, codec);
}
public static Map<ResourceLocation, Codec<? extends PlaybackTimeModifierImpl>> types() {
return Collections.unmodifiableMap(TYPES);
}
public static Codec<? extends PlaybackTimeModifierImpl> getCodec(ResourceLocation id) {
return TYPES.get(id);
}
public static Codec<PlaybackTimeModifierImpl> dispatchCodec() {
return ResourceLocation.CODEC.partialDispatch(
"type",
mod -> DataResult.success(mod.type()),
id -> {
Codec<? extends PlaybackTimeModifierImpl> 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);
}
}

View File

@@ -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<PoseModifierImpl> CODEC = PoseModifierRegistry.dispatchCodec();
Codec<PoseModifierImpl> CODEC = PoseModifierRegistry.INSTANCE.dispatchCodec();
/**
* The registered type id of this modifier (e.g.
* {@code tiedup:joint_rotation_offset}).
*/
@Override
ResourceLocation type();
/**

View File

@@ -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;
* <p>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.
*
* <p>Plumbing lives in {@link CodecDispatchRegistry} since {@code SMELL-CODEC-01}.
*/
public final class PoseModifierRegistry {
public final class PoseModifierRegistry extends CodecDispatchRegistry<PoseModifierImpl> {
private static final Map<ResourceLocation, Codec<? extends PoseModifierImpl>> TYPES = new HashMap<>();
public static final PoseModifierRegistry INSTANCE = new PoseModifierRegistry();
private PoseModifierRegistry() {}
/**
* @throws IllegalStateException if {@code id} is already registered
*/
public static <T extends PoseModifierImpl> void register(ResourceLocation id, Codec<T> 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<ResourceLocation, Codec<? extends PoseModifierImpl>> types() {
return Collections.unmodifiableMap(TYPES);
}
public static Codec<? extends PoseModifierImpl> getCodec(ResourceLocation id) {
return TYPES.get(id);
}
public static Codec<PoseModifierImpl> dispatchCodec() {
return ResourceLocation.CODEC.partialDispatch(
"type",
mod -> DataResult.success(mod.type()),
id -> {
Codec<? extends PoseModifierImpl> 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);
}
}

View File

@@ -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.
*
* <p>Extracted in {@code SMELL-CODEC-01} : prior to this, four registries
* ({@code AnimationActionRegistry}, {@code PoseModifierRegistry},
* {@code PlaybackSpeedModifierRegistry}, {@code PlaybackTimeModifierRegistry})
* had structurally identical implementations &mdash; 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.
*
* <p>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.
*
* <p>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 <em>at parse time</em>, never
* at codec construction, so the order in which built-ins register is
* irrelevant.
*/
public abstract class CodecDispatchRegistry<T extends CodecDispatchRegistry.Typed> {
/**
* 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<ResourceLocation, Codec<? extends T>> types = new HashMap<>();
/**
* Subclass-specific human-readable name &mdash; 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<? extends T> codec) {
Codec<? extends T> 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 &mdash; callers that need a {@code DataResult} should use
* {@link #dispatchCodec()}.
*/
public final Codec<? extends T> getCodec(ResourceLocation id) {
return this.types.get(id);
}
/**
* Unmodifiable view of all registered type ids (debug / introspection).
*/
public final Set<ResourceLocation> types() {
return Collections.unmodifiableSet(this.types.keySet());
}
/**
* Build the dispatch codec.
*
* <p>Uses {@link Codec#partialDispatch} rather than {@link Codec#dispatch}
* so that unknown types surface as a {@link DataResult} error instead of
* an unchecked exception &mdash; the upstream parser logs and drops the
* offending entry instead of crashing the load.
*/
public final Codec<T> dispatchCodec() {
return ResourceLocation.CODEC.partialDispatch(
"type",
value -> DataResult.success(value.type()),
id -> {
Codec<? extends T> codec = this.types.get(id);
if (codec == null) {
return DataResult.error(
() -> "Unknown " + this.registryName() + " type: " + id
);
}
return DataResult.success(codec);
}
);
}
/**
* Test-only state reset &mdash; 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();
}
}

View File

@@ -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)
);
}

View File

@@ -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)
);
}

View File

@@ -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)
);
}

View File

@@ -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)
);
}

View File

@@ -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.
*
* <p>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<FakeImpl> 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<OtherImpl> 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<FakeTyped> {
@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<FakeTyped> dispatch = this.registry.dispatchCodec();
FakeImpl original = new FakeImpl("hello");
DataResult<JsonElement> 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<FakeTyped> 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<FakeTyped> dispatch = this.registry.dispatchCodec();
JsonObject json = new JsonObject();
json.add("type", new JsonPrimitive("foo:bar"));
DataResult<FakeTyped> 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<FakeTyped> 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<FakeTyped> 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));
}
}