nmrp
An open-source roleplay game-mode base for nanos world, written in Lua: a clean, modular foundation where an object-oriented MVC is wired by a loader + dependency-injection registry, so you spend your time on your gamemode instead of on plumbing.
What it is
nmrp is a game-mode package. It gives server creators the structure they usually have to build from scratch:
- MVC — every feature is a module split into a Model (data), a Service (logic), a Controller (engine wiring), and an optional Store (in-memory state).
- A loader — modules are booted in dependency order; you list them once and the loader does three ordered passes over them.
- Dependency injection — a single
ctxcontainer holds the database, models, services and config, so any service reaches any other without global lookups.
It stands on the No More RP package ecosystem:
| Package | Role |
|---|---|
nmrp-promise | JS-grade promises (async/await, combinators). |
nmrp-norm | Server-side ORM (Norm): models, relations, migrations. |
nmrp-rpc | Promise-based RPC across server and client. |
nmrp-locale | Shared localization (i18n) for Lua + WebUI. |
The client UI is built with Svelte + WebUI.
Installation
nmrp is a game-mode package, so it declares its dependencies in Package.toml and the engine loads them in order:
[game_mode]
packages_requirements = [
"nmrp-promise",
"nmrp-norm",
"nmrp-rpc",
"nmrp-locale",
]
Make sure those packages exist in your server’s Packages/ folder, select nmrp as the active game mode, then start the server. The database connection is configured through the game-mode custom setting database_connection (new-game menu, Config.toml, or the server command line).
Full setup. This is only the short version. For prerequisites, cloning the packages (with submodules), configuring the database and adding add-ons, follow the complete Installation page.
Architecture
Realms
nanos world runs two Lua VMs. Code is organized by which one it targets:
| Realm | Folder | Role |
|---|---|---|
| Server | Server/ | Authority, database, business logic. |
| Client | Client/ | UI (WebUI / Svelte), input, rendering. |
| Shared | Shared/ | Code loaded into both VMs (lib, classes, helpers, globals). |
Layer rule.
Shared/is loaded into both VMs, so it must never reach for server-only APIs (database, authority) or client-only APIs (WebUI, rendering). Keep those inServer/andClient/respectively.
Bootstrap
Each realm has a tiny, uniform entry point:
Server/Index.luaandClient/Index.luacontain onlyrequire 'app.lua';.Server/app.luabuilds the DB + thectxcontainer, then callsloader.boot(mod1, mod2, ...).Client/app.luamounts the WebUI + views + network/input wiring.
-- Server/Index.lua
require 'app.lua';
MVC + DI (server side)
A module is a folder Server/modules/<name>/ exposing a descriptor (<name>.module.lua). The loader wires it in three passes over the topological order of depends:
| Layer | File | Role |
|---|---|---|
| Model | <name>.model.lua | fun(db): models — defines the Norm tables, returns the models. |
| Service | <name>.service.lua | fun(ctx): service — business logic (closure-factory), stored in ctx.services[name]. |
| Controller | <name>.controller.lua | fun(ctx): void — wires the engine: commands, Events, Timer, Player.Subscribe. |
| Store (optional) | <name>.store.lua | In-memory repository / cache. |
ctx (AppContext) is the injection container:
---@class AppContext
---@field db Database
---@field models table<string, Model>
---@field services table<string, Service>
---@field config Config
---@field events EventEmitter
A service reaches others through ctx.services.x — and declares depends = { "x" } so the loader boots that dependency first.
Layer rule. The controller owns everything that touches the engine (timers, subscriptions, inbound RPC, commands); the service is logic + lifecycle hooks. Keep engine wiring out of services.
The reference module is Server/modules/player.
Add a module
- Create
Server/modules/<name>/with<name>.module.lua(plusmodel/service/controlleras needed). - Write requires relative to the folder and type them by hand (nanos resolves
requireper caller directory; paths end with.lua). serviceis a closure-factory; thecontrollerwires the engine; use the lifecycle hooks onctx.services.player.- Declare
dependsif the module relies on another. - Add the module to
loader.boot(...)inServer/app.lua.
A module with no persistence (runtime state only) has no
model.
Conventions
All new code follows the same conventions:
- English-only comments.
;-terminated statements.<const>on every non-reassigned local.- Parenthesized conditions.
- Full LuaCATS annotations, and an example on every public function.
- No user-facing string is hardcoded — everything goes through
nmrp-locale.
See also
- Installation — full server setup, from an empty
Packages/folder to a live gamemode. - nmrp-promise — the promise primitives every async path builds on.
- nmrp-norm — the ORM behind the Model layer.
- nmrp-rpc — server↔client request/reply used by controllers.
- nmrp-locale — localization for Lua + WebUI.
- nmrp-character-needs — a reference add-on that extends the core.
MIT © 2026 JustGod.