Skip to content

Commit 88281b5

Browse files
feat(neoforge): v26.7-Alpha.4 - E2E hardening round with real JEI mod
Real-game E2E against JEI 30.25.0.177 on MC 26.2 via mdl drove these fixes: Diagnosability: - Handler logs the FULL root cause chain on construct failure (was toString-only, hiding NoClassDefFoundError causes) New shims (constructor-chain driven): - ModLoadingContext (get(), getActiveContainer(), setActiveModId) - ModContainer stub with registerConfig(Type, IConfigSpec) - IConfigSpec marker interface - ModConfig abstract class with Type enum - ModConfigSpec full family: ConfigValue<T>/BooleanValue/IntValue/ LongValue/DoubleValue/EnumValue implements Supplier; Builder.define* returns typed holders seeded with defaults - IEventBus.addListener(boolean,...) and addListener(EventPriority, boolean,...) default overloads (linkage-compatible with mods compiled against real NeoForge) - Dist.isClient()/isServer() - IModBusEvent marker; FMLLifecycleEvent implements it - RegisterPayloadHandlersEvent, OnDatapackSyncEvent event classes - ModList facade (isLoaded/getLoadedMods/getAllScanData) - FMLPaths (GAMEDIR/CONFIGDIR/MODDIR resolution), FMLLoader stubs - neoforgespi ModFileScanData/AnnotationData interfaces JEI 30.x progression under Aprism: - v26.6: failed at CLASS LOAD (NoClassDefFoundError: api/distmarker/Dist) - v26.7-Alpha.4: full classload OK, constructor executes through config registration and network event subscription, stops at JEI-internal 'plugins must not be empty' (requires real annotation-scan of mod jars, i.e., Aprism known-issue #4 scope, not a shim gap) Build green, 11 tests pass.
1 parent 6349369 commit 88281b5

17 files changed

Lines changed: 703 additions & 17 deletions

File tree

src/main/java/com/aprism/refract/neoforge/NeoForgeEntrypointBridge.java

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,12 @@ private static String scanForMod(Path classFile, String modId) {
109109

110110
/**
111111
* Instantiates a NeoForge entrypoint class, injecting the given event bus.
112-
* Prefers a constructor accepting a single {@link IEventBus} argument; if
113-
* absent, falls back to the no-arg constructor.
112+
* Tries the known NeoForge constructor conventions in order:
113+
* <ol>
114+
* <li>{@code (IEventBus, Dist)} — common on NeoForge 21.x (e.g. JEI)</li>
115+
* <li>{@code (IEventBus)} — classic mod-scoped bus injection</li>
116+
* <li>{@code ()} — no-arg fallback</li>
117+
* </ol>
114118
*
115119
* @param clazz the {@code @Mod} entrypoint class
116120
* @param eventBus the mod-scoped event bus to inject
@@ -119,13 +123,21 @@ private static String scanForMod(Path classFile, String modId) {
119123
public static Object construct(Class<?> clazz, IEventBus eventBus) {
120124
try {
121125
try {
122-
var ctor = clazz.getDeclaredConstructor(IEventBus.class);
126+
var ctor = clazz.getDeclaredConstructor(IEventBus.class,
127+
net.neoforged.api.distmarker.Dist.class);
123128
ctor.setAccessible(true);
124-
return ctor.newInstance(eventBus);
129+
return ctor.newInstance(eventBus,
130+
net.neoforged.fml.loading.FMLEnvironment.dist);
125131
} catch (NoSuchMethodException ignored) {
126-
var ctor = clazz.getDeclaredConstructor();
127-
ctor.setAccessible(true);
128-
return ctor.newInstance();
132+
try {
133+
var ctor = clazz.getDeclaredConstructor(IEventBus.class);
134+
ctor.setAccessible(true);
135+
return ctor.newInstance(eventBus);
136+
} catch (NoSuchMethodException alsoIgnored) {
137+
var ctor = clazz.getDeclaredConstructor();
138+
ctor.setAccessible(true);
139+
return ctor.newInstance();
140+
}
129141
}
130142
} catch (ReflectiveOperationException e) {
131143
Throwable cause = e.getCause() != null ? e.getCause() : e;

src/main/java/com/aprism/refract/neoforge/NeoForgeEntrypointHandler.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ public boolean isExclusive() {
7878

7979
@Override
8080
public void invoke(LoadedModContainer container, AprismPhase phase) {
81+
// Feed the ModList shim so ModList.get().isLoaded(id) answers for
82+
// inter-mod presence checks (v26.7-Alpha.4).
83+
net.neoforged.fml.ModList.setLoadedMods(java.util.stream.Stream.concat(
84+
java.util.Arrays.stream(net.neoforged.fml.ModList.get().getLoadedMods().toArray(new String[0])),
85+
java.util.stream.Stream.of(container.getId()))
86+
.distinct()
87+
.toList());
8188
switch (phase) {
8289
case INIT -> initOrFireLifecycleEvent(container, new FMLCommonSetupEvent());
8390
case CLIENT -> fireLifecycleEvent(container, new FMLClientSetupEvent());
@@ -106,6 +113,9 @@ private void initOrFireLifecycleEvent(LoadedModContainer container, Object lifec
106113
return;
107114
}
108115
try {
116+
// ModLoadingContext.getActiveContainer() must answer THIS mod
117+
// while its constructor runs (v26.7-Alpha.4).
118+
net.neoforged.fml.ModLoadingContext.setActiveModId(container.getId());
109119
Class<?> clazz = Class.forName(modClasses.get(0), true,
110120
getClass().getClassLoader());
111121
NeoForgeEventBus bus = new NeoForgeEventBus();
@@ -117,8 +127,16 @@ private void initOrFireLifecycleEvent(LoadedModContainer container, Object lifec
117127
throw new RuntimeException("Failed to load NeoForge entrypoint for "
118128
+ container.getId(), e);
119129
} catch (RuntimeException e) {
130+
// Log the full cause chain (v26.7-Alpha.4): shim gaps surface
131+
// as NoClassDefFoundError deep in the cause tree; printing only
132+
// toString() hid the missing class name.
133+
Throwable cause = e;
134+
while (cause.getCause() != null && cause.getCause() != cause) {
135+
cause = cause.getCause();
136+
}
120137
LOG.warning("NeoForge mod " + container.getId()
121-
+ " failed to construct during INIT: " + e);
138+
+ " failed to construct during INIT: " + e
139+
+ " | root cause: " + cause);
122140
}
123141
} else {
124142
fireLifecycleEvent(container, lifecycleEvent);
Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,27 @@
11
package net.neoforged.api.distmarker;
22

33
/**
4-
* NeoForge API shim: distribution side enum. Many NeoForge mods reference
5-
* {@code Dist.CLIENT} or {@code Dist.DEDICATED_SERVER} in {@code @OnlyIn}
6-
* annotations and conditional logic. This shim provides the enum so that
7-
* genuine NeoForge mods can be loaded without the real NeoForge runtime.
4+
* NeoForge API shim: distribution side enum with side predicates. Many
5+
* NeoForge mods reference {@code Dist.CLIENT} / {@code Dist.DEDICATED_SERVER}
6+
* and call {@link #isClient()} / {@link #isServer()} in conditional logic.
87
*
98
* @author BlockConnect@StarsailsClover
109
*/
1110
public enum Dist {
1211
CLIENT,
13-
DEDICATED_SERVER
12+
DEDICATED_SERVER;
13+
14+
/**
15+
* @return true if this is the client distribution
16+
*/
17+
public boolean isClient() {
18+
return this == CLIENT;
19+
}
20+
21+
/**
22+
* @return true if this is the dedicated server distribution
23+
*/
24+
public boolean isServer() {
25+
return this == DEDICATED_SERVER;
26+
}
1427
}

src/main/java/net/neoforged/bus/api/IEventBus.java

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,6 @@
1313
* backs this shim with its own {@code NeoForgeEventBus}, so listeners
1414
* registered during mod construction are held by the branch extension.
1515
*
16-
* <p>Extracted from the Aprism core ({@code aprism-loader-core}) in
17-
* v26.0-Alpha.2 per the loader-support extraction.
18-
*
1916
* @author BlockConnect@StarsailsClover
2017
*/
2118
public interface IEventBus {
@@ -29,6 +26,37 @@ public interface IEventBus {
2926
*/
3027
<T> void addListener(Class<T> type, Consumer<T> consumer);
3128

29+
/**
30+
* Registers an event listener with the receiveCancelled flag (NeoForge
31+
* convention overload). Under Aprism the flag is accepted and ignored;
32+
* cancelled-event semantics are not modelled by the shim bus.
33+
*
34+
* @param receiveCancelled whether to receive cancelled events
35+
* @param type the event class
36+
* @param consumer the listener invoked when an event of {@code type} is posted
37+
* @param <T> the event type
38+
*/
39+
default <T> void addListener(boolean receiveCancelled, Class<T> type, Consumer<T> consumer) {
40+
addListener(type, consumer);
41+
}
42+
43+
/**
44+
* Registers an event listener with priority and cancelled-receipt flags
45+
* (full NeoForge convention overload, used by mods that wrap registration
46+
* in their own helpers). Priority ordering is not modelled by the shim
47+
* bus; the flag arguments are accepted and ignored.
48+
*
49+
* @param priority the listener priority
50+
* @param receiveCancelled whether to receive cancelled events
51+
* @param type the event class
52+
* @param consumer the listener invoked when an event of {@code type} is posted
53+
* @param <T> the event type
54+
*/
55+
default <T> void addListener(EventPriority priority, boolean receiveCancelled,
56+
Class<T> type, Consumer<T> consumer) {
57+
addListener(type, consumer);
58+
}
59+
3260
/**
3361
* Registers a generic event listener.
3462
*
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package net.neoforged.fml;
2+
3+
import java.util.logging.Logger;
4+
5+
import net.neoforged.fml.config.ModConfig;
6+
import net.neoforged.fml.config.ModConfig.Type;
7+
8+
/**
9+
* NeoForge API shim: per-mod container. The real container carries the mod
10+
* metadata, config registration, and extension points; under Aprism only the
11+
* surface mods touch during construction is provided. {@link #registerConfig}
12+
* accepts and logs the request without persisting anything.
13+
*
14+
* @author BlockConnect@StarsailsClover
15+
*/
16+
public class ModContainer {
17+
18+
private static final Logger LOG = Logger.getLogger(ModContainer.class.getName());
19+
20+
private final String modId;
21+
22+
/**
23+
* @param modId the mod id this container represents
24+
*/
25+
public ModContainer(String modId) {
26+
this.modId = modId;
27+
}
28+
29+
/**
30+
* @return the mod id
31+
*/
32+
public String getModId() {
33+
return modId;
34+
}
35+
36+
/**
37+
* Registers a config with the container (no-op under Aprism; logged).
38+
*
39+
* @param type config scope
40+
* @param spec the config spec
41+
*/
42+
public void registerConfig(Type type, net.neoforged.fml.config.IConfigSpec spec) {
43+
LOG.info("[shim] registerConfig(" + type + ") for " + modId
44+
+ " accepted (no persistence under Aprism)");
45+
}
46+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package net.neoforged.fml;
2+
3+
import java.nio.file.Path;
4+
import java.util.List;
5+
6+
/**
7+
* NeoForge API shim: the loaded-mod list facade. Mods query {@code ModList.get().isLoaded(id)}
8+
* for inter-mod presence checks. Under Aprism answers come from the ids
9+
* registered by the SupportExtension handler during mod discovery.
10+
*
11+
* @author BlockConnect@StarsailsClover
12+
*/
13+
public final class ModList {
14+
15+
private static volatile List<String> loadedIds = List.of();
16+
17+
private ModList() {
18+
}
19+
20+
/**
21+
* Returns the shared ModList instance.
22+
*
23+
* @return the singleton
24+
*/
25+
public static ModList get() {
26+
return Holder.INSTANCE;
27+
}
28+
29+
/**
30+
* Updates the known mod ids. Called by the Aprism handler after each
31+
* discovery pass.
32+
*
33+
* @param ids all discovered mod ids
34+
*/
35+
public static void setLoadedMods(List<String> ids) {
36+
loadedIds = List.copyOf(ids);
37+
}
38+
39+
/**
40+
* Checks whether a mod is loaded.
41+
*
42+
* @param id the mod id
43+
* @return true if loaded
44+
*/
45+
public boolean isLoaded(String id) {
46+
return loadedIds.contains(id);
47+
}
48+
49+
/**
50+
* Returns all loaded mod ids.
51+
*
52+
* @return unmodifiable list
53+
*/
54+
public List<String> getLoadedMods() {
55+
return loadedIds;
56+
}
57+
58+
/**
59+
* Returns annotation scan data across all mod files (empty under Aprism:
60+
* the shim runtime performs no annotation scan pass).
61+
*
62+
* @return unmodifiable empty list
63+
*/
64+
public List<net.neoforged.neoforgespi.language.ModFileScanData> getAllScanData() {
65+
return List.of();
66+
}
67+
68+
private static final class Holder {
69+
private Holder() {
70+
}
71+
72+
static final ModList INSTANCE = new ModList();
73+
}
74+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package net.neoforged.fml;
2+
3+
import net.neoforged.fml.config.ModConfig;
4+
5+
/**
6+
* NeoForge API shim: the mod loading context handed to mods for container
7+
* operations. The real FML provides one context per active mod; this shim
8+
* returns a shared instance whose {@link #getActiveContainer()} answers a
9+
* per-call stub backed by the most recently constructed mod id.
10+
*
11+
* @author BlockConnect@StarsailsClover
12+
*/
13+
public final class ModLoadingContext {
14+
15+
private static volatile String activeModId = "unknown";
16+
17+
private ModLoadingContext() {
18+
}
19+
20+
/**
21+
* Returns the shared context instance (real FML semantics).
22+
*
23+
* @return the singleton context
24+
*/
25+
public static ModLoadingContext get() {
26+
return Holder.INSTANCE;
27+
}
28+
29+
/**
30+
* Sets the id reported by {@link #getActiveContainer()}.
31+
* Called by the Aprism handler before constructing each mod.
32+
*
33+
* @param modId the currently-loading mod id
34+
*/
35+
public static void setActiveModId(String modId) {
36+
activeModId = modId;
37+
}
38+
39+
/**
40+
* Returns the active mod container stub.
41+
*
42+
* @return a minimal {@link ModContainer} for the active mod
43+
*/
44+
public ModContainer getActiveContainer() {
45+
return new ModContainer(activeModId);
46+
}
47+
48+
private static final class Holder {
49+
private Holder() {
50+
}
51+
52+
static final ModLoadingContext INSTANCE = new ModLoadingContext();
53+
}
54+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package net.neoforged.fml.config;
2+
3+
/**
4+
* NeoForge API shim: marker interface for config specs. The real interface
5+
* carries validation and reload contracts; under Aprism it exists only so
6+
* {@code ModContainer.registerConfig(Type, IConfigSpec)} resolves.
7+
*
8+
* @author BlockConnect@StarsailsClover
9+
*/
10+
public interface IConfigSpec {
11+
12+
/**
13+
* @return true if the spec is considered loaded (always true under Aprism)
14+
*/
15+
default boolean isLoaded() {
16+
return true;
17+
}
18+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package net.neoforged.fml.config;
2+
3+
/**
4+
* NeoForge API shim: config type holder. The real class carries file paths
5+
* and reload callbacks; under Aprism only the {@link Type} enum and a
6+
* constructor-compatible shape are provided.
7+
*
8+
* @author BlockConnect@StarsailsClover
9+
*/
10+
public abstract class ModConfig {
11+
12+
/** Config scope. */
13+
public enum Type {
14+
CLIENT,
15+
COMMON,
16+
SERVER,
17+
STARTUP
18+
}
19+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package net.neoforged.fml.event;
2+
3+
/**
4+
* NeoForge API shim: marker interface for mod-bus events. The real FML uses
5+
* it to validate that an event type belongs on the mod-scoped bus; under
6+
* Aprism it exists so event-class resolution succeeds.
7+
*
8+
* @author BlockConnect@StarsailsClover
9+
*/
10+
public interface IModBusEvent {
11+
}

0 commit comments

Comments
 (0)