Table of Contents

Class ModManager

Namespace
GrindFest
Assembly
GrindFest.dll

Central mod manager. Handles discovery, deployment, and hot-reload of all mod types.

Currently supports:

  • Ink Story mods (.ink files → runtime compilation → world regeneration)
  • UI mods (JavaScript/TypeScript → evaluated by the OneJS JSRunner)

Future:

  • Prefab mods (Addressable prefab overrides)
  • Script mods (C# bot scripts)

Folder structure: persistentDataPath/Mods/GrindFest/Story/ — base story mod (smart-copied from StreamingAssets) persistentDataPath/Mods/[ModName]/Story/ — additional story mods persistentDataPath/Mods/[ModName]/UI/dist/index.js — UI mod entry point (eval'd after app.js) persistentDataPath/Mods/[ModName]/Prefabs/ — .prefab DSL files defining custom prefabs (resolved by InkWorldGenerator) persistentDataPath/Mods/[ModName]/Scripts/ — (future) script mods

In Editor, can optionally use Assets/_GrindFest/Story/ directly (toggle via UseEditorStoryFiles).

[DefaultExecutionOrder(-50)]
public class ModManager : Singleton<ModManager>
Inheritance
object
ModManager
Inherited Members

Fields

HotReloadEnabled
Mods
ReopenModBrowserAfterReload

When true, the UI app will auto-open the Mod Browser after the next Runner.Reload(). Set by JS before triggering a reload from the Mod Browser context.

StoryManifestFileName

Lists the story files a build shipped (.inkdata, one relative path per line). Written by StoryBuildPreprocessor next to those files and read back here, because StreamingAssets on WebGL/Android is a URL: it can be fetched but not enumerated. Both halves must agree on the name, which is why it lives here rather than being spelled out in each place.

UIModHotReloadEnabled
UIModsEnabled
UseEditorStoryFiles

Properties

ActiveStoryDirectory

The active story directory used for compilation. In Editor with UseEditorStoryFiles=true: Assets/_GrindFest/Story/ Otherwise: persistentDataPath/Mods/GrindFest/Story/

BaseModStoryDirectory

Base mod directory: persistentDataPath/Mods/GrindFest/Story/

EditorStoryDirectory

Editor source: Assets/_GrindFest/Story/

LoadedUIMods

Discovered UI mods: modName → full path to index.js

ModsReady
ModsRootDirectory

Root mods directory: persistentDataPath/Mods/

StartupCompiledStory

Story compiled at startup with mod knots included. Set during Awake continuation (after async Workshop discovery). StoryManager picks this up via OnStoryRecompiled, or checks it in Awake.

StreamingAssetsStoryDirectory

StreamingAssets story source: StreamingAssets/Mods/GrindFest/Story/

TypingsDirectory

Shared typings directory: persistentDataPath/typings/ (sibling to Mods/)

Methods

Awake()
CompileStory()

Compile Story.ink from the active directory (with mod injection) and return a Story object. Returns null on compilation failure.

DeployBaseStoryModAsync()

Deploy base .ink files to Mods/GrindFest/Story/ (persistentDataPath). Uses hash-based detection to avoid overwriting player-modified files. In editor: copies from Assets/_GrindFest/Story/ directly (.ink files). In builds: copies from StreamingAssets/Story/ (.inkdata files, renamed to .ink). StoryBuildPreprocessor uses .inkdata extension to avoid Ink editor auto-compilation.

Asynchronous because on WebGL/Android the shipped files are fetched over HTTP; the boot awaits DeployBaseStoryModAsync() so the story folder is populated before the world generator looks.

DeployStoryToModsFolder()

Copy .ink files from Assets/_GrindFest/Story/ to persistentDataPath/Mods/GrindFest/Story/. Use this to test the mod pipeline: toggle UseEditorStoryFiles off, then edit files in the Mods folder.

DiscoverLocalMods()

Synchronously discover mods from the local Mods/ folder. Safe to call from Awake — no async / await needed.

DiscoverMods()

Discover all mods from local Mods/ folder and Steam Workshop. Populates the Mods list with ModDefinitions. Local mods take priority (if same Name exists in both).

DiscoverWorkshopMods()

Async — discover Workshop mods from Steam download folder. Skips mods already found locally (local takes priority).

GetAllModPaths()

Get all mod folder paths (local + Workshop). Used by JS-side loadUIMods to find UI/dist/index.js files.

RecompileStoryAndReload()

Recompile ink and reload the story + world.

RefreshAndReload()

Live refresh used by the mod browser after subscribing/unsubscribing to Workshop mods and after the user trusts a blocked mod. Re-runs the boot pipeline in boot order — discover, order, compile (only mods that have not been compiled yet), run entry points (once per mod per session), re-merge content, recompile the story if a mod contributes knots — and then reloads the UI via GrindFest.ModManager._uiModDirty (Runner.Reload() happens safely on the next frame, never while JS code is still on the call stack).

Two things deliberately stay behind a restart, and are reported as such: a compiled assembly cannot be unloaded (a removed mod gets its OnUnload() and its session state is dropped, but code it scheduled elsewhere keeps running until the next boot), and the world is not regenerated here — content definitions apply to future spawns, while the ink/prefab watchers own world regeneration.

Events

OnCompilationError

Fired when compilation fails. Listeners receive the error message.

OnStoryRecompiled

Fired when story is recompiled from .ink sources. Listeners receive the compiled Story object.

OnUIModError

Fired when a UI mod fails to load. Listeners receive (modName, error).

OnUIModLoaded

Fired when a UI mod is loaded. Listeners receive (modName, modPath).