P3-01 review fixes : thread-safe assign() + enforce enum load order

BUG-001 reviewer: ExtendableEnumManager.assign() non thread-safe,
synchronize la méthode. Pas d impact perf (N×11 calls au bootstrap).

RISK-001 reviewer: load-order LivingMotions vanilla vs TiedUpLivingMotions
custom pouvait faire coller les ordinals si vanilla pas pre-loadé.
Force explicit LivingMotions.values() avant TiedUpLivingMotions.values()
dans commonSetup.

Test renommé pour refléter honnêtement ce qui est testé (vs load order
implicitement correct dans le test).
This commit is contained in:
notevil
2026-04-23 13:16:47 +02:00
parent cef589aac1
commit ddaa25b971
3 changed files with 53 additions and 14 deletions

View File

@@ -138,9 +138,16 @@ public class TiedUpMod {
// Register dispenser behaviors (must be on main thread)
event.enqueueWork(DispenserBehaviors::register);
// RIG Phase 3 — force class-load des motions custom TiedUp! pour assigner
// les universalOrdinal() via ExtendableEnumManager (init lazy JLS sinon).
// LivingMotions (vanilla EF) est class-loaded naturellement par les patches.
// RIG Phase 3 — force class-load des motions pour assigner les
// universalOrdinal() via ExtendableEnumManager (init lazy JLS sinon).
//
// ORDER MATTERS (review RISK-001 on P3-01 commit 15e405f) : LivingMotions
// vanilla EF doit etre class-loaded AVANT TiedUpLivingMotions, sinon nos
// ordinals prennent 0..N, puis les vanilla arriveraient plus tard avec
// les memes ordinals -> collision silencieuse. Les patches EF ne
// garantissent pas que LivingMotions soit touche avant ce hook, donc on
// le force explicitement ici.
com.tiedup.remake.rig.anim.LivingMotions.values();
com.tiedup.remake.rig.anim.TiedUpLivingMotions.values();
// RIG Phase 2 — dispatcher EntityType → EntityPatch (PLAYER Phase 2, NPCs Phase 5)

View File

@@ -42,7 +42,14 @@ public class ExtendableEnumManager<T extends ExtendableEnum> {
this.enums.put(modid, cls);
}
public void loadEnum() {
/**
* Synchronized to guard against concurrent class-load of multiple extendable
* enum classes on different threads. The reflective invoke of values() below
* triggers static init which calls {@link #assign(ExtendableEnum)} — also
* synchronized on the same monitor (re-entrant, no deadlock risk). See
* review BUG-001 on P3-01 commit 15e405f.
*/
public synchronized void loadEnum() {
List<String> orderByModid = new ArrayList<>(this.enums.keySet());
Collections.sort(orderByModid);
Class<?> cls = null;
@@ -68,7 +75,17 @@ public class ExtendableEnumManager<T extends ExtendableEnum> {
TiedUpRigConstants.LOGGER.debug("All enums are loaded: " + this.enumName + " " + this.enumMapByName.values());
}
public int assign(T value) {
/**
* Synchronized because {@code lastOrdinal}, {@code enumMapByOrdinal}
* ({@link Int2ObjectLinkedOpenHashMap}) and {@code enumMapByName}
* ({@link java.util.LinkedHashMap}) are not thread-safe. Concurrent class
* init of extendable enums on different threads (plausible if another mod
* extends LivingMotion at the same bootstrap) would race on the
* read-modify-write of lastOrdinal and on map mutation. No perf impact —
* called N×|motions| times at bootstrap only. See review BUG-001 on P3-01
* commit 15e405f.
*/
public synchronized int assign(T value) {
int lastOrdinal = this.lastOrdinal;
String enumName = ParseUtil.toLowerCase(value.toString());