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-dev

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

  1. Verify API members against the reflection DB before writing code.
    • Game Nod classes live in <GameDir>/Openplanet4.json under ns.Game.<Class>.m[] (entries n=name, t=type, p=parent). Walk it with the procedure in references/static-verify-workflow.md.
    • Openplanet script namespaces (UI::, IO::, Time::, Json::, Math::, Text::, Net::, Audio::, Icons::, getExceptionInfo()) are NOT in Openplanet4.json. They live in OpenplanetCore.json (same game dir) under functions[] (filter by ns), 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)), grep OpenplanetCore.json — false assumptions here cost a full kill+relaunch recompile cycle each time.
    • MP4 game-class specifics: references/mp4-api-verified.md (editor) and references/mp4-runtime-and-dev-memory.md (in-race / playground).
    • A class can appear in the DB with an EMPTY m[] (e.g. ns.Scene.CSceneVehicleVis is {"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 raw Dev::GetOffset* / Dev::SetOffset against its byte size. Report that honestly instead of inventing member names.
  2. 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.
  3. Unsigned folder plugins need Developer signature mode. Launch with ManiaPlanet.exe /openplanet:developer (or F3 → Developer → Signature Mode). Otherwise the log shows Invalid file hash + not suitable for the current signature mode and the plugin silently doesn't appear.
  4. Debug via the log, not the GUI. Clear Openplanet.log, launch, wait ~50 s, then grep for ERR : / compilation failed / Loaded plugin. Full command recipes: references/launch-and-verify.md.
  5. 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.
  6. Null-safe editor access. Route all App.Editor access through one gateway that re-checks every handle each frame; wrap each UI module in try/catch. Proven skeleton: references/mapforge-architecture.md.
  7. Cross-game (TMNEXT + MP4) plugins. One plugin can target BOTH games via the predefined TMNEXT / MP4 preprocessor defines (Openplanet injects them per-game; no info.toml flag needed). CGameCtnApp is the shared base, so app.Switcher.ModuleStack, app.RootMap, app.Editor, app.CurrentPlayground work on BOTH. Gate TMNEXT-only API (cast<CTrackManiaMenus>, NGameLoadProgress::EState, app.LoadProgress) behind #if TMNEXT and add an #elif MP4 branch. Keep TMNEXT and MP4 pause reasons/settings SEPARATE — do NOT collapse Pause In Menu + Pause While Loading into one inMenuOrLoading check (Copilot flagged exactly this in the green-timer port: a combined ModuleStack<1 || RootMap is null with return true always 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 separate ifs. 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 the nvg::LoadFont root-path pitfall: references/crossgame-tmnext-mp4.md.
  8. 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 bad SetOffset into a physics object crashes the game, not just the plugin. Procedure: references/mp4-runtime-and-dev-memory.md §6.
  9. A plugin whose logic lives in a bundled native .dll is a REWRITE, not a port. Unpack the .op (it is a ZIP) and check whether the .as files are just an Import::GetLibrary shell. 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.
  10. 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 CurTriggerIndex the driver actually crosses rather than a precomputed checkpoint table; it adapts to any map, multilap included. See references/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.
  11. 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, so Dev::GetOffset(nod, off) is one line. An external Python ctypes/pymem or .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".
  12. 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's references/ (esp. references/mp4-api-verified.md). Put the verified member list, error→fix table, and the "no MapForge.* SDK / no Lua-C# / MwFastBuffer is a value type" facts in README (a summary) and in references/.
  • Do not keep API facts in IDEA.md; keep IDEA.md a 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)
  • MwFastBuffer is a value type → never Buf is null
  • ternary cond ? x.Name : "?" with wstring Name → string(x.Name) both branches
  • UI::InputInt3, UI::BeginTable, UI::Columns, flag args on CollapsingHeader/BeginTabBar → don't exist; use BeginChild+SameLine
  • no string::ToLower → manual byte compare helper

Reference index (load on demand)

FileWhen to load
references/mp4-api-verified.mdAlways for MP4 EDITOR work — verified member list + error→fix table
references/mp4-runtime-and-dev-memory.mdAlways 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.mdBuilding 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.mdUser 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.mdMore MP4 error→fix cases (time API, ISO 8601, session notes)
references/mp4-supplement.mdMP4/TM2 platform supplement (differences vs TM2020)
references/mp4-ui-rendering.mdMP4 UI limits (no images, layout primitives that work)
references/api-quirks-and-pitfalls.mdGeneral AngelScript/Openplanet quirks (both games)
references/patterns-performance-ui.mdPerformance, diagnostic UI, NanoVG, JSON persistence, public API namespace
references/api-namespaces.mdNamespace/function reference (UI, Time, Text, IO, Net, Json, nvg, Math…)
references/launch-and-verify.mdinfo.toml reference + launch/kill/reload/log-verify command recipes
references/mapforge-architecture.mdProven MP4 editor-plugin skeleton (module registry + safe context)
references/static-verify-workflow.mdNo-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.mdCross-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.pyStatic 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.mdSource 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.shRe-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.pyRun 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.mdNaming/structure conventions
references/openplanet-ui-menu-and-scaling.mdMenu integration + UI scaling
references/plugin-cleanup-recipes.mdRemoving features/plugins cleanly, visibility recipes
references/feedback-map-uid.mdManiaPlanet feedback page map-UID extraction
references/blockable-input-callbacks-mp4.mdTrainer 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.mdRaw Openplanet docs dump (search only when the above fail)
references/review-failure-ledger.mdReviewing 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.mdReview 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

@byungkyu

GitHub 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).

4719k

google-slides

@byungkyu

Google 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).

3420k

google-workspace-admin

@byungkyu

Google 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).

2119k

Linear

@byungkyu

Linear 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).

1815k

google-play

@byungkyu

Google 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).

1218k