diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e2f61f6..52ce2b8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,22 +1,37 @@ # Contributing to Weave Loader -Thank you for your interest in contributing to Weave Loader! +Thanks for your interest in contributing. This guide covers how the project is organized, how to build it, and what conventions to follow. -## Project Structure +## Project Overview -- **WeaveLoader.Launcher/** -- C# console app that launches the game and injects the runtime DLL -- **WeaveLoaderRuntime/** -- C++ DLL injected into the game process (hooks, .NET hosting, native exports) -- **WeaveLoader.Core/** -- C# assembly loaded inside the game process (mod discovery, lifecycle management) -- **WeaveLoader.API/** -- C# class library that mod authors reference (IMod, Registry, Events) -- **ExampleMod/** -- Sample mod demonstrating the API +Weave Loader is split into four main components plus an example mod: + +| Component | Language | Purpose | +|-----------|----------|---------| +| **WeaveLoader.Launcher** | C# | Console app that launches the game suspended and injects the runtime DLL | +| **WeaveLoaderRuntime** | C++ | DLL injected into the game process; handles PDB parsing, hooking, .NET hosting, and all native game interaction | +| **WeaveLoader.Core** | C# | Loaded inside the game process by the runtime; discovers mods, manages lifecycle | +| **WeaveLoader.API** | C# | Public API that mod authors reference; contains registries, events, and the `IMod` interface | +| **ExampleMod** | C# | Working sample mod that registers a block, item, recipe, and event handler | + +### Communication Flow + +``` +Launcher ──(inject)──> Runtime (C++) ──(hostfxr)──> Core (C#) ──(reflection)──> Mods + ▲ │ + └──────────── P/Invoke (NativeExports) ─────────────────────┘ +``` + +Mods call `Registry.Block.Register(...)` in C#, which calls `NativeInterop.native_register_block(...)` via P/Invoke, which arrives in `NativeExports.cpp` in the C++ runtime, which calls `GameObjectFactory::CreateTile(...)` to construct a real game `Tile` object through resolved PDB symbols. ## Building ### Prerequisites -- Visual Studio 2022+ with C++ Desktop Development and .NET 8 workloads +- Visual Studio 2022+ with **C++ Desktop Development** and **.NET 8** workloads - CMake 3.24+ - .NET 8.0 SDK +- The target game must have been compiled with PDB generation (`/Zi` or `/ZI`) ### C++ Runtime @@ -26,23 +41,101 @@ cmake -B build -A x64 cmake --build build --config Release ``` +The output is `WeaveLoaderRuntime.dll` in `build/Release/`. + ### C# Projects ```bash -dotnet build Weave Loader.sln -c Release +dotnet build WeaveLoader.sln -c Debug ``` -## Guidelines +All outputs go to `build/`. -- Keep game source modifications at zero -- all integration is via injection and hooking -- Test hooks against the latest game build with PDB symbols -- Use namespaced string IDs (`"modid:name"`) for all registered content -- Catch exceptions from mod code to prevent crashes -- Document public API methods with XML doc comments +### Full Clean Build + +```bash +dotnet clean WeaveLoader.sln +cd WeaveLoaderRuntime && cmake --build build --target clean && cd .. +dotnet build WeaveLoader.sln -c Debug +cd WeaveLoaderRuntime && cmake --build build --config Release +``` + +## Development Guidelines + +### Core Principles + +1. **No game source modifications.** All interaction with the game is through DLL injection, PDB symbol resolution, and MinHook detours. Never suggest changes to the game's own source code. + +2. **Fix root causes, not symptoms.** If something crashes, understand why at the engine level. Don't add null checks around broken pointers -- resolve the actual construction or lifecycle issue. + +3. **Use PDB symbols, not hardcoded offsets.** Game updates change binary layouts. All function pointers must be resolved from the PDB at runtime using their decorated (mangled) names. + +### C++ Runtime (`WeaveLoaderRuntime/`) + +- **Symbol resolution**: Add new game functions in `SymbolResolver.cpp`. Use `llvm-pdbutil dump --publics` or the built-in `PdbParser::DumpMatching()` to find the correct mangled name. MSVC mangles are sensitive to calling convention, const qualifiers, and parameter types -- double-check them. + +- **Hooking**: New hooks go in `GameHooks.cpp` (implementation) and `HookManager.cpp` (installation). Always store the original function pointer so the hook can call through to the original. + +- **Native exports**: Functions called by C# are declared in `NativeExports.cpp` with `extern "C" __declspec(dllexport)`. Keep signatures simple (primitive types, `const char*`). Add corresponding `[DllImport]` entries in `WeaveLoader.API/NativeInterop.cs`. + +- **Logging**: Use `LogUtil::Log(message)` for normal output (goes to `weaveloader.log`). Never use `printf` or `std::cout`. + +- **Memory safety**: The game's memory layout is discovered at runtime. Always validate pointers before dereferencing. Add diagnostic logging when traversing game structures. + +### C# API (`WeaveLoader.API/`) + +- **Backward compatibility**: The API is what mod authors compile against. Avoid breaking changes to public interfaces. New features should be additive. + +- **Thread safety**: `OnTick()` runs on the game thread. Registration methods are called during init. The API's internal P/Invoke calls are not thread-safe by design. + +- **Naming**: Use `PascalCase` for public members and `camelCase` for parameters. Registry methods follow the pattern `Registry..Register(id, properties)`. + +- **Identifiers**: All content IDs use the `"namespace:path"` format. The `Identifier` class handles parsing. Default namespace is `"minecraft"`. + +### C# Core (`WeaveLoader.Core/`) + +- **Mod isolation**: Each mod's lifecycle method is wrapped in try/catch in `ModManager.cs`. A failing mod should never crash the loader or other mods. + +- **Discovery**: Mods are found by scanning `mods/*/` for assemblies containing types that implement `IMod` and have the `[Mod]` attribute. + +### Testing + +- Always test DLL injection on a real game build with PDB symbols available. +- Verify that new hooks don't break existing functionality (creative inventory, item names, textures). +- Check `logs/crash.log` after crashes -- the symbolicated stack trace usually points directly to the issue. +- Test with multiple mods loaded to ensure isolation works. + +## Adding a New Registry Type + +To add support for a new game object type (e.g., particles): + +1. **PDB**: Find the constructor and relevant setters using `PdbParser::DumpMatching("ParticleType")` or `llvm-pdbutil` +2. **SymbolResolver**: Add the mangled symbol constants and resolution in `SymbolResolver.cpp` +3. **GameObjectFactory**: Add a `CreateParticle()` function that allocates memory and calls the resolved constructor +4. **NativeExports**: Add `native_register_particle()` as a C export +5. **NativeInterop.cs**: Add the `[DllImport]` declaration +6. **API**: Create `ParticleRegistry.cs`, `ParticleProperties.cs` in `WeaveLoader.API/Particle/` +7. **Registry.cs**: Add `public static ParticleRegistry Particle { get; }` to the facade +8. **ExampleMod**: Add a usage example + +## Adding a New Game Hook + +1. Find the target function's mangled name in the PDB +2. Add the symbol constant and resolution in `SymbolResolver.cpp` +3. Define the function pointer typedef and `Original_*` variable in `GameHooks.h` +4. Implement `Hooked_*` in `GameHooks.cpp`, calling `Original_*` at the appropriate point +5. Add `MH_CreateHook` / `MH_EnableHook` calls in `HookManager.cpp` + +## Commit Messages + +- Use present tense: "Add particle registry" not "Added particle registry" +- Be specific: "Fix creative tab pagination after mod item injection" not "Fix bug" +- Prefix with area when helpful: "runtime: resolve Tile::setLightLevel from PDB" ## Pull Requests -- Fork the repository and create a feature branch -- Include a clear description of what your PR does -- Make sure the C# projects build without warnings +- Fork the repository and create a feature branch from `main` +- Include a clear description of what the PR does and why +- Ensure both C++ and C# projects build without errors - Test DLL injection on a real game build if possible +- Keep PRs focused -- one feature or fix per PR diff --git a/README.md b/README.md index c186de4..c970cad 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,130 @@ # Weave Loader -An SKSE-style mod loader for Minecraft Legacy Edition. Weave Loader injects into the game process at runtime, hooks key engine functions, and hosts the .NET runtime to load C# mods. **Zero game source modifications required.** +A runtime mod loader for Minecraft Legacy Edition (Xbox 360 / PS3 / Windows 64-bit port). Weave Loader injects into the game process, hooks engine functions via PDB symbol resolution, and hosts the .NET runtime so mods can be written in C#. **Zero game source modifications required.** -## How It Works +## Features -1. **Weave Loader.exe** launches the game in a suspended state and injects `WeaveLoaderRuntime.dll` -2. The runtime DLL uses PDB debug symbols to locate game functions (`MinecraftWorld_RunStaticCtors`, `Minecraft::tick`, etc.) -3. MinHook detours those functions to insert mod lifecycle callbacks -4. The .NET CoreCLR runtime is hosted inside the game process via hostfxr -5. `WeaveLoader.Core` discovers and loads C# mod assemblies from the `mods/` folder -6. Mods use the `WeaveLoader.API` to register blocks, items, entities, and subscribe to game events using Fabric-style namespaced string IDs +- **DLL injection** -- Launcher starts the game suspended, injects the runtime DLL, then resumes +- **PDB symbol resolution** -- Uses raw PDB parsing (no DIA dependency) to locate game functions by their mangled names at runtime +- **Function hooking** -- MinHook detours on game lifecycle functions (init, tick, static constructors, rendering) +- **Full .NET hosting** -- .NET 8 CoreCLR is loaded inside the game process via hostfxr; mods are standard C# class libraries +- **Block and item registration** -- Create real game objects (Tile, TileItem, Item) by calling the game's own constructors through resolved PDB symbols +- **Dynamic texture atlas merging** -- Mod textures are stitched into the game's `terrain.png` and `items.png` atlases at runtime using empty cells +- **Creative inventory injection** -- Mod items appear in the correct creative tabs with proper pagination +- **Localized display names** -- Mod strings are injected directly into the game's `StringTable` vector, bypassing inlined `GetString` calls +- **Crash reporting** -- Vectored exception handler produces detailed crash logs with register dumps, symbolicated stack traces, and loaded module lists +- **Main menu branding** -- Renders loader version and mod count on the main menu via the game's own font renderer +- **Furnace recipes** -- Register smelting recipes with input, output, and XP values +- **Event system** -- Subscribe to block break, block place, chat, entity spawn, and player join events + +## Architecture + +``` +┌──────────────────────────────────────────────────────────────┐ +│ WeaveLoader.Launcher │ +│ Starts game, injects runtime DLL │ +└────────────────────────────┬─────────────────────────────────┘ + │ CreateRemoteThread +┌────────────────────────────▼─────────────────────────────────┐ +│ WeaveLoaderRuntime.dll │ +│ C++ runtime injected into game process │ +│ │ +│ ┌─────────────┐ ┌──────────────┐ ┌───────────────────────┐ │ +│ │ PDB Parser │ │ Hook Mgr │ │ .NET Host (hostfxr) │ │ +│ │ (raw_pdb) │ │ (MinHook) │ │ │ │ +│ └──────┬──────┘ └──────┬───────┘ └───────────┬───────────┘ │ +│ │ │ │ │ +│ ┌──────▼──────┐ ┌──────▼───────┐ ┌───────────▼───────────┐ │ +│ │ Symbol │ │ Game Hooks │ │ WeaveLoader.Core.dll │ │ +│ │ Resolver │ │ (lifecycle, │ │ (mod discovery, │ │ +│ │ │ │ textures, │ │ lifecycle mgmt) │ │ +│ │ │ │ UI, strings)│ │ │ │ +│ └─────────────┘ └─────────────┘ └───────────┬───────────┘ │ +│ │ │ +│ ┌────────────────────────────────────────────▼───────────┐ │ +│ │ Native Exports (C ABI) │ │ +│ │ register_block, register_item, add_furnace_recipe, ... │ │ +│ └────────────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────┘ + │ P/Invoke +┌────────────────────────────▼─────────────────────────────────┐ +│ WeaveLoader.API │ +│ Public C# API that mod authors reference │ +│ │ +│ Registry.Block · Registry.Item · Registry.Recipe │ +│ Registry.Entity · Registry.Assets · GameEvents │ +│ Logger · CreativeTab · Identifier · [Mod] attribute │ +└──────────────────────────────────────────────────────────────┘ + │ implements IMod +┌────────────────────────────▼─────────────────────────────────┐ +│ Mod DLLs │ +│ ExampleMod, user mods, etc. │ +└──────────────────────────────────────────────────────────────┘ +``` ## Project Structure ``` ModLoader/ -├── WeaveLoader.Launcher/ C# launcher (the exe users run) -├── WeaveLoaderRuntime/ C++ DLL (injected into game process) -├── WeaveLoader.Core/ C# mod management (loaded inside game) -├── WeaveLoader.API/ C# mod API (what mod authors reference) -└── ExampleMod/ Sample mod for reference +├── WeaveLoader.Launcher/ # C# launcher executable +├── WeaveLoaderRuntime/ # C++ DLL injected into the game +│ └── src/ +│ ├── dllmain.cpp # Entry point, init thread +│ ├── PdbParser.cpp # Raw PDB symbol parsing (no DIA) +│ ├── SymbolResolver.cpp # Resolves game functions by mangled name +│ ├── HookManager.cpp # MinHook-based function detouring +│ ├── GameHooks.cpp # Hook implementations (lifecycle, UI) +│ ├── GameObjectFactory.cpp # Creates Tile/Item objects via resolved ctors +│ ├── CreativeInventory.cpp # Injects mod items into creative tabs +│ ├── ModAtlas.cpp # Texture atlas merging (terrain.png, items.png) +│ ├── ModStrings.cpp # String table injection for item names +│ ├── CrashHandler.cpp # Vectored exception handler + crash logs +│ ├── MainMenuOverlay.cpp # Renders branding text on main menu +│ ├── DotNetHost.cpp # Hosts .NET CoreCLR via hostfxr +│ ├── NativeExports.cpp # C exports called by C# via P/Invoke +│ ├── IdRegistry.cpp # Namespaced ID <-> numeric ID mapping +│ └── LogUtil.cpp # Timestamped logging to files +├── WeaveLoader.Core/ # C# mod discovery and lifecycle +│ ├── ModDiscovery.cs # Scans mods/ for IMod implementations +│ ├── ModManager.cs # Calls lifecycle hooks with error isolation +│ └── WeaveLoaderCore.cs # Entry points called from C++ runtime +├── WeaveLoader.API/ # C# public API for mod authors +│ ├── IMod.cs # Mod interface with lifecycle hooks +│ ├── ModAttribute.cs # [Mod("id", Name, Version, Author)] +│ ├── Registry.cs # Static facade for all registries +│ ├── Block/ # BlockRegistry, BlockProperties, MaterialType +│ ├── Item/ # ItemRegistry, ItemProperties +│ ├── Recipe/ # RecipeRegistry (shaped, furnace) +│ ├── Entity/ # EntityRegistry, EntityDefinition +│ ├── Assets/ # AssetRegistry (string table access) +│ ├── Events/ # GameEvents (block break/place, chat, etc.) +│ ├── Logger.cs # Debug/Info/Warning/Error logging +│ ├── CreativeTab.cs # Creative inventory tab enum +│ └── Identifier.cs # Namespaced ID parsing ("namespace:path") +├── ExampleMod/ # Sample mod demonstrating the API +│ ├── ExampleMod.cs +│ └── assets/ +│ ├── blocks/ruby_ore.png +│ └── items/ruby.png +├── build/ # Shared build output +│ ├── mods/ # Mod DLLs and assets go here +│ └── logs/ # weaveloader.log, game_debug.log, crash.log +├── WeaveLoader.sln +├── README.md +└── CONTRIBUTING.md ``` ## Building ### Prerequisites -- Visual Studio 2022 or later (with C++ and .NET workloads) -- .NET 8.0 SDK or later -- CMake 3.24 or later -- The game must be compiled with PDB generation +- Visual Studio 2022+ with **C++ Desktop Development** and **.NET 8** workloads +- CMake 3.24+ +- .NET 8.0 SDK ### Build Steps -**C++ Runtime DLL:** +Build the C++ runtime DLL: ```bash cd WeaveLoaderRuntime @@ -41,48 +132,227 @@ cmake -B build -A x64 cmake --build build --config Release ``` -**C# Projects:** +Build all C# projects (launcher, core, API, example mod): ```bash -dotnet build Weave Loader.sln +dotnet build WeaveLoader.sln -c Debug ``` +All outputs land in `build/`. + ## Usage 1. Build Weave Loader (see above) -2. Copy the output files to a folder: - - `Weave Loader.exe` - - `WeaveLoaderRuntime.dll` - - `WeaveLoader.Core.dll` - - `WeaveLoader.API.dll` -3. Create a `mods/` folder and drop mod DLLs in it -4. Run `Weave Loader.exe` -- it will ask for the game exe path on first launch -5. The game starts with mods loaded +2. Run `WeaveLoader.exe` -- it prompts for the game executable path on first launch (saved to `weaveloader.json`) +3. The launcher starts the game suspended, injects `WeaveLoaderRuntime.dll`, and resumes +4. Mods are loaded from `build/mods/` automatically + +### Log Files + +All logs are written to `build/logs/`: + +| File | Contents | +|------|----------| +| `weaveloader.log` | Loader initialization, symbol resolution, hook installation, mod lifecycle | +| `game_debug.log` | Game's own `OutputDebugString` output (captured via hook) | +| `crash.log` | Detailed crash reports with symbolicated stack traces | ## Writing a Mod -Create a new .NET 8 class library and reference `WeaveLoader.API`: +### 1. Create a .NET 8 class library + +```bash +dotnet new classlib -n MyMod --framework net8.0 +``` + +### 2. Reference the API + +Add a project reference to `WeaveLoader.API`: + +```xml + + + +``` + +Set the output to the mods folder: + +```xml + + ..\build\mods\$(AssemblyName) + false + +``` + +### 3. Implement IMod ```csharp using WeaveLoader.API; +using WeaveLoader.API.Block; +using WeaveLoader.API.Item; +using WeaveLoader.API.Recipe; +using WeaveLoader.API.Events; [Mod("mymod", Name = "My Mod", Version = "1.0.0", Author = "You")] public class MyMod : IMod { public void OnInitialize() { - var myBlock = Registry.Block.Register("mymod:cool_block", + // Register a block + var oreBlock = Registry.Block.Register("mymod:example_ore", new BlockProperties() .Material(MaterialType.Stone) - .Hardness(2.0f) - .Resistance(10f)); + .Hardness(3.0f) + .Resistance(15.0f) + .Sound(SoundType.Stone) + .Icon("mymod:example_ore") + .InCreativeTab(CreativeTab.BuildingBlocks) + .Name("Example Ore")); + + // Register an item + var gem = Registry.Item.Register("mymod:example_gem", + new ItemProperties() + .MaxStackSize(64) + .Icon("mymod:example_gem") + .InCreativeTab(CreativeTab.Materials) + .Name("Example Gem")); + + // Add a smelting recipe + Registry.Recipe.AddFurnace("mymod:example_ore", "mymod:example_gem", 1.0f); + + // Subscribe to events + GameEvents.OnBlockBreak += (sender, args) => + { + if (args.BlockId == oreBlock.NumericId) + Logger.Info("Player broke example ore!"); + }; Logger.Info("My Mod loaded!"); } } ``` -Build it, copy the DLL to `mods/`, and launch via Weave Loader. +### 4. Add textures + +Place 16x16 PNG textures in your mod's assets folder: + +``` +MyMod/ +├── assets/ +│ ├── blocks/ +│ │ └── example_ore.png # Block texture +│ └── items/ +│ └── example_gem.png # Item texture +├── MyMod.cs +└── MyMod.csproj +``` + +The icon name in `BlockProperties.Icon()` / `ItemProperties.Icon()` uses the format `"modid:texture_name"` where `texture_name` matches the PNG filename (without extension). + +### 5. Build and run + +```bash +dotnet build MyMod.csproj +# Output goes to build/mods/MyMod/ +# Run WeaveLoader.exe to launch with mods +``` + +## Mod Lifecycle + +Mods go through these phases in order: + +| Phase | Method | When | Use for | +|-------|--------|------|---------| +| PreInit | `OnPreInit()` | Before vanilla static constructors | Early configuration | +| Init | `OnInitialize()` | After vanilla registries are set up | Registering blocks, items, recipes, events | +| PostInit | `OnPostInitialize()` | After `Minecraft::init` completes | Cross-mod interactions, late setup | +| Tick | `OnTick()` | Every game tick (~20 Hz) | Per-frame logic | +| Shutdown | `OnShutdown()` | When the game exits | Cleanup, saving state | + +Each mod's lifecycle methods are wrapped in try/catch, so one mod crashing won't take down others. + +## API Reference + +### Registry.Block + +```csharp +RegisteredBlock Register(string id, BlockProperties properties) +``` + +Creates a real `Tile` game object with the specified material, hardness, resistance, and sound type. Automatically creates a corresponding `TileItem` so the block appears in inventory. + +### Registry.Item + +```csharp +RegisteredItem Register(string id, ItemProperties properties) +``` + +Creates a real `Item` game object. The constructor parameter is derived from the numeric ID to match the game's internal convention (`Item::Item(numericId - 256)`). + +### Registry.Recipe + +```csharp +void AddFurnace(string inputId, string outputId, float xp) +``` + +Registers a furnace smelting recipe. + +### GameEvents + +```csharp +event EventHandler OnBlockBreak; +event EventHandler OnBlockPlace; +event EventHandler OnChat; +event EventHandler OnEntitySpawn; +event EventHandler OnPlayerJoin; +``` + +### Logger + +```csharp +Logger.Debug(string message) +Logger.Info(string message) +Logger.Warning(string message) +Logger.Error(string message) +``` + +All log output goes to `logs/weaveloader.log` with timestamps and log level prefixes. + +### Identifier + +Namespaced string IDs follow the `"namespace:path"` convention (e.g., `"mymod:ruby_ore"`). If no namespace is provided, `"minecraft"` is assumed. + +## ID Ranges + +| Type | Numeric Range | Notes | +|------|--------------|-------| +| Blocks (Tiles) | 174 -- 255 | 82 slots; maps to TileItem in `Item::items[]` | +| Items | 3000 -- 31999 | Constructor param = `numericId - 256` | +| Entities | 1000 -- 9999 | Reserved for mod entities | + +## How It Works Internally + +### Symbol Resolution + +The runtime opens the game's PDB file and parses it using [raw_pdb](https://github.com/MolecularMatters/raw_pdb) (no dependency on Microsoft's DIA SDK). It searches public, global, and module symbol streams for decorated C++ names like `??0Tile@@IEAA@HPEAVMaterial@@_N@Z` (Tile's protected constructor). The resolved RVAs are added to the module base address to get callable function pointers. + +### Texture Atlas Merging + +1. Mod textures are discovered in `mods/*/assets/blocks/` and `mods/*/assets/items/` +2. The vanilla `terrain.png` (16x32 grid) and `items.png` (16x16 grid) are loaded via stb_image +3. Empty cells are identified by checking for fully transparent pixels +4. Mod textures are placed into empty cells +5. The merged atlas is written to a temp directory and swapped in before `Minecraft::init` +6. After the game loads textures, the original files are restored from backup +7. `SimpleIcon` objects are created for each mod texture with correct UV coordinates + +### String Table Injection + +The game's `CMinecraftApp::GetString(int)` is a thin wrapper around `StringTable::getString(int)`, which does a vector index lookup. Since MSVC's link-time optimization inlines `GetString` at call sites like `Item::getHoverName`, a MinHook detour alone isn't sufficient. The runtime parses the x64 machine code of `GetString` to locate the RIP-relative reference to `app.m_stringTable`, then directly resizes the string table's internal vector and writes mod strings at the allocated indices. + +### Crash Handler + +A vectored exception handler catches access violations and other fatal exceptions. It uses the PDB address index (built during initialization) to resolve crash addresses to symbol names, producing crash reports with full register dumps, symbolicated stack traces, and loaded module lists. The handler also intercepts `int 3` breakpoints in game code (used by `__debugbreak()` for assertions) and skips them so execution continues. ## License