openplanet-plugin-dev
Create, debug, structure, and run Openplanet AngelScript plugins for Trackmania 2020 (TMNEXT) and ManiaPlanet 4 (TM2/MP4). Covers API quirks, AngelScript pitfalls, performance patt…
tomekdot
@tomekdot
Install
$ openclaw skills install @tomekdot/openplanet-plugin-devOpenplanet 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. |
Top skills in this category
GitHub
@byungkyuGitHub API integration with managed OAuth. Access repositories, issues, pull requests, commits, branches, and users. Use this skill when users want to interact with GitHub repositories, manage issues and PRs, search code, or automate workflows. For other third party apps, use the api-gateway skill (https://clawhub.ai/byungkyu/api-gateway). Calls run through the `maton` CLI with OAuth login, or over raw HTTP with a Maton API key where the CLI cannot be installed. Every call is authenticated as the user's connection and reaches only what that connection's authorization allows, which the provider enforces on every request; the endpoints documented here are the ones this skill uses, and any other endpoint of this app needs the user to ask for it by name. Default to read and list calls, and confirm every write or new connection with the user. This file also documents the three constructs that turn a GitHub connection into automation, in the order they are used: the connection (the first step), a hosted function that runs a GitHub action through the Maton SDK, and a trigger that calls that function on a schedule or on an event. Those sections are the platform's own reference text, shared with the api-gateway skill, with GitHub examples; they add no GitHub capability - GitHub is itself an event source, so its own events are in `references/github/triggers.md`, and the files under `references/<source>/triggers.md` are the platform's event catalogues for the sources Maton offers (time, Calendly, GitHub, Gmail, HubSpot, Linear, Notion, Slack, Stripe).
google-slides
@byungkyuGoogle Slides API integration with managed OAuth. Create presentations, add slides, insert content, and manage slide formatting. Use this skill when users want to interact with Google Slides. For other third party apps, use the api-gateway skill (https://clawhub.ai/byungkyu/api-gateway). Calls run through the `maton` CLI with OAuth login, or over raw HTTP with a Maton API key where the CLI cannot be installed. Every call is authenticated as the user's connection and reaches only what that connection's authorization allows, which the provider enforces on every request; the endpoints documented here are the ones this skill uses, and any other endpoint of this app needs the user to ask for it by name. Default to read and list calls, and confirm every write or new connection with the user. This file also documents the three constructs that turn a Google Slides connection into automation, in the order they are used: the connection (the first step), a hosted function that runs a Google Slides action through the Maton SDK, and a trigger that calls that function on a schedule or on an event. Those sections are the platform's own reference text, shared with the api-gateway skill, with Google Slides examples; they add no Google Slides capability - Google Slides is not an event source, a trigger cannot read Google Slides data, and the files under `references/<source>/triggers.md` are the platform's event catalogues for the sources Maton offers (time, Calendly, GitHub, Gmail, HubSpot, Linear, Notion, Slack, Stripe).
google-workspace-admin
@byungkyuGoogle Workspace Admin SDK integration with managed OAuth. This is a write-capable administrative integration for users, groups, organizational units, roles, and domain settings. Only connect with a least-privileged Google admin account, restrict OAuth scopes to the specific resources needed, and revoke the connection after use. All write operations require explicit user approval showing the exact HTTP method, endpoint path, and target resource identifier before execution. Use this skill only when users need Google Workspace administration. For other third party apps, use the api-gateway skill (https://clawhub.ai/byungkyu/api-gateway). Calls run through the `maton` CLI with OAuth login, or over raw HTTP with a Maton API key where the CLI cannot be installed. Every call is authenticated as the user's connection and reaches only what that connection's authorization allows, which the provider enforces on every request; the endpoints documented here are the ones this skill uses, and any other endpoint of this app needs the user to ask for it by name. Default to read and list calls, and confirm every write or new connection with the user. This file also documents the three constructs that turn a Google Workspace Admin connection into automation, in the order they are used: the connection (the first step), a hosted function that runs a Google Workspace Admin action through the Maton SDK, and a trigger that calls that function on a schedule or on an event. Those sections are the platform's own reference text, shared with the api-gateway skill, with Google Workspace Admin examples; they add no Google Workspace Admin capability - Google Workspace Admin is not an event source, a trigger cannot read Google Workspace Admin data, and the files under `references/<source>/triggers.md` are the platform's event catalogues for the sources Maton offers (time, Calendly, GitHub, Gmail, HubSpot, Linear, Notion, Slack, Stripe).
Linear
@byungkyuLinear API integration with managed OAuth. Query and manage issues, projects, teams, cycles, and labels using GraphQL. Use this skill when users name Linear explicitly or ask about their Linear issues, cycles, projects, or teams — for example "create a Linear issue for this bug", "what is in my current cycle", "move ENG-412 to done". Not for GitHub issues, Jira, or generic task lists, and not for planning work that is not tracked in Linear. For other third party apps, use the api-gateway skill (https://clawhub.ai/byungkyu/api-gateway). Requires network access and valid Maton API key. Calls run through the `maton` CLI with OAuth login, or over raw HTTP with a Maton API key where the CLI cannot be installed. Every call is authenticated as the user's connection and reaches only what that connection's authorization allows, which the provider enforces on every request; the endpoints documented here are the ones this skill uses, and any other endpoint of this app needs the user to ask for it by name. Default to read and list calls, and confirm every write or new connection with the user. This file also documents the three constructs that turn a Linear connection into automation, in the order they are used: the connection (the first step), a hosted function that runs a Linear action through the Maton SDK, and a trigger that calls that function on a schedule or on an event. Those sections are the platform's own reference text, shared with the api-gateway skill, with Linear examples; they add no Linear capability - Linear is itself an event source, so its own events are in `references/linear/triggers.md`, and the files under `references/<source>/triggers.md` are the platform's event catalogues for the sources Maton offers (time, Calendly, GitHub, Gmail, HubSpot, Linear, Notion, Slack, Stripe).
google-play
@byungkyuGoogle Play Developer API (Android Publisher) integration with managed OAuth. Manage apps, subscriptions, in-app purchases, and reviews. Use this skill when users want to interact with Google Play Console programmatically. For other third party apps, use the api-gateway skill (https://clawhub.ai/byungkyu/api-gateway). Calls run through the `maton` CLI with OAuth login, or over raw HTTP with a Maton API key where the CLI cannot be installed. Every call is authenticated as the user's connection and reaches only what that connection's authorization allows, which the provider enforces on every request; the endpoints documented here are the ones this skill uses, and any other endpoint of this app needs the user to ask for it by name. Default to read and list calls, and confirm every write or new connection with the user. This file also documents the three constructs that turn a Google Play connection into automation, in the order they are used: the connection (the first step), a hosted function that runs a Google Play action through the Maton SDK, and a trigger that calls that function on a schedule or on an event. Those sections are the platform's own reference text, shared with the api-gateway skill, with Google Play examples; they add no Google Play capability - Google Play is not an event source, a trigger cannot read Google Play data, and the files under `references/<source>/triggers.md` are the platform's event catalogues for the sources Maton offers (time, Calendly, GitHub, Gmail, HubSpot, Linear, Notion, Slack, Stripe).