Openplanet Plugin Development
Core guide for Openplanet AngelScript plugins on TM2020 (TMNEXT) and
ManiaPlanet 4 (MP4/TM2). Deep material lives in references/ — load only
what the task needs (index at the bottom).
Golden rules
- Verify API members against the reflection DB before writing code.
- Game Nod classes live in
<GameDir>/Openplanet4.jsonunderns.Game.<Class>.m[](entriesn=name,t=type,p=parent). Walk it with the procedure inreferences/static-verify-workflow.md. - Openplanet script namespaces (
UI::,IO::,Time::,Json::,Math::,Text::,Net::,Audio::,Icons::,getExceptionInfo()) are NOT inOpenplanet4.json. They live inOpenplanetCore.json(same game dir) underfunctions[](filter byns),classes[],enums[]. Extraction recipes + verified signatures:references/openplanet-core-json.md. Before assuming a built-in exists (e.g.IO::WriteFile,UI::ListBox,Time::Format(string)), grepOpenplanetCore.json— false assumptions here cost a full kill+relaunch recompile cycle each time. - MP4 game-class specifics:
references/mp4-api-verified.md(editor) andreferences/mp4-runtime-and-dev-memory.md(in-race / playground). - A class can appear in the DB with an EMPTY
m[](e.g.ns.Scene.CSceneVehicleVisis{"i":...,"s":3464}with no members). That is not a lookup failure — it means the class has no scriptable named members and the only way in is rawDev::GetOffset*/Dev::SetOffsetagainst its byte size. Report that honestly instead of inventing member names.
- Game Nod classes live in
- Target game matters. TM2020 examples often DON'T compile on MP4 (missing
UI overloads, missing members, wstring vs string). MP4 specifics:
references/mp4-api-verified.md,references/mp4-api-mismatches.md,references/mp4-supplement.md,references/mp4-ui-rendering.md. - Unsigned folder plugins need Developer signature mode. Launch with
ManiaPlanet.exe /openplanet:developer(or F3 → Developer → Signature Mode). Otherwise the log showsInvalid file hash+not suitable for the current signature modeand the plugin silently doesn't appear. - Debug via the log, not the GUI. Clear
Openplanet.log, launch, wait ~50 s, then grep forERR :/compilation failed/Loaded plugin. Full command recipes:references/launch-and-verify.md. - A running game holds the compiled state. After editing sources, kill the
game (
taskkill /F /IM ManiaPlanet.exe) and relaunch for a clean recompile — stale errors in the log mean the old build, not a failed patch. - Null-safe editor access. Route all
App.Editoraccess through one gateway that re-checks every handle each frame; wrap each UI module in try/catch. Proven skeleton:references/mapforge-architecture.md. - Cross-game (TMNEXT + MP4) plugins. One plugin can target BOTH games via
the predefined
TMNEXT/MP4preprocessor defines (Openplanet injects them per-game; noinfo.tomlflag needed).CGameCtnAppis the shared base, soapp.Switcher.ModuleStack,app.RootMap,app.Editor,app.CurrentPlaygroundwork on BOTH. Gate TMNEXT-only API (cast<CTrackManiaMenus>,NGameLoadProgress::EState,app.LoadProgress) behind#if TMNEXTand add an#elif MP4branch. Keep TMNEXT and MP4 pause reasons/settings SEPARATE — do NOT collapsePause In Menu+Pause While Loadinginto oneinMenuOrLoadingcheck (Copilot flagged exactly this in the green-timer port: a combinedModuleStack<1 || RootMap is nullwithreturn truealways reports "Loading Screen" and lets "menu" setting pause during load). Correct MP4 form:bool inMenu = app.Switcher.ModuleStack.Length < 1;bool inLoading = !inMenu && app.RootMap is null;then two separateifs. Deprecated calls to fix while porting:nvg::LoadFont("x.ttf", true, true)→nvg::LoadFont("x.ttf");Time::Format(int, bool, bool, bool)→Time::Format(uint64, bool fractions, bool forceMinutes, bool forceHours, bool short). Verified pattern, shared-member list, deprecated fixes, and thenvg::LoadFontroot-path pitfall:references/crossgame-tmnext-mp4.md. - Never hardcode memory offsets you did not verify on THIS build.
Dev::read/write/hook is fully available on MP4, but offsets are build-specific. Discover them with a live correlation panel, freeze them behind a load-time self-check (re-read a known field and compare it to a reflected value), and fall back to read-only mode when the check fails — a badSetOffsetinto a physics object crashes the game, not just the plugin. Procedure:references/mp4-runtime-and-dev-memory.md§6. - A plugin whose logic lives in a bundled native
.dllis a REWRITE, not a port. Unpack the.op(it is a ZIP) and check whether the.asfiles are just anImport::GetLibraryshell. If so, say that up front instead of promising a port — the offsets target a different executable and no sources ship in the package. Analysis recipe + verdict/brief template:references/op-package-analysis-and-porting.md. - Prefer dynamic, runtime-observed game state over static/parsed lists.
The user explicitly asks for this ("statyczne checkpointy → zrób dynamiczne").
Example: bucket by the live
CurTriggerIndexthe driver actually crosses rather than a precomputed checkpoint table; it adapts to any map, multilap included. Seereferences/mp4-runtime-and-dev-memory.md§7. Offer the static form as an explicitly-labelled fallback in the same module, never as the primary design — the user accepts "static if dynamic proves impossible", which is not the same as being handed static first. - In-plugin AngelScript beats any external process for reading game
state. When asked "can this be done in Python / .NET instead", the deciding
fact is pointer acquisition: Openplanet hands you the live nod via
GetApp()for free, soDev::GetOffset(nod, off)is one line. An external Pythonctypes/pymemor .NET process must first find that same object by scanning process memory — hours of work for something already solved. Answer with that comparison (a 3-row table works well), then keep Python for offline tooling: reflection-DB queries, code generation, Kanban seeding, and the static pre-flight gate. Do not accept "rewrite it in Python" as a way to avoid .NET when the real answer is "neither — it belongs in the plugin". - Stage delivery as Tier 1 (safe) then Tier 2 (memory writes). Tier 1 =
read-only reflection + teleport-style restore, ships immediately and cannot
crash the game. Tier 2 = velocity/physics restore via
Dev::SetOffset, gated behind a load-time offset self-check with automatic Tier-1 fallback. Ship and verify Tier 1 first; it also proves the nod chain before you build anything on top of it. A read-only telemetry tab is the cheapest possible proof that the whole context chain resolves — build it first and ask the user to confirm the numbers move before writing further modules.
Project documentation convention (where knowledge lives)
For OpenPlanet plugin projects on disk, the user's preferred layout:
IDEA.md(project root) = project catalog only. List what each plugin/project does (name → one-line description + module map). It is NOT a dumping ground for API knowledge or verified facts.- API knowledge →
README.md+ this skill'sreferences/(esp.references/mp4-api-verified.md). Put the verified member list, error→fix table, and the "noMapForge.*SDK / no Lua-C# /MwFastBufferis a value type" facts in README (a summary) and inreferences/. - Do not keep API facts in
IDEA.md; keepIDEA.mda plain index so a future session finds projects fast without re-learning the API.
Plugin skeleton (callback style)
MyPlugin/
├── info.toml # metadata (see references/launch-and-verify.md §info.toml)
└── src/
├── Main.as # Main(), Render(), RenderMenu(), OnDestroyed()
├── Core/ # context/gateway, module manager
└── Modules/ # one feature per file
// Main.as — minimal
[Setting name="Show window"] bool S_ShowWindow = true;
void Main() { /* init; yield in loops */ }
void RenderMenu() {
if (UI::MenuItem("\\$0f0" + Icons::Wrench + " MyPlugin", "", S_ShowWindow))
S_ShowWindow = !S_ShowWindow;
}
void Render() {
if (!S_ShowWindow) return;
if (UI::Begin("MyPlugin", S_ShowWindow)) {
// guard editor access every frame:
auto app = cast<CGameCtnApp>(GetApp());
auto editor = cast<CGameCtnEditorFree>(app.Editor);
if (editor is null) UI::Text("Open the map editor.");
else { /* modules */ }
}
UI::End();
}
info.toml minimum:
[meta]
name = "MyPlugin"
author = "you"
category = "Map Editing"
version = "1.0.0"
[script]
timeout = 5000
Top MP4 compile-error killers (details in references/mp4-api-verified.md)
tostring(uint)ambiguous →Text::Format("%d", int(v))- no
char()/ char literals → byte math ('A'=65,'Z'=90) MwFastBufferis a value type → neverBuf is null- ternary
cond ? x.Name : "?"withwstring Name→string(x.Name)both branches UI::InputInt3,UI::BeginTable,UI::Columns, flag args onCollapsingHeader/BeginTabBar→ don't exist; useBeginChild+SameLine- no
string::ToLower→ manual byte compare helper
Reference index (load on demand)
| File | When to load |
|---|---|
references/mp4-api-verified.md |
Always for MP4 EDITOR work — verified member list + error→fix table |
references/mp4-runtime-and-dev-memory.md |
Always for MP4 IN-RACE work (trainers, HUDs, telemetry, save-state) — playground nod chain, CTrackManiaPlayer / CTrackManiaScriptPlayer (~120 verified members incl. driver inputs, gear, rpm), the CSceneVehicleVis reflection gap (size 3464, no members), full Dev:: read/write/hook surface, the offset-hunting procedure, and the dynamic-checkpoint pattern |
references/mp4-trainer-scaffold.md |
Building an in-race trainer/HUD on MP4. Session-verified nod chain (GetApp → CurrentPlayground → GameTerminals[0].GUIPlayer → cast CTrackManiaPlayer → ScriptAPI), 41 confirmed members with types, ERaceState values, the CGameWaypointSpecialProperty lives-in-GameData-not-Game trap, the dynamic CurTriggerIndex checkpoint pattern with multilap bucket formula + static scan fallback, the Core/Modules scaffold that compiles, and the de-risking build order (telemetry tab first as proof the chain resolves). |
references/op-package-analysis-and-porting.md |
User drops a compiled .op and asks "can this run on ManiaPlanet?" — unzip pitfalls, telling a script plugin from a native-DLL plugin, fingerprinting the DLL without a disassembler, recovering structs from the bridge, port-vs-rewrite verdict + the port-brief deliverable template |
references/mp4-api-mismatches.md |
More MP4 error→fix cases (time API, ISO 8601, session notes) |
references/mp4-supplement.md |
MP4/TM2 platform supplement (differences vs TM2020) |
references/mp4-ui-rendering.md |
MP4 UI limits (no images, layout primitives that work) |
references/api-quirks-and-pitfalls.md |
General AngelScript/Openplanet quirks (both games) |
references/patterns-performance-ui.md |
Performance, diagnostic UI, NanoVG, JSON persistence, public API namespace |
references/api-namespaces.md |
Namespace/function reference (UI, Time, Text, IO, Net, Json, nvg, Math…) |
references/launch-and-verify.md |
info.toml reference + launch/kill/reload/log-verify command recipes |
references/mapforge-architecture.md |
Proven MP4 editor-plugin skeleton (module registry + safe context) |
references/static-verify-workflow.md |
No-compiler static verification pass for .as + grid/array block-replication pattern. When grepping for forbidden natives, strip /* */ and // comments first — a naive scan flags legitimate mentions inside comments (e.g. a doc line "no char() literals on MP4") as false positives. |
references/crossgame-tmnext-mp4.md |
Cross-game TMNEXT+MP4 plugins — #if TMNEXT/#elif MP4 pattern, shared CGameCtnApp members (Switcher/RootMap/Editor/CurrentPlayground), MP4 menu/load detection, deprecated-API fixes, .op-is-zip build workaround, and the nvg::LoadFont root-path pitfall. Read BEFORE porting any TMNEXT plugin to MP4. |
scripts/verify-mp4-branch.py |
Static pre-load guard: extracts the #elif MP4/#else branch and checks it has no forbidden TMNEXT symbols and only app.X members present in Openplanet4.json. Run before the in-game kill+relaunch. |
references/openplanet-core-json.md |
Source for Openplanet built-in namespaces (IO/Time/UI/Json/Math/Text/Net/Audio/Icons). Where to find them (OpenplanetCore.json, not Openplanet4.json) + verified signatures that bit us (IO::CreateFolder not CreateDirectory, no IO::WriteFile, Time::FormatString not Time::Format(string), UI::ListBox absent → use BeginCombo, Json::Value has no IsArray). |
scripts/hermes-verify-mapforge.sh |
Re-runnable ad-hoc load-check: greps Openplanet.log for clean Loaded plugin + zero ERR :/compilation failed, confirms 18 module files present & registered. Use after kill+relaunch to prove a clean compile (AngelScript has no offline compiler). |
scripts/static-preflight-plugin.py |
Run BEFORE every kill+relaunch. Generic static gate over a whole plugin tree: brace/paren balance, every handle.Member resolved against Openplanet4.json (walking the p parent chain), forbidden-native scan on comment-stripped source, Icons:: allowlist, and module-registered-vs-defined cross-check. Adapt ROOT/PLUGIN/HANDLE_TYPES at the top. Two hard-won details baked in: handle regexes need a (?<![\w.]) guard (else s. matches the tail of m_modules.Length and emits phantom errors), and you must prove the gate non-vacuous by injecting UI::BeginTable into a sandbox copy and asserting FAIL. |
references/plugin-standardization.md |
Naming/structure conventions |
references/openplanet-ui-menu-and-scaling.md |
Menu integration + UI scaling |
references/plugin-cleanup-recipes.md |
Removing features/plugins cleanly, visibility recipes |
references/feedback-map-uid.md |
ManiaPlanet feedback page map-UID extraction |
references/blockable-input-callbacks-mp4.md |
Trainer hotkey input layer — BlockableInputCallbacks on MP4: verified import surface (RegisterCallback/UnregisterCallbacks, SetInputBlocked, VirtualKeyToKeyboardInput), KeyboardInput F5=0x3F/F6=0x40/F8=0x42 enum values, PadInput/InputDecision, its [script] module/exports/shared_exports manifest shape, and the zero-dep OnKeyPress fallback |
references/openplanet-docs-dump.md |
Raw Openplanet docs dump (search only when the above fail) |
references/review-failure-ledger.md |
Reviewing a finished plugin/PR/release. Mechanism checklist of subtle runtime/state/lifecycle/UI/architecture/sync failures (UI containment, state desync, transactional mutation, architecture erosion, network mismatch). Use as a grading checklist, not a keyword linter. Imported from XertroV/openplanet-reviewer (CC0/Unlicense) — see header in file. |
references/review-evidence-precedents.md |
Review evidence + concrete fault probes. Evidence grades (Demonstrated / Confirmed source invariant / Hypothesis), real project-local precedents (tm-bosslike, Map Together, Dips++) with path:line fault fixtures, and the network fault-matrix. Load when a review branch needs a concrete probe or a precedent. Imported from XertroV/openplanet-reviewer (CC0/Unlicense) — see header in file. |
