diff options
Diffstat (limited to 'docs/handbook/forgewrapper/architecture.md')
| -rw-r--r-- | docs/handbook/forgewrapper/architecture.md | 1202 |
1 files changed, 1202 insertions, 0 deletions
diff --git a/docs/handbook/forgewrapper/architecture.md b/docs/handbook/forgewrapper/architecture.md new file mode 100644 index 0000000000..9f93c9dc76 --- /dev/null +++ b/docs/handbook/forgewrapper/architecture.md @@ -0,0 +1,1202 @@ +# ForgeWrapper Architecture + +## Table of Contents +1. [Class Hierarchy and Structure](#class-hierarchy-and-structure) +2. [Package Organization](#package-organization) +3. [Design Patterns Used](#design-patterns-used) +4. [Data Flow and Execution Model](#data-flow-and-execution-model) +5. [Thread Model and Concurrency](#thread-model-and-concurrency) +6. [Error Handling Strategy](#error-handling-strategy) +7. [Dual-Module System](#dual-module-system) +8. [ModuleUtil Implementation Details](#moduleutil-implementation-details) +9. [Class Diagram](#class-diagram) +10. [Reflection-Based Architecture](#reflection-based-architecture) + +## Class Hierarchy and Structure + +### Core Class Relationships + +``` +io.github.zekerzhayard.forgewrapper.installer package: +│ +├─ Main (public class) +│ ├─ main(String[]) : void - ENTRY POINT +│ └─ isFile(Path) : boolean +│ +├─ Installer (public class) +│ ├─ getData(File) : Map<String, Object> +│ ├─ install(File, File, File) : boolean +│ ├─ getWrapper(File) : InstallV1Wrapper +│ │ +│ ├─ InstallV1Wrapper (public static inner class extends InstallV1) +│ │ ├─ processors : Map<String, List<Processor>> +│ │ ├─ librariesDir : File +│ │ ├─ getProcessors(String) : List<Processor> +│ │ ├─ checkProcessorFiles(List<Processor>, Map<String, String>, File) : void +│ │ └─ setOutputs(Processor, Map<String, String>) : void +│ │ +│ └─ Version0 (public static class extends Version) +│ ├─ mainClass : String +│ ├─ arguments : Arguments +│ ├─ getMainClass() : String +│ ├─ getArguments() : Arguments +│ └─ Arguments (public static inner class) +│ ├─ jvm : String[] +│ └─ getJvm() : String[] +│ +├─ Bootstrap (public class) +│ └─ bootstrap(String[], String, String) : void throws Throwable +│ +└─ detector package: + │ + ├─ IFileDetector (public interface) + │ ├─ name() : String (abstract) + │ ├─ enabled(HashMap<String, IFileDetector>) : boolean (abstract) + │ ├─ getLibraryDir() : Path (default, can override) + │ ├─ getInstallerJar(String, String, String) : Path (default) + │ └─ getMinecraftJar(String) : Path (default) + │ + ├─ DetectorLoader (public class) + │ └─ loadDetector() : IFileDetector (static) + │ + └─ MultiMCFileDetector (public class implements IFileDetector) + ├─ libraryDir : Path + ├─ installerJar : Path + ├─ minecraftJar : Path + ├─ name() : String + ├─ enabled(HashMap<String, IFileDetector>) : boolean + ├─ getLibraryDir() : Path + ├─ getInstallerJar(String, String, String) : Path + └─ getMinecraftJar(String) : Path + +util package: +│ +└─ ModuleUtil (public class) + ├─ addModules(String) : void throws Throwable + ├─ addExports(List<String>) : void + ├─ addOpens(List<String>) : void + ├─ setupClassPath(Path, List<String>) : void throws Throwable + ├─ setupBootstrapLauncher(Class<?>) : Class<?> + ├─ getPlatformClassLoader() : ClassLoader + └─ (version-specific implementation) +``` + +### Field Definitions and Types + +#### Main.java Fields +None (purely algorithmic, no state) + +#### Installer.java Fields +```java +// Static field +private static InstallV1Wrapper wrapper; // Cached wrapper instance + +// Inner class InstallV1Wrapper fields +protected Map<String, List<Processor>> processors; // Cache of side-specific processors +protected File librariesDir; // Base libraries directory +protected String serverJarPath; // Server JAR path (inherited) + +// Inner class Version0 fields +protected String mainClass; // Forge bootstrap main class +protected Version0.Arguments arguments; // JVM arguments +protected String[] jvm; // JVM argument array (in Arguments) +``` + +#### Bootstrap.java Fields +None (purely algorithmic, no state) + +#### DetectorLoader.java Fields +None (stateless utility) + +#### MultiMCFileDetector.java Fields +```java +protected Path libraryDir; // Cached library directory (lazy-initialized) +protected Path installerJar; // Cached installer JAR path +protected Path minecraftJar; // Cached Minecraft JAR path +``` + +#### ModuleUtil.java Fields + +**Main version (Java 8)**: +```java +// No fields - all methods are no-op for Java 8 +``` + +**Jigsaw version (Java 9+)**: +```java +private final static MethodHandles.Lookup IMPL_LOOKUP; // Internal method handle lookup + +// TypeToAdd enum fields +EXPORTS { + private final MethodHandle implAddMH; // Invoke implAddExports() + private final MethodHandle implAddToAllUnnamedMH; // Invoke implAddExportsToAllUnnamed() +} +OPENS { + private final MethodHandle implAddMH; // Invoke implAddOpens() + private final MethodHandle implAddToAllUnnamedMH; // Invoke implAddOpensToAllUnnamed() +} + +// ParserData fields +class ParserData { + final String module; // Source module name + final String packages; // Package names to export/open + final String target; // Target module or ALL-UNNAMED +} +``` + +## Package Organization + +### Main Package Structure + +``` +io.github.zekerzhayard.forgewrapper.installer/ +├─ Main.java (entry point, 50 lines) +├─ Installer.java (installer integration, 200+ lines) +├─ Bootstrap.java (runtime configuration, 70 lines) +│ +├─ detector/ (file detection subsystem) +│ ├─ IFileDetector.java (interface contract, 90 lines) +│ ├─ DetectorLoader.java (SPI loader, 30 lines) +│ └─ MultiMCFileDetector.java (concrete impl, 40 lines) +│ +└─ util/ (utility functions) + └─ ModuleUtil.java (Java 8 version, 40 lines) + +jigsaw/src/main/java/io.../util/ +└─ ModuleUtil.java (Java 9+ version, 200+ lines) +``` + +### Resource Structure + +``` +src/main/resources/ +└─ META-INF/services/ + └─ io.github.zekerzhayard.forgewrapper.installer.detector.IFileDetector + (contains: io.github.zekerzhayard.forgewrapper.installer.detector.MultiMCFileDetector) + +jigsaw/src/main/resources/ +(empty - no SPI definitions) +``` + +### Build Output Structure + +The Gradle multi-release JAR structure (from `build.gradle` lines 47-54): + +``` +ForgeWrapper.jar/ +├─ io/github/zekerzhayard/forgewrapper/installer/ +│ ├─ Main.class (Java 8 version) +│ ├─ Installer.class including inner classes (Java 8 version) +│ ├─ Bootstrap.class (Java 8 version) +│ ├─ detector/ +│ │ ├─ IFileDetector.class (Java 8 version) +│ │ ├─ DetectorLoader.class (Java 8 version) +│ │ └─ MultiMCFileDetector.class (Java 8 version) +│ └─ util/ +│ └─ ModuleUtil.class (Java 8 stub version) +│ +├─ META-INF/versions/9/ +│ └─ io/github/zekerzhayard/forgewrapper/installer/ +│ └─ util/ +│ └─ ModuleUtil.class (Java 9+ Advanced version) +│ +└─ META-INF/services/ + └─ io.github.zekerzhayard.forgewrapper.installer.detector.IFileDetector +``` + +When running on Java 9+, the JVM automatically prefers classes in `META-INF/versions/9/` over base classes. + +## Design Patterns Used + +### 1. Service Provider Interface (SPI) Pattern + +**Location**: `DetectorLoader.java` and `IFileDetector` interface + +**Purpose**: Decouple file detection from launcher specifics. Allows multiple implementations without modifying core code. + +**Implementation**: +- `IFileDetector` defines the contract +- `MultiMCFileDetector` implements it +- `META-INF/services/` file registers the implementation +- `ServiceLoader.load()` discovers implementations at runtime + +**Benefits**: +- Custom launchers can provide their own detector by adding a JAR with SPI metadata +- ForgeWrapper doesn't need recompilation +- Multiple detectors can coexist; the `enabled()` logic selects the active one + +**Code Example** (from `DetectorLoader.java` lines 4-8): +```java +ServiceLoader<IFileDetector> sl = ServiceLoader.load(IFileDetector.class); +HashMap<String, IFileDetector> detectors = new HashMap<>(); +for (IFileDetector detector : sl) { + detectors.put(detector.name(), detector); +} +``` + +### 2. Strategy Pattern + +**Location**: `IFileDetector` implementations (current: `MultiMCFileDetector`) + +**Purpose**: Encapsulate different file location strategies for different launchers. + +**Implementation**: +- `IFileDetector` is the strategy interface defining `getLibraryDir()`, `getInstallerJar()`, `getMinecraftJar()` +- `MultiMCFileDetector` is the concrete strategy for MultiMC/MeshMC +- Future: `MeshMCFileDetector`, `ATLauncherFileDetector` could be added + +**Algorithm Variation**: File path construction differs: +- MultiMC: `libraries/net/neoforged/neoforge/VERSION/neoforge-VERSION-installer.jar` +- Another launcher: might use different structure + +**Code Example** (from `MultiMCFileDetector.java` lines 25-34): +```java +@Override +public Path getInstallerJar(String forgeGroup, String forgeArtifact, String forgeFullVersion) { + Path path = IFileDetector.super.getInstallerJar(...); + if (path == null) { + Path installerBase = this.getLibraryDir(); + for (String dir : forgeGroup.split("\\.")) + installerBase = installerBase.resolve(dir); + return installerBase.resolve(forgeArtifact).resolve(forgeFullVersion)...; + } + return path; +} +``` + +### 3. Template Method Pattern + +**Location**: `IFileDetector.getLibraryDir()` default implementation + +**Purpose**: Provide default library discovery algorithm while allowing overrides. + +**Implementation**: +- `IFileDetector` provides `getLibraryDir()` implementation (lines 34-63) +- `MultiMCFileDetector` calls `IFileDetector.super.getLibraryDir()` then caches (lines 19-23) +- Template steps: + 1. Check system property + 2. Find launcher JAR location + 3. Walk up directory tree to find `libraries` folder + 4. Return absolute path + +**Template Hook**: Subclasses can call `super.getLibraryDir()` to use default or override entirely + +**Code Example** (from `IFileDetector.java` lines 34-63): +```java +default Path getLibraryDir() { + String libraryDir = System.getProperty("forgewrapper.librariesDir"); + if (libraryDir != null) { + return Paths.get(libraryDir).toAbsolutePath(); + } + // ... walk classloader resources and find libraries/ +} +``` + +### 4. Wrapper/Adapter Pattern + +**Location**: `Installer.InstallV1Wrapper` class + +**Purpose**: Adapt Forge's `InstallV1` class to handle missing processor outputs. + +**Implementation**: +- Extends `InstallV1` (the wrapped class) +- Overrides `getProcessors()` to intercept processor handling +- Calls `super.getProcessors()` to get original implementation +- Wraps result with custom `checkProcessorFiles()` logic + +**Problem Solved**: Forge's installer expects all processor outputs to have SHA1 hashes, but some libraries may not. ForgeWrapper computes hashes for files that exist. + +**Code Example** (from `Installer.java` lines 42-52): +```java +@Override +public List<Processor> getProcessors(String side) { + List<Processor> processor = this.processors.get(side); + if (processor == null) { + checkProcessorFiles( + processor = super.getProcessors(side), // get original + super.getData("client".equals(side)), + this.librariesDir + ); + this.processors.put(side, processor); + } + return processor; +} +``` + +### 5. Facade Pattern + +**Location**: `Main.java` + +**Purpose**: Provide simplified interface to complex subsystems (Detector, Installer, Bootstrap, ModuleUtil). + +**Implementation**: +- `Main.main()` coordinates multiple subsystems +- Hides detector loading complexity +- Hides custom classloader creation +- Hides reflection-based Installer invocation +- Hides version-specific ModuleUtil calls + +**Client Perspective**: Launcher only needs to invoke: +```bash +java -cp ForgeWrapper.jar io.github.zekerzhayard.forgewrapper.installer.Main args... +``` + +**Code Example** (from `Main.java` lines 17-46): +```java +public static void main(String[] args) throws Throwable { + // Simplified entry point - complex logic hidden + IFileDetector detector = DetectorLoader.loadDetector(); + // ... use detector ... + // ... create classloader ... + // ... invoke Installer via reflection ... +} +``` + +### 6. Lazy Initialization Pattern + +**Location**: `MultiMCFileDetector` field initialization and `Installer` wrapper caching + +**Purpose**: Defer expensive operations until actually needed. + +**Implementation** (from `MultiMCFileDetector.java`): +```java +@Override +public Path getLibraryDir() { + if (this.libraryDir == null) { + this.libraryDir = IFileDetector.super.getLibraryDir(); // computed on first access + } + return this.libraryDir; +} +``` + +**Benefits**: +- If detector method isn't called, path resolution doesn't happen +- Caching prevents repeated expensive classloader searches +- Memory efficient for unused paths + +### 7. Builder Pattern (Implicit) + +**Location**: `Installer.Version0.Arguments` and `Installer.Version0` + +**Purpose**: Construct complex configuration objects from JSON. + +**Implementation**: +- `Version0.loadVersion()` (lines 209-214) deserializes from JSON using GSON +- JSON structure becomes nested object graph +- Fields are populated via reflection by GSON + +**Code Example** (from `Installer.java` lines 203-214): +```java +public static Version0 loadVersion(Install profile) { + try (InputStream stream = Util.class.getResourceAsStream(profile.getJson())) { + return Util.GSON.fromJson(new InputStreamReader(stream, StandardCharsets.UTF_8), Version0.class); + } catch (Throwable t) { + throw new RuntimeException(t); + } +} +``` + +## Data Flow and Execution Model + +### Execution Pipeline + +``` +Phase 1: Invocation + Launcher executes: java -cp ForgeWrapper.jar ... Main args... + │ + ├─ JVM starts ForgeWrapper JAR as classpath + ├─ Java locates Main.main(String[]) entry point + └─ Execution begins + +Phase 2: Argument Parsing (Main.java lines 17-27) + args[] contains: --fml.mcVersion 1.20.2 --fml.neoForgeVersion 20.2.20-beta ... + │ + ├─ Convert String[] to List for easier manipulation + ├─ Check for --fml.neoForgeVersion to detect NeoForge vs Forge + ├─ Search list for mcVersion index, forge version index + ├─ Extract and store values + └─ Result: 4 string variables containing version info + +Phase 3: Detector Selection (Main.java line 28) + Calls: IFileDetector detector = DetectorLoader.loadDetector() + │ + ├─ ServiceLoader scans classpath for IFileDetector implementations + ├─ Instantiates each implementation (no-arg constructor) + ├─ Builds HashMap of name() → detector instance + ├─ Iterates to find exactly one with enabled(others) == true + ├─ Validates no conflicts + └─ Returns single enabled detector (MultiMCFileDetector) + +Phase 4: File Location (Main.java lines 30-36) + Calls detector methods to locate files: + │ + ├─ detector.getInstallerJar(forgeGroup, forgeArtifact, forgeFullVersion) + │ └─ Returns Path or null + ├─ Validates with isFile(Path) + ├─ If not found, throws "Unable to detect the forge installer!" + │ + ├─ detector.getMinecraftJar(mcVersion) + │ └─ Returns Path or null + ├─ Validates with isFile(Path) + └─ If not found, throws "Unable to detect the Minecraft jar!" + +Phase 5: Custom Classloader Creation (Main.java lines 38-41) + Creates URLClassLoader with: + │ + ├─ URLClassLoader.newInstance(new URL[] { + │ Main.class.getProtectionDomain().getCodeSource().getLocation(), // ForgeWrapper.jar + │ installerJar.toUri().toURL() // forge-installer.jar + │ }, ModuleUtil.getPlatformClassLoader()) + │ + └─ Result: custom classloader with both JARs + +Phase 6: Installer Invocation via Reflection (Main.java lines 42-43) + Loads Installer from custom classloader: + │ + ├─ Class<?> installer = ucl.loadClass("io.github.zekerzhayard.forgewrapper.installer.Installer") + ├─ installer.getMethod("getData", File.class) gets method reference + │ + ├─ Invokes: installer.getMethod("getData", File.class) + │ .invoke(null, detector.getLibraryDir().toFile()) + │ + └─ Returns: Map<String, Object> with mainClass, jvmArgs, extraLibraries + +Phase 7: Bootstrap Configuration (Main.java line 44) + Calls: Bootstrap.bootstrap((String[]) data.get("jvmArgs"), ...) + │ + ├─ Replaces placeholders in JVM arguments + ├─ Removes NewLaunch.jar from classpath + ├─ Extracts module paths from arguments + ├─ Calls ModuleUtil.addModules(), addExports(), addOpens() + ├─ Sets up system properties + └─ Returns normally (or logs error if Java 8) + +Phase 8: Installation Execution (Main.java line 47) + Invokes: installer.getMethod("install", File.class, File.class, File.class) + .invoke(null, libraryDir, minecraftJar, installerJar) + │ + ├─ Installer.install() creates InstallV1Wrapper + ├─ Loads installation profile from installer JAR + ├─ Gets processors via getProcessors(side) + ├─ InstallV1Wrapper.checkProcessorFiles() computes missing hashes + ├─ PostProcessors.process() executes installation + ├─ Libraries are downloaded or linked + ├─ Mod loaders are configured + └─ Returns boolean success + +Phase 9: Classloader Setup (Main.java line 50) + Calls: ModuleUtil.setupClassPath(detector.getLibraryDir(), extraLibraries) + │ + ├─ Reflects into sun.misc.Unsafe to access internal classloader + ├─ Gets URLClassPath for system classloader + ├─ Adds URLs for all extra libraries + └─ System classloader now has all library JARs in path + +Phase 10: Module System Finalization (Main.java line 51) + Calls: Class<?> mainClass = ModuleUtil.setupBootstrapLauncher(...) + │ + ├─ Java 8: returns mainClass unchanged (no-op) + ├─ Java 9+: performs final module system configuration + └─ Result: ClassLoader-ready class + +Phase 11: Forge Invocation (Main.java line 52) + Calls: mainClass.getMethod("main", String[].class) + .invoke(null, new Object[] {args}) + │ + ├─ Forge bootstrap launcher main() method is invoked + ├─ Original launcher arguments are passed through + ├─ Forge initializes mod framework + ├─ Minecraft main is invoked + └─ Game runs under mod loader +``` + +### Data Transformation Chain + +``` +Original Arguments from Launcher + ↓ +Parsed into: { mcVersion, forgeVersion, forgeGroup, isNeoForge, ... } + ↓ +Detector#get*() methods + ↓ +File Path objects + ↓ +URLClassLoader creation + ↓ +Installer class loading and invocation + ↓ +Installation Profile (JSON deserialization) + ↓ +Version0 object with mainClass, Arguments + ↓ +JVM Arguments string array with placeholders + ↓ +Bootstrap placeholder replacement + ↓ +Modified JVM Arguments suitable for Forge + ↓ +Module paths extracted from arguments + ↓ +ModuleUtil#addModules() invocations + ↓ +Modified Java module layer + ↓ +Final Forge main class invocation + ↓ +Running Minecraft with mods +``` + +## Thread Model and Concurrency + +### Single-Threaded Execution Model + +ForgeWrapper operates entirely in a single thread (the launcher thread): + +1. **Main thread**: Entry point and execution coordinator +2. **No worker threads**: ForgeWrapper itself doesn't spawn threads +3. **Blocking operations**: All I/O is synchronous and blocking +4. **Sequential execution**: Phases execute in strict order + +**Code Flow**: +```java +Main.main(args) // launcher thread + ├─ DetectorLoader.loadDetector() // synchronous + └─ detector.getInstallerJar() // synchronous + └─ detector.getMinecraftJar() // synchronous + └─ URLClassLoader creation // synchronous + └─ Installer.getData() // synchronous + └─ Bootstrap.bootstrap() // synchronous + └─ ModuleUtil methods // synchronous + └─ Installer.install() // synchronous (Forge may download, happens here) + └─ mainClass.main(args) // delegates to Forge (Forge may be multi-threaded) +``` + +### Concurrency Considerations + +**Not Thread-Safe**: +- `MultiMCFileDetector` field caching (lines 7-9 in `MultiMCFileDetector.java`) is not synchronized +- If multiple threads called detector methods, race conditions could occur +- However, this is not a concern because ForgeWrapper is designed as single-shot execution + +**Why Single-Threaded Design**: +1. Launchers invoke ForgeWrapper once, get result, and move on +2. No need for concurrent execution +3. Simplifies error handling (no thread coordination needed) +4. Reduces memory overhead + +**Installer Thread Safety**: +- `Installer.wrapper` static field (line 23 in `Installer.java`) is cached but not synchronized +- Safe because it's only accessed during single-shot initialization +- Even if multiple threads accessed, worst case is re-creation (not data corruption) + +### Reflection and ClassLoader Thread Safety + +The custom URLClassLoader (Main.java line 38-41) operates in a thread-safe manner: +- ClassLoader has internal synchronization for class loading +- Multiple threads can load classes from same classloader +- However, ForgeWrapper doesn't use this capability + +**ModuleUtil Reflection Safety** (jigsaw version): +- Uses `sun.misc.Unsafe` for low-level access +- `Unsafe.putObject()` operations are atomic at hardware level +- Module layer is designed to be modified during startup only +- No concurrent access to module layer during bootstrap + +## Error Handling Strategy + +### Error Scenarios and Recovery + +#### Scenario 1: Detector Not Found + +**Code** (from `DetectorLoader.java` line 21): +```java +if (temp == null) { + throw new RuntimeException("No file detector is enabled!"); +} +``` + +**When**: No implementations of `IFileDetector` found in `META-INF/services/` + +**Recovery**: None - launcher must ensure ForgeWrapper JAR is complete and classpath includes implementations + +**User Impact**: Launcher cannot start; must fix installation + +#### Scenario 2: Multiple Detectors Enabled + +**Code** (from `DetectorLoader.java` lines 18-19): +```java +} else if (detector.getValue().enabled(others)) { + throw new RuntimeException("There are two or more file detectors are enabled! ..."); +} +``` + +**When**: Two implementations both return `true` from `enabled()` + +**Recovery**: None - one detector must disable itself + +**User Impact**: Configuration error; launcher installation needs repair + +#### Scenario 3: Installer JAR Not Found + +**Code** (from `Main.java` line 32): +```java +if (!isFile(installerJar)) { + throw new RuntimeException("Unable to detect the forge installer!"); +} +``` + +**When**: +- `detector.getInstallerJar()` returns null +- Or returns path to non-existent file +- Or returns directory instead of file + +**Recovery Hints**: +- Check ForgeWrapper properties: `-Dforgewrapper.installer=/path/to/jar` +- Verify installer JAR exists at expected MultiMC paths +- Ensure forge version is correctly specified + +**User Impact**: Modded instance cannot launch + +#### Scenario 4: Minecraft JAR Not Found + +**Code** (from `Main.java` line 36): +```java +if (!isFile(minecraftJar)) { + throw new RuntimeException("Unable to detect the Minecraft jar!"); +} +``` + +**When**: +- Vanilla Minecraft not installed in expected location +- Path resolution failed + +**Recovery Hints**: +- Manually specify: `-Dforgewrapper.minecraft=/path/to/1.20.2.jar` +- Ensure vanilla Minecraft is installed first + +**User Impact**: Modded instance cannot launch + +#### Scenario 5: Library Directory Not Resolvable + +**Code** (from `IFileDetector.java` line 60): +```java +throw new UnsupportedOperationException( + "Could not detect the libraries folder - it can be manually specified with `-Dforgewrapper.librariesDir=`" +); +``` + +**When**: +- Classloader resources don't contain expected Forge classes +- Libraries folder walking fails +- ClassLoader inspection fails + +**Recovery**: +- Set property: `-Dforgewrapper.librariesDir=/home/user/.minecraft/libraries` +- Standard recovery path is well-documented + +**User Impact**: ForgeWrapper fails at startup; user must configure manually + +#### Scenario 6: Installation Failed + +**Code** (from `Main.java` line 47): +```java +if (!((boolean) installer.getMethod("install", ...) + .invoke(null, ...))) { + return; // Silent failure +} +``` + +**When**: `PostProcessors.process()` returns false + +**Recovery**: None built-in + +**User Impact**: +- Mod loaders not installed +- Minecraft may fail to start +- User must reinstall or redownload + +#### Scenario 7: Bootstrap Configuration Error + +**Code** (from `Main.java` lines 44-46): +```java +try { + Bootstrap.bootstrap(...); +} catch (Throwable t) { + t.printStackTrace(); // Error logged but execution continues +} +``` + +**When**: JVM argument parsing fails or module configuration fails + +**Recovery**: Execution continues regardless + +**Rationale**: Bootstrap errors should not prevent Forge initialization attempt + +**User Impact**: Depends on specific error; may cause module system misconfiguration + +#### Scenario 8: Reflection Error (ModuleUtil) + +**Code** (from jigsaw `ModuleUtil.java` line 35): +```java +} catch (Throwable t) { + throw new RuntimeException(t); +} +``` + +**When**: Module system manipulation via reflection fails + +**Recovery**: None - throws RuntimeException + +**User Impact**: Java 9+ users cannot run modded Minecraft; critical error + +## Dual-Module System + +ForgeWrapper uses a sophisticated dual-module architecture to support both Java 8 and Java 9+: + +### File Organization + +**Main project** (`src/main/`): +- Targets Java 8 (minimum compatibility) +- Contains all public APIs +- `ModuleUtil.java` is stub implementation with all methods as no-ops +- This is the "default" version used on Java 8 + +**Jigsaw subproject** (`jigsaw/src/main/`): +- Targets Java 9+ +- Compiles with `-targetCompatibility 9` +- Contains advanced `ModuleUtil.java` with full module system support +- Only compiled if JDK 9+ is available for compilation +- Classes packaged into `META-INF/versions/9/` in final JAR + +### Build Configuration + +**From `build.gradle` lines 23-27**: +```gradle +configurations { + multirelase { + implementation.extendsFrom multirelase // typo in original, should be multirelease + } +} + +dependencies { + multirelase project(":jigsaw") // jigsaw output goes into multirelease config +} +``` + +**From `build.gradle` lines 47-54**: +```gradle +jar { + into "META-INF/versions/9", { + from configurations.multirelase.files.collect { + zipTree(it) + } + } +} +``` + +**From `jigsaw/build.gradle` lines 14-23**: +```gradle +compileJava { + if (JavaVersion.current() < JavaVersion.VERSION_1_9) { + javaCompiler = javaToolchains.compilerFor { + languageVersion = JavaLanguageVersion.of(9) + } + } + sourceCompatibility = 9 + targetCompatibility = 9 +} +``` + +### Runtime Behavior + +**On Java 8**: +``` +JVM loads ForgeWrapper.jar + ├─ Main class path: io/github/zekerzhayard/forgewrapper/installer/Main.class (Java 8 version) + ├─ ModuleUtil path: io/.../util/ModuleUtil.class (Java 8 stub) + │ ├─ addModules() → no-op + │ ├─ addExports() → no-op + │ ├─ addOpens() → no-op + │ └─ getPlatformClassLoader() → returns null + └─ Execution proceeds with no module system features +``` + +**On Java 9+**: +``` +JVM loads ForgeWrapper.jar + ├─ Main class path: io/github/zekerzhayard/forgewrapper/installer/Main.class (Java 8 version) + │ (same binary works on both versions) + ├─ ModuleUtil path: Preferentially loads from META-INF/versions/9/ + │ io/.../util/ModuleUtil.class (Java 9+ advanced version) + │ ├─ addModules() → complex module layer manipulation + │ ├─ addExports() → module export configuration + │ ├─ addOpens() → module open configuration + │ └─ getPlatformClassLoader() → returns actual PlatformClassLoader + └─ Execution proceeds with full module system support +``` + +### Manifest Configuration + +**From `build.gradle` lines 50-51**: +```gradle +"Multi-Release": "true", +``` + +This manifest attribute signals to the JVM that the JAR contains multi-release content. Without this, the JVM ignores `META-INF/versions/9/` directory. + +### Why Dual Module System Is Necessary + +**Java 8** has no module system: +- No `java.lang.module` package +- No `sun.misc.Unsafe` reflective access patterns for modules +- Runtime module manipulation not possible or necessary +- Stub implementation sufficient + +**Java 9+** requires module configuration: +- Minecraft uses JPMS +- Forge uses module system features +- Need to add modules at runtime +- Need to configure exports and opens +- Requires low-level JDK manipulation + +**Forward Compatibility**: +- Java 8 code can run unchanged on Java 9+ +- Java 9+ code cannot run on Java 8 (compilation fails) +- Multi-release JAR solves this elegantly + +## ModuleUtil Implementation Details + +### Java 8 Version (Simple) + +Located in `src/main/java/...ModuleUtil.java` (40 lines total): + +**Purpose**: Provide stubs that do nothing, allowing the same API to be called regardless of Java version. + +**Methods**: + +```java +public static void addModules(String modulePath) { + // nothing to do with Java 8 +} +``` +No-op: Java 8 has no module system; modulePath argument is ignored. + +```java +public static void setupClassPath(Path libraryDir, List<String> paths) throws Throwable { + Method addURLMethod = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); + addURLMethod.setAccessible(true); + for (String path : paths) { + addURLMethod.invoke(ClassLoader.getSystemClassLoader(), + libraryDir.resolve(path).toUri().toURL()); + } +} +``` +Functional: Uses reflection to access Java 8's URLClassLoader.addURL() to dynamically add JARs to the system classloader. + +**Pattern**: All other methods follow same no-op structure for Java 8 compatibility. + +### Java 9+ Version (Complex) + +Located in `jigsaw/src/main/java/...ModuleUtil.java` (200+ lines): + +**Prerequisite Knowledge**: +- Java module system basics: modules named, exports controlled +- JDK internals: MethodHandles, Unsafe, reflection APIs +- Module layer: boot layer is the root layer + +**Key Insight** (line 32): +```java +private final static MethodHandles.Lookup IMPL_LOOKUP = getImplLookup(); +``` + +The `IMPL_LOOKUP` is a special method handle lookup obtained via `sun.misc.Unsafe`. This provides access to internal JDK classes not normally accessible through reflection. + +**The addModules() Method (lines 52-137)**: This is the most complex method. Step-by-step: + +1. **Module Discovery** (lines 56-60): +```java +ModuleFinder finder = ModuleFinder.of( + Stream.of(modulePath.split(File.pathSeparator)) + .map(Paths::get) + .filter(p -> ...) // exclude existing modules + .toArray(Path[]::new) +); +``` +Creates a ModuleFinder for the provided module path, excluding modules that already exist. + +2. **Module Loading** (lines 61-63): +```java +MethodHandle loadModuleMH = IMPL_LOOKUP.findVirtual( + Class.forName("jdk.internal.loader.BuiltinClassLoader"), + "loadModule", + MethodType.methodType(void.class, ModuleReference.class) +); +``` +Obtains a method handle for `BuiltinClassLoader.loadModule()`, which is an internal JDK method. + +3. **Resolution and Configuration** (lines 66-79): +```java +Configuration config = Configuration.resolveAndBind( + finder, List.of(ModuleLayer.boot().configuration()), finder, roots +); +``` +Creates a module configuration graph for the new modules, bound to the boot module layer. + +4. **Graph Manipulation** (lines 81-118): +```java +HashMap<ResolvedModule, Set<ResolvedModule>> graphMap = ... +for (Map.Entry<ResolvedModule, Set<ResolvedModule>> entry : graphMap.entrySet()) { + cfSetter.invokeWithArguments(entry.getKey(), ModuleLayer.boot().configuration()); + ... +} +``` +Modifies the module dependency graph to integrate new modules into the boot layer's configuration. + +5. **Module Definition** (lines 120-123): +Invokes internal `Module.defineModules()` to actually create the module objects in the boot layer. + +6. **Read Edge Setup** (lines 131-137): +```java +implAddReadsMH.invokeWithArguments(module, bootModule); +``` +Establishes "reads" relationships between new modules and existing boot modules, allowing them to see each other. + +**The addExports() Method (lines 139-140)**: +```java +public static void addExports(List<String> exports) { + TypeToAdd.EXPORTS.implAdd(exports); +} +``` +Delegates to enum that uses method handles to call module export methods. + +**The addOpens() Method (lines 143-144)**: +```java +public static void addOpens(List<String> opens) { + TypeToAdd.OPENS.implAdd(opens); +} +``` +Similar to addExports but for "opens" (deep reflection access). + +**The parseModuleExtra() Method (lines 173-186)**: +Parses format: `<module>/<package>=<target>` + +Example: `java.base/java.lang=ALL-UNNAMED` + +This means: Module `java.base` should export/open package `java.lang` to all unnamed modules. + +## Class Diagram + +ASCII UML representation of full architecture: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ <<entry-point>> │ +│ Main │ +├─────────────────────────────────────────────────────────────────┤ +│ - (no fields, stateless) │ +├─────────────────────────────────────────────────────────────────┤ +│ + main(arguments: String[]): void (static) │ +│ + isFile(path: Path): boolean (private static) │ +└────────────────┬──────────────────────────────────────────────┬─┘ + │ invokes creates │ + │ │ + ▼ ▼ + ┌────────────────────────┐ ┌──────────────────────────┐ + │ DetectorLoader │ │ URLClassLoader │ + ├────────────────────────┤ ├──────────────────────────┤ + │ - (no fields) │ │ - custom classloader │ + ├────────────────────────┤ ├──────────────────────────┤ + │ + loadDetector() │────┐ │ loadClass(name) │ + │ : IFileDetector │ │ └──────────────────────────┘ + └────────────────────────┘ │ + ▲ │ + uses SPI │ ┌──────▼─────────────────┐ + │ │ <<interface>> │ + ┌────────────┴───────────IFileDetector │ + │ │ ├────────────────────────┤ + │ │ │ + name(): String │ + │ returns │ │ + enabled(...):bool │ + │ │ │ + getLibraryDir():Path │ + │ └────────→ │ + getInstallerJar(...):Path + │ │ + getMinecraftJar(...):Path + │ └────────────────────────┘ + │ ▲ + │ │ implements + │ │ + │ ┌──────┴──────────────────────┐ + │ │ MultiMCFileDetector │ + │ ├─────────────────────────────┤ + │ │ - libraryDir: Path (cached) │ + │ │ - installerJar: Path │ + │ │ - minecraftJar: Path │ + │ ├─────────────────────────────┤ + │ │ + name() │ + │ │ + enabled(...) │ + │ │ + getLibraryDir() │ + │ │ + getInstallerJar(...): Path + │ │ + getMinecraftJar(...) │ + │ └─────────────────────────────┘ + │ + └────────────────────────────────────────────────────────┘ + + ┌─────────────────────────────────────────────────────┐ + │ Main calls Bootstrap.bootstrap(...) │ + └─────────────────────┬───────────────────────────────┘ + │ + ▼ + ┌────────────────────────────┐ + │ Bootstrap │ + ├────────────────────────────┤ + │ - (no fields) │ + ├────────────────────────────┤ + │ + bootstrap(jvmArgs,...): │ + │ void (static throws) │ + └────┬───────────────────────┘ + │ invokes + ▼ + ┌─────────────────────────────────┐ + │ ModuleUtil │ + ├─────────────────────────────────┤ + │ - (version-specific) │ + ├─────────────────────────────────┤ + │ + addModules(path): void │ + │ + addExports(list): void │ + │ + addOpens(list): void │ + │ + setupClassPath(...): void │ + │ + setupBootstrapLauncher(...): │ + │ Class<?> │ + │ + getPlatformClassLoader(): │ + │ ClassLoader │ + └─────────────────────────────────┘ + (Java 8: all no-op) + (Java 9+: advanced module manipulation) + + ┌──────────────────────────────────────────┐ + │ URLClassLoader loads Installer via │ + │ reflection │ + └──────────────────────────┬───────────────┘ + │ + ▼ + ┌────────────────────────────────────┐ + │ Installer │ + ├────────────────────────────────────┤ + │- wrapper: InstallV1Wrapper (static)│ + ├────────────────────────────────────┤ + │+ getData(File): Map │ + │+ install(File,File,File): boolean │ + │- getWrapper(File):InstallV1Wrapper │ + └────────────┬─────────────────────┬─┘ + │ │ + ┌────────────────┘ └────────────┐ + │ │ + ▼ ▼ + ┌────────────────────────────────┐ ┌──────────────────────────────┐ + │ InstallV1Wrapper │ │ Version0 │ + │ extends InstallV1 │ │ extends Version │ + ├────────────────────────────────┤ ├──────────────────────────────┤ + │- processors: Map<...> │ │- mainClass: String │ + │- librariesDir: File │ │- arguments: Arguments │ + ├────────────────────────────────┤ ├──────────────────────────────┤ + │+ getProcessors(side): List │ │+ getMainClass(): String │ + │- checkProcessorFiles(...) │ │+ getArguments(): Arguments │ + │- setOutputs(...) │ │+ loadVersion(...):Version0 │ + └────────────────────────────────┘ └──────────────────────────────┘ + │ + │ contains + ▼ + ┌──────────────────────┐ + │ Arguments │ + ├──────────────────────┤ + │- jvm: String[] │ + ├──────────────────────┤ + │+ getJvm(): String[] │ + └──────────────────────┘ +``` + +## Reflection-Based Architecture + +ForgeWrapper uses reflection extensively to provide a flexible, decoupled architecture. Here are key reflection patterns: + +### Pattern 1: Dynamic Class Loading (Main.java lines 42-43) + +```java +Class<?> installer = ucl.loadClass("io.github.zekerzhayard.forgewrapper.installer.Installer"); +``` + +**Why**: Avoids compile-time dependency on installer JAR. The Installer class is only loaded from the custom classloader that includes the installer JAR. + +**Benefits**: +- ForgeWrapper source doesn't need Installer on buildpath +- Different Installer versions can be used without rebuilding ForgeWrapper +- Decouples ForgeWrapper versioning from Forge versioning + +### Pattern 2: Method Invocation via Reflection (Main.java lines 43-44) + +```java +Map<String, Object> data = (Map<String, Object>) installer + .getMethod("getData", File.class) + .invoke(null, detector.getLibraryDir().toFile()); +``` + +**Why**: Main.java doesn't have the Installer class available at compile time. + +**Mechanics**: +- `getMethod("getData", File.class)` finds method that takes File parameter +- `.invoke(null, ...)` invokes it (null = static method, no instance needed) +- Result cast to Map<String, Object> + +### Pattern 3: Unsafe Field Access (jigsaw ModuleUtil.java lines 33-48) + +```java +Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe"); +unsafeField.setAccessible(true); +Unsafe unsafe = (Unsafe) unsafeField.get(null); + +Field implLookupField = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP"); +return (MethodHandles.Lookup) unsafe.getObject( + unsafe.staticFieldBase(implLookupField), + unsafe.staticFieldOffset(implLookupField) +); +``` + +**Why**: Need access to internal `IMPL_LOOKUP` field that's normally inaccessible. + +**What It Does**: Uses Unsafe to reflectively read a private static field from MethodHandles.Lookup, obtaining the internal "implementation lookup" that has special JDK permissions. + +**Risk**: Relies on internal JDK implementation; may break in future Java versions. + +### Pattern 4: Method Handle Creation (TypeToAdd enum constructors) + +```java +this.implAddMH = IMPL_LOOKUP.findVirtual(Module.class, "implAddExports", + MethodType.methodType(void.class, String.class, Module.class)); +``` + +**Why**: Method handles provide high-performance reflection; used for repeated invocations. + +**Benefit**: Once created, method handles are cached. Repeated calls are very fast. + +### Pattern 5: Field Reflection for Modification (Installer.java lines 185-192) + +```java +private static Field outputsField; +private static void setOutputs(Processor processor, Map<String, String> outputs) { + try { + if (outputsField == null) { + outputsField = Processor.class.getDeclaredField("outputs"); + outputsField.setAccessible(true); + } + outputsField.set(processor, outputs); + } catch (Throwable t) { + throw new RuntimeException(t); + } +} +``` + +**Why**: Need to modify private field `outputs` on Processor objects. + +**Pattern**: Lazy-initialize field reference, cache it, reuse for all modifications. + +This comprehensive architectural documentation provides deep understanding of ForgeWrapper's design, pattern usage, and implementation strategy. +``` + |
