nonkit is a non-official mod development kit for the RPG game The NOexistenceN of Morphean Paradox : The Forest of Silver Shallots.
⚠️ Disclaimer: This is a fan project and is not affiliated with the developers or team behind NONMP. It's maintained to the best of my capacities, but keep in mind that NONMP is a new game currently in Early Access and rapidly changing, sometimes receiving more than one update per day as of writing. This plugin should be considered experimental.
nonkit-plugin is a BepInEx plugin that enables runtime modification of the game's dialogue system. The game uses Yarn Spinner as its scripting language for story content, and this plugin provides a way to dynamically load and run Yarn Spinner scripts without modifying the game's files or restarting.
The main use case is mod development. When paired with an external tool like nonkit-vsc (a VS Code extension), you can edit Yarn scripts and immediately test them in-game. This makes the iteration cycle much faster than the traditional "edit, compile, replace files, restart game" workflow.
Requires .NET 6.0 SDK.
# Debug build
dotnet build
# Release build
dotnet build -c ReleaseThe output DLL will be in bin/Debug/net6.0/ or bin/Release/net6.0/.
Drop nonkit-plugin.dll into your BepInEx plugins folder (typically BepInEx/plugins/). That's it.
This project follows SemVer.
The plugin is split into a few focused classes:
The BepInEx entry point. Registers the PluginUpdater MonoBehaviour and sets up logging. Not much else.
A Unity MonoBehaviour that runs every frame. Handles:
- Processing commands from the pipe server
- Processing commands from the internal command queue
A static class that acts as a command queue. Other parts of the plugin (or theoretically other plugins) can enqueue commands here, and PluginUpdater will execute them on the main thread. Also holds a status string that gets updated as commands complete.
Commands are represented as a DebugCommand struct with a DebugCommandType enum:
RunNode- start dialogue at a named nodeRunBytes- load a compiled Yarn program from bytes and run itRunFile- load a compiled Yarn program from a file path and run it
Static methods that actually do the dialogue manipulation:
RunNode(nodeName)- finds the game'sDialogueRunnerand callsStartDialogue(nodeName)RunBytes(data, nodeName)- parses protobuf bytes into aYarn.Program, merges it into the existing program, and starts dialogueRunFile(path, nodeName)- reads a file and callsRunBytesLoadLocalization(args)- loads localization strings into the game'sLocalizationobjects without running any dialogueRunCombined(args, nodeName)- handles multiple files with localization support; usesLoadLocalizationinternally
The key thing here is the merging. Rather than replacing the game's Yarn program, we merge our new nodes into it using protobuf's MergeFrom. This means the game's existing dialogue still works, and we just add or override specific nodes.
For localization, the plugin finds all Localization objects in the game and adds strings to them. It tries to match locale codes (like "en", "zh-cn", "ja") and falls back to the base localization if needed.
Handles injection of Luban configuration tables into the game at runtime using Harmony patches. This enables adding custom quests, conditions, and other game data without modifying the game's files.
Key components:
- Harmony patches - A postfix patch on
Table.Init()triggers config injection after the game initializes its tables - Table injection - Parses JSON config data and injects records into Luban tables like
TbMapEvent,TbCondition, andTbLocalizationAutoGenerated - Icon fallback system - A postfix patch on
SpriteUtils.LoadMapEventIcon()allows custom quests to borrow icons from existing events via theNonkit.IconSourceIdproperty
The plugin supports both int-keyed tables (using ID field) and string-keyed tables (using Key field, like localization).
Config records can include a Nonkit object with special properties that the plugin processes:
| Property | Table | Description |
|---|---|---|
IconSourceId |
TbMapEvent | Borrows the icon from another event ID (useful since custom events don't have icon assets) |
Example:
{
"ID": 900001,
"Title": "My Quest",
"Nonkit": {
"IconSourceId": 10084
}
}A named pipe server that listens on nonmp_debug for commands from external tools. Uses a length-prefixed JSON protocol:
- Client connects
- Server sends
{"type":"hello","version":"0.2.0"} - Client sends commands like
{"cmd":"run_combined","args":{...}} - Server sends responses like
{"type":"status","message":"OK"}or{"type":"error","message":"..."}
The pipe server runs on a background thread. Commands are queued and processed on the main Unity thread (via PluginUpdater). Responses are queued and sent back on a separate response thread.
Messages are length-prefixed: 4 bytes (little-endian int32) followed by that many bytes of UTF-8 JSON.
| Command | Args | Description |
|---|---|---|
run_node |
name |
Run an existing node by name |
run_bytes |
data, node |
Load compiled Yarn from base64 bytes, run specified node |
run_file |
path, node |
Load compiled Yarn from file path, run specified node |
load_localization |
localization |
Load localization strings without running a Yarn script |
load_scripts |
files |
Load compiled Yarn scripts without running (for preloading) |
load_config |
config |
Load Luban config tables (quests, conditions, etc.) |
run_combined |
files, node, localization, config |
Load scripts, localization, and config, then run |
The localization arg is keyed by locale code:
{
"localization": {
"en": {
"line:abc123": "Hello, world!"
},
"zh-cn": {
"line:abc123": "..."
}
}
}This is useful for preloading translation strings before running dialogue, or for updating strings independently of script changes.
The files arg is an array of objects:
{
"name": "MyScript.yarn",
"data": "<base64 compiled yarn>",
"strings": {
"line:abc123": "Hello, world!"
}
}The localization arg is keyed by locale code:
{
"zh-cn": {
"line:abc123": "..."
},
"ja": {
"line:abc123": "..."
}
}The config arg is an array of table objects:
{
"config": [
{
"tableName": "TbMapEvent",
"records": [
{
"ID": 900001,
"PlaceID": 9,
"Title": "My Quest",
"DialogID": "MyQuest_Start",
"Nonkit": {
"IconSourceId": 10084
}
}
]
},
{
"tableName": "TbLocalizationAutoGenerated",
"records": [
{
"Key": "MapEvent_900001_Title",
"CH": "我的任务",
"EN": "My Quest"
}
]
}
]
}| Table | Key Type | Description |
|---|---|---|
TbMapEvent |
int (ID) |
Quest/event definitions |
TbCondition |
int (ID) |
Unlock/lock conditions for events |
TbLocalizationAutoGenerated |
string (Key) |
Localization strings for UI elements |
The game looks up quest titles and descriptions using these key patterns in TbLocalizationAutoGenerated:
MapEvent_{ID}_Title- Quest title displayed in UIMapEvent_{ID}_Desc- Quest description
For example, a quest with ID 900001 needs:
MapEvent_900001_Titlefor its titleMapEvent_900001_Descfor its description
The plugin interacts with:
DialogueRunner- Unity component from Yarn Spinner that runs dialogueYarnProject.Program- the compiled Yarn program (nodes, instructions, etc.)Dialogue- the Yarn VM instanceLocalization- Yarn Spinner's localization objects for string lookupTable.tables- Luban configuration tables (for config injection)SpriteUtils- sprite loading utilities (patched for icon fallback)
The plugin uses Harmony to patch Table.Init() (to inject configs after table initialization) and SpriteUtils.LoadMapEventIcon() (to enable icon borrowing for custom events). It doesn't modify game files on disk. Everything happens in memory at runtime.
nonkit-plugin is published under the Unlicense.
