# Skopje modding (BepInEx 6 IL2CPP) This game loads [BepInEx 6](https://docs.bepinex.dev/) plugins for Unity **IL2CPP**. Official hooks live in the interop assembly **`SkopjeAssembly`**, namespace `Skopje.Modding`. ## Requirements | Item | Value | |------|--------| | Unity (game build) | 2022.3.62 | | Loader | BepInEx **6** for Unity **IL2CPP** | | Game assembly to reference | `SkopjeAssembly` (from a built game’s Il2CppInterop / dummy assemblies) | | Hook API | `Skopje.Modding.ModAPI` | ## Install layout Install BepInEx next to the game executable (same folder as the `.exe`). Typical layout: ```text GameRoot/ Game.exe winhttp.dll # Doorstop (Windows) doorstop_config.ini BepInEx/ core/ config/ # Local settings — NOT compared on join plugins/ # Your mod DLLs (+ shared data files) patchers/ # Optional patcher DLLs (+ shared data files) LogOutput.log ``` Put gameplay plugins in `BepInEx/plugins/`. Shared override tables (JSON/CFG/etc.) must sit under `plugins/` or `patchers/` next to the DLL, **not** under `BepInEx/config`. ## Minimal plugin Subscribe in `BasePlugin.Load` with `+=` (plugins load before the first scene): ```csharp using BepInEx; using BepInEx.Unity.IL2CPP; using Skopje.Modding; [BepInPlugin("com.example.skopje.hello", "Hello Skopje", "1.0.0")] public class HelloPlugin : BasePlugin { public override void Load() { ModAPI.OnServerStarted += () => Log.LogInfo("Server started"); ModAPI.OnGameModeStartedServer += gm => Log.LogInfo($"GameMode server: {gm}"); ModAPI.OnModCheckFailed += msg => Log.LogError(msg); } } ``` A full logging sample lives in [`Modding/SamplePlugin/`](../Modding/SamplePlugin/). ## ModAPI events | Event | Where it runs | Notes | |-------|----------------|-------| | `OnServerStarted` / `OnServerStopped` | Dedicated server, listen host | FishNet local server state | | `OnClientStarted` / `OnClientStopped` | Pure client, listen host | FishNet local client state | | `OnPlayerStartedServer` / `OnPlayerStoppedServer` | Server / host | `SkopjePlayer` | | `OnPlayerStartedClient` / `OnPlayerStoppedClient` | Clients / host | `SkopjePlayer` | | `OnGameModeStartedServer` | Server / host | After `SkopjeGameMode.OnStartServer` | | `OnGameModeStartedClient` | Clients / host | After `SkopjeGameMode.OnStartClient` | | `OnGameStatusChanged` | Server/host and client-only | `NotStarted`, `Active`, `GameOver` | | `OnGameOver` | Same peers as status | When status becomes `GameOver` | | `OnNewDay` / `OnDay` / `OnNight` | Server and clients | Day/night cycle | | `OnModCheckFailed` | Local client | Join rejected; message lists path diffs | A **listen host** raises both server and client events. Prefer **server** events for gameplay changes so dedicated servers work. ### ScriptableObject lookup helpers ```csharp if (ModAPI.TryGetGuidObject(123456, out GuidScriptableObject obj)) { /* ... */ } if (ModAPI.TryGetGuidObject(123456, out var weapon)) { /* mutate fields */ } ``` Call these after `OnServerStarted` or `OnGameModeStartedServer` so the GUID database is registered. ## ScriptableObject / item stat overrides Items and many definitions are baked `ScriptableObject`s keyed by integer `Guid` in `GuidScriptableObjectDBManager`. **Supported today** - Mutate **public fields** on existing instances (`ItemData`, `WeaponData`, `UtilityData`, …) at runtime after the DB is ready. - Same plugin must run on the **dedicated server and every client** for gameplay stats. **Not supported yet** - Adding brand-new items/recipes as new assets. - Replacing loose `.asset` files on disk (IL2CPP does not load those as Mono would). Example pattern (also commented in the sample plugin): ```csharp ModAPI.OnGameModeStartedServer += _ => { if (ModAPI.TryGetGuidObject(YOUR_GUID, out var weapon)) { // weapon.SomePublicStatField = newValue; } }; ``` ## Multiplayer contract - FishNet is **server-authoritative**. Client-only stat changes do not define combat/inventory outcomes. - Gameplay mods must be **identical** on the host/dedicated server and every joining client. - Client-only mods (UI, camera, cosmetics) should subscribe only to **client** events and must not change server-owned results. - Put shared override data under `BepInEx/plugins/` (or `patchers/`) so the join check can see it. Local keybinds and BepInEx settings stay in `BepInEx/config/` and are **not** compared. ## Manifest join check Before a remote client is authenticated, the game hashes: - `BepInEx/plugins/**` and `BepInEx/patchers/**` - Extensions: `.dll`, `.json`, `.cfg`, `.txt`, `.xml` Relative paths (forward slashes) + SHA-256 must match the server’s install. If `BepInEx` is missing on both sides, both look vanilla and match. **Not hashed:** `BepInEx/core`, `BepInEx/config`, logs. **Note:** A DLL present under `plugins/` still counts even if disabled in BepInEx config. Remove or rename the file to drop it from the check. On mismatch the client is disconnected, `ModAPI.OnModCheckFailed` runs with a detail string, and a Doozy game event named **`ModCheckFailed`** is sent (UI can listen for that name). ## Troubleshooting | Symptom | What to check | |---------|----------------| | Join rejected / disconnect | Log message and `OnModCheckFailed`; compare listed missing/extra/changed paths under `plugins`/`patchers` | | Plugin never loads | BepInEx 6 IL2CPP pack; `LogOutput.log`; Doorstop `winhttp.dll` next to the exe | | Overrides work solo but not in MP | Same plugin + sidecar files on **server and clients**; use server hooks for gameplay | | Different stats with “matching” mods | Shared data must live under `plugins/`, not only in `BepInEx/config/` | | Disabled mod still blocks join | File still hashed; remove the DLL/data file from `plugins` | Logs: `BepInEx/LogOutput.log` and the Unity player log.