{"id":"482ac1a6-8052-45e5-910d-845c2e73e734","entityType":"agent","slug":"clawhub-tomekdot-openplanet-plugin-dev","name":"openplanet-plugin-dev","canonicalUrl":"https://www.xpersona.co/agent/clawhub-tomekdot-openplanet-plugin-dev","canonicalPath":"/agent/clawhub-tomekdot-openplanet-plugin-dev","generatedAt":"2026-10-11T08:39:50.842Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"editorial-content","verified":true,"confidence":"high","updatedAt":"2026-10-11T06:05:19.302Z","emptyReason":null},"description":"Create, debug, structure, and run Openplanet AngelScript plugins for Trackmania 2020 (TMNEXT) and ManiaPlanet 4 (TM2/MP4). Covers API quirks, AngelScript pitfalls, performance patterns, the launch/verify loop, MP4 API-mismatch fixes, and proven templates. Skill: openplanet-plugin-dev Owner: tomekdot Summary: Create, debug, structure, and run Openplanet AngelScript plugins for Trackmania 2020 (TMNEXT) and ManiaPlanet 4 (TM2/MP4). Covers API quirks, AngelScript pitfalls, performance patterns, the launch/verify loop, MP4 API-mismatch fixes, and proven templates. Tags: latest:3.0.0 Version history: v3.0.0 | 2026-08-17T11:58:33.878Z | user Removed all bundled API reference","descriptionLabel":"Technical summary","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1.1K downloads reported by the source. Last updated 10/11/2026.","installCommand":"clawhub skill install s1714s261dv2y0nczy9vxs2bcd87fpcv:openplanet-plugin-dev","sourceUrl":"https://clawhub.ai/tomekdot/openplanet-plugin-dev","homepage":"https://clawhub.ai/tomekdot/skills/openplanet-plugin-dev","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/tomekdot/openplanet-plugin-dev","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/tomekdot/skills/openplanet-plugin-dev","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":61,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"Create, debug, structure, and run Openplanet AngelScript plugins for Trackmania 2020 (TMNEXT) and ManiaPlanet 4 (TM2/MP4). Covers API quirks, AngelScript pitfal"},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-11T06:05:19.302Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T06:05:19.302Z","emptyReason":null},"stars":null,"forks":null,"downloads":1138,"packageName":null,"latestVersion":"3.0.0","tractionLabel":"1.1K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T06:05:19.228Z","emptyReason":null},"lastUpdatedAt":"2026-10-11T06:05:19.302Z","lastCrawledAt":"2026-10-11T06:05:19.228Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-12T06:05:19.228Z","lastVerifiedAt":null,"highlights":[{"version":"3.0.0","createdAt":"2026-08-17T11:58:33.878Z","changelog":"Removed all bundled API reference, changelog, and template files to streamline the skill package. - Deleted 10 files, including API reference material, changelogs, implementation notes, and sample templates. - Core guidance and best practices remain in the primary documentation. - Users now refer to external resources or official documentation for in-depth API and template details.","fileCount":30,"zipByteSize":142250},{"version":"2.2.1","createdAt":"2026-08-17T11:39:14.316Z","changelog":"**Major documentation expansion and reference integration** - Added 30+ deep reference files under `references/`, covering official Openplanet API docs, static verification workflow, MP4 porting, plugin skeletons, pitfalls, and more. - SKILL.md now features detailed, step-by-step \"golden rules\" for plugin structure, debug, cross-game support (TMNEXT + MP4), API verification, signature/developer mode, log-based debugging, and memory safety. - Expanded best practices around API reflection, dynamic runtime state, packaging, and staged feature delivery (safe vs. memory-write modes). - Improved guidance for cross-game plugin compatibility and handling of TMNEXT/MP4-specific quirks. - Removed legacy/duplicate summary files; streamlined documentation conventions and project layout advice.","fileCount":40,"zipByteSize":206132},{"version":"2.2.0","createdAt":"2026-06-12T17:56:21.404Z","changelog":"## openplanet-plugin-dev v2.2.0 - Documentation update: various clarifications and format tweaks in SKILL.md. - No changes to core functionality or API; content remains focused on AngelScript plugin development for Openplanet.","fileCount":3,"zipByteSize":11172},{"version":"2.1.0","createdAt":"2026-06-12T17:27:19.154Z","changelog":"**v2.1.0 — Adds improved formatting and readability to documentation** - Enhanced SKILL.md formatting with emoji section headings and clearer structure - Clarified info.toml sample syntax (`category = \"Tools\"`) - Improved code samples and error explanations for language pitfalls - Added visual cues for warnings, errors, and notes - No changes to plugin APIs or technical content; documentation improvement only","fileCount":3,"zipByteSize":9519},{"version":"2.0.0","createdAt":"2026-06-12T17:09:32.515Z","changelog":"**openplanet-plugin-dev 2.0.0 — Major Guide Overhaul** - Expanded SKILL.md with deeper guidance: now includes language-specific AngelScript gotchas, performance notes, and advanced development patterns. - Added detailed explanations and tables for Openplanet plugin callbacks, settings, folder/package layouts, dependencies, and API namespace usage. - Improved documentation of frequent pitfalls, compile errors, and workarounds unique to Openplanet/AngelScript development. - Removed outdated skill-card.md for clarity and maintenance. - Practical code snippets and best-practice templates for settings, array usage, error handling, and plugin window design are now included.","fileCount":3,"zipByteSize":9036},{"version":"1.5.0","createdAt":"2026-06-12T11:58:38.473Z","changelog":"- SKILL.md updated with clearer formatting, added emojis, and improved readability for all sections. - Simplified and streamlined documentation; removed author/license/platforms fields and metadata now under \"openclaw\" tag. - List of common pitfalls and best practices made more concise and visual using icons. - skill-card.md removed; now only SKILL.md included.","fileCount":3,"zipByteSize":6294},{"version":"1.0.4","createdAt":"2026-06-11T23:38:33.710Z","changelog":"- Removed the file skill-card.md from the skill package. - No changes to SKILL.md contents; documentation and reference information unchanged. - Maintenance release with minor cleanup of packaged files.","fileCount":3,"zipByteSize":10624},{"version":"1.0.3","createdAt":"2026-06-02T10:21:37.216Z","changelog":"- Removed the file `skill-card.md`. - No further user-facing changes.","fileCount":3,"zipByteSize":10508}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s1714s261dv2y0nczy9vxs2bcd87fpcv:openplanet-plugin-dev","setupComplexity":"low","setupSteps":["Setup complexity is classified as HIGH. You must provision dedicated cloud infrastructure or an isolated VM. Do not run this directly on your local workstation.","Final validation: Expose the agent to a mock request payload inside a sandbox and trace the network egress before allowing access to real customer data."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-tomekdot-openplanet-plugin-dev/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-tomekdot-openplanet-plugin-dev/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-tomekdot-openplanet-plugin-dev/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-tomekdot-openplanet-plugin-dev/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-tomekdot-openplanet-plugin-dev/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-tomekdot-openplanet-plugin-dev/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-11T08:39:50.836Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-tomekdot-openplanet-plugin-dev/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-tomekdot-openplanet-plugin-dev/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-tomekdot-openplanet-plugin-dev/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-tomekdot-openplanet-plugin-dev/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"high","updatedAt":"2026-10-11T06:05:19.302Z","emptyReason":null},"readme":"Skill: openplanet-plugin-dev\n\nOwner: tomekdot\n\nSummary: Create, debug, structure, and run Openplanet AngelScript plugins for Trackmania 2020 (TMNEXT) and ManiaPlanet 4 (TM2/MP4). Covers API quirks, AngelScript pitfalls, performance patterns, the launch/verify loop, MP4 API-mismatch fixes, and proven templates.\n\nTags: latest:3.0.0\n\nVersion history:\n\nv3.0.0 | 2026-08-17T11:58:33.878Z | user\n\nRemoved all bundled API reference, changelog, and template files to streamline the skill package.\n\n- Deleted 10 files, including API reference material, changelogs, implementation notes, and sample templates.\n- Core guidance and best practices remain in the primary documentation.\n- Users now refer to external resources or official documentation for in-depth API and template details.\n\nv2.2.1 | 2026-08-17T11:39:14.316Z | auto\n\n**Major documentation expansion and reference integration**\n\n- Added 30+ deep reference files under `references/`, covering official Openplanet API docs, static verification workflow, MP4 porting, plugin skeletons, pitfalls, and more.\n- SKILL.md now features detailed, step-by-step \"golden rules\" for plugin structure, debug, cross-game support (TMNEXT + MP4), API verification, signature/developer mode, log-based debugging, and memory safety.\n- Expanded best practices around API reflection, dynamic runtime state, packaging, and staged feature delivery (safe vs. memory-write modes).\n- Improved guidance for cross-game plugin compatibility and handling of TMNEXT/MP4-specific quirks.\n- Removed legacy/duplicate summary files; streamlined documentation conventions and project layout advice.\n\nv2.2.0 | 2026-06-12T17:56:21.404Z | auto\n\n## openplanet-plugin-dev v2.2.0\n\n- Documentation update: various clarifications and format tweaks in SKILL.md.\n- No changes to core functionality or API; content remains focused on AngelScript plugin development for Openplanet.\n\nv2.1.0 | 2026-06-12T17:27:19.154Z | auto\n\n**v2.1.0 — Adds improved formatting and readability to documentation**\n\n- Enhanced SKILL.md formatting with emoji section headings and clearer structure\n- Clarified info.toml sample syntax (`category = \"Tools\"`)\n- Improved code samples and error explanations for language pitfalls\n- Added visual cues for warnings, errors, and notes\n- No changes to plugin APIs or technical content; documentation improvement only\n\nv2.0.0 | 2026-06-12T17:09:32.515Z | auto\n\n**openplanet-plugin-dev 2.0.0 — Major Guide Overhaul**\n\n- Expanded SKILL.md with deeper guidance: now includes language-specific AngelScript gotchas, performance notes, and advanced development patterns.\n- Added detailed explanations and tables for Openplanet plugin callbacks, settings, folder/package layouts, dependencies, and API namespace usage.\n- Improved documentation of frequent pitfalls, compile errors, and workarounds unique to Openplanet/AngelScript development.\n- Removed outdated skill-card.md for clarity and maintenance.\n- Practical code snippets and best-practice templates for settings, array usage, error handling, and plugin window design are now included.\n\nv1.5.0 | 2026-06-12T11:58:38.473Z | auto\n\n- SKILL.md updated with clearer formatting, added emojis, and improved readability for all sections.\n- Simplified and streamlined documentation; removed author/license/platforms fields and metadata now under \"openclaw\" tag.\n- List of common pitfalls and best practices made more concise and visual using icons.\n- skill-card.md removed; now only SKILL.md included.\n\nv1.0.4 | 2026-06-11T23:38:33.710Z | user\n\n- Removed the file skill-card.md from the skill package.\n- No changes to SKILL.md contents; documentation and reference information unchanged.\n- Maintenance release with minor cleanup of packaged files.\n\nv1.0.3 | 2026-06-02T10:21:37.216Z | user\n\n- Removed the file `skill-card.md`.\n- No further user-facing changes.\n\nv1.0.2 | 2026-05-29T12:40:41.327Z | user\n\n- Removed the sample file \"skill-card.md\" from the package.\n- No user-facing changes to documentation or feature content.\n- All main functionality, development information, and reference material remain unchanged.\n\nv1.0.1 | 2026-05-27T20:49:39.724Z | auto\n\n**New in 1.5.0: Adds official Openplanet API reference docs as reference files.**\n\n- Integrated the full, official Openplanet AngelScript API documentation as included reference files.\n- Added online API reference link for always-up-to-date docs.\n- Improved error/quirk table with more common warnings, IDE and compilation hints.\n- Updated weekday calculation documentation for correctness.\n- Removed obsolete skill-card; documentation is now in SKILL.md and reference files.\n\nv1.0.0 | 2026-05-26T14:20:53.707Z | auto\n\nOpenplanet Plugin Development 1.0.0 – Initial Release\n\n- Provides an overview and best practices for creating and structuring folder-based Openplanet AngelScript plugins.\n- Documents essential entry points, plugin metadata (`info.toml`), and setting definitions.\n- Details critical API quirks and common pitfalls (e.g., case sensitivity, UI functions, time handling).\n- Includes debugging tips for compilation errors and log file usage.\n- Presents patterns for calendar calculations and recurring weekly events in Trackmania/Maniaplanet plugins.\n- Compatible with Windows, Linux, and macOS platforms.\n\nArchive index:\n\nArchive v3.0.0: 30 files, 142250 bytes\n\nFiles: references/api-namespaces.md (24292b), references/api-quirks-and-pitfalls.md (7832b), references/blockable-input-callbacks-mp4.md (2540b), references/cross-game-tmnt-next-mp4.md (5146b), references/crossgame-tmnext-mp4.md (4662b), references/feedback-map-uid.md (1501b), references/launch-and-verify.md (4822b), references/mapforge-architecture.md (2432b), references/mp4-api-mismatches.md (7277b), references/mp4-api-verified.md (11175b), references/mp4-runtime-and-dev-memory.md (6800b), references/mp4-supplement.md (12281b), references/mp4-trainer-scaffold.md (6285b), references/mp4-ui-rendering.md (6155b), references/op-package-analysis-and-porting.md (3627b), references/openplanet-core-json.md (5542b), references/openplanet-docs-dump.md (193517b), references/openplanet-ui-menu-and-scaling.md (4209b), references/patterns-performance-ui.md (18803b), references/plugin-cleanup-recipes.md (8228b), references/plugin-standardization.md (6458b), references/review-evidence-precedents.md (6546b), references/review-failure-ledger.md (4629b), references/static-verify-workflow.md (5124b), scripts/hermes-verify-mapforge.sh (1784b), scripts/static-preflight-plugin.py (7739b), scripts/verify-mp4-branch.py (2348b), skill-card.md (2779b), SKILL.md (16733b), _meta.json (140b)\n\nFile v3.0.0:SKILL.md\n\n---\r\nname: openplanet-plugin-dev\r\ndescription: Create, debug, structure, and run Openplanet AngelScript plugins for Trackmania 2020 (TMNEXT) and ManiaPlanet 4 (TM2/MP4). Covers API quirks, AngelScript pitfalls, performance patterns, the launch/verify loop, MP4 API-mismatch fixes, and proven templates. Use when building, debugging, reviewing, or launching Openplanet plugins.\r\nversion: 3.0.0\r\nmetadata:\r\n  openclaw:\r\n    tags: [openplanet, trackmania, angelscript, plugin, game-modding]\r\n---\r\n\r\n# Openplanet Plugin Development\r\n\r\nCore guide for Openplanet AngelScript plugins on **TM2020 (TMNEXT)** and\r\n**ManiaPlanet 4 (MP4/TM2)**. Deep material lives in `references/` — load only\r\nwhat the task needs (index at the bottom).\r\n\r\n## Golden rules\r\n\r\n1. **Verify API members against the reflection DB before writing code.**\r\n   - **Game Nod classes** live in `<GameDir>/Openplanet4.json` under\r\n     `ns.Game.<Class>.m[]` (entries `n`=name, `t`=type, `p`=parent). Walk it with\r\n     the procedure in `references/static-verify-workflow.md`.\r\n   - **Openplanet script namespaces** (`UI::`, `IO::`, `Time::`, `Json::`,\r\n     `Math::`, `Text::`, `Net::`, `Audio::`, `Icons::`, `getExceptionInfo()`) are\r\n     **NOT in `Openplanet4.json`**. They live in **`OpenplanetCore.json`** (same\r\n     game dir) under `functions[]` (filter by `ns`), `classes[]`, `enums[]`.\r\n     Extraction recipes + verified signatures: `references/openplanet-core-json.md`.\r\n     **Before assuming a built-in exists (e.g. `IO::WriteFile`, `UI::ListBox`,\r\n     `Time::Format(string)`), grep `OpenplanetCore.json`** — false assumptions\r\n     here cost a full kill+relaunch recompile cycle each time.\r\n   - MP4 game-class specifics: `references/mp4-api-verified.md` (editor) and\r\n     `references/mp4-runtime-and-dev-memory.md` (in-race / playground).\r\n   - **A class can appear in the DB with an EMPTY `m[]`** (e.g.\r\n     `ns.Scene.CSceneVehicleVis` is `{\"i\":...,\"s\":3464}` with no members). That is\r\n     not a lookup failure — it means the class has no scriptable named members and\r\n     the only way in is raw `Dev::GetOffset*` / `Dev::SetOffset` against its byte\r\n     size. Report that honestly instead of inventing member names.\r\n2. **Target game matters.** TM2020 examples often DON'T compile on MP4 (missing\r\n   UI overloads, missing members, wstring vs string). MP4 specifics:\r\n   `references/mp4-api-verified.md`, `references/mp4-api-mismatches.md`,\r\n   `references/mp4-supplement.md`, `references/mp4-ui-rendering.md`.\r\n3. **Unsigned folder plugins need Developer signature mode.** Launch with\r\n   `ManiaPlanet.exe /openplanet:developer` (or F3 → Developer → Signature Mode).\r\n   Otherwise the log shows `Invalid file hash` + `not suitable for the current\r\n   signature mode` and the plugin silently doesn't appear.\r\n4. **Debug via the log, not the GUI.** Clear `Openplanet.log`, launch, wait\r\n   ~50 s, then grep for `ERR :` / `compilation failed` / `Loaded plugin`.\r\n   Full command recipes: `references/launch-and-verify.md`.\r\n5. **A running game holds the compiled state.** After editing sources, kill the\r\n   game (`taskkill /F /IM ManiaPlanet.exe`) and relaunch for a clean recompile —\r\n   stale errors in the log mean the old build, not a failed patch.\r\n6. **Null-safe editor access.** Route all `App.Editor` access through one\r\n   gateway that re-checks every handle each frame; wrap each UI module in\r\n   try/catch. Proven skeleton: `references/mapforge-architecture.md`.\r\n7. **Cross-game (TMNEXT + MP4) plugins.** One plugin can target BOTH games via\r\n   the predefined `TMNEXT` / `MP4` preprocessor defines (Openplanet injects them\r\n   per-game; no `info.toml` flag needed). `CGameCtnApp` is the shared base, so\r\n   `app.Switcher.ModuleStack`, `app.RootMap`, `app.Editor`, `app.CurrentPlayground`\r\n   work on BOTH. Gate TMNEXT-only API (`cast<CTrackManiaMenus>`,\r\n   `NGameLoadProgress::EState`, `app.LoadProgress`) behind `#if TMNEXT` and add an\r\n   `#elif MP4` branch. **Keep TMNEXT and MP4 pause reasons/settings SEPARATE** —\r\n   do NOT collapse `Pause In Menu` + `Pause While Loading` into one\r\n   `inMenuOrLoading` check (Copilot flagged exactly this in the green-timer port:\r\n   a combined `ModuleStack<1 || RootMap is null` with `return true` always reports\r\n   \"Loading Screen\" and lets \"menu\" setting pause during load). Correct MP4 form:\r\n   `bool inMenu = app.Switcher.ModuleStack.Length < 1;`\r\n   `bool inLoading = !inMenu && app.RootMap is null;` then two separate `if`s.\r\n   Deprecated calls to fix while porting: `nvg::LoadFont(\"x.ttf\", true, true)` →\r\n   `nvg::LoadFont(\"x.ttf\")`; `Time::Format(int, bool, bool, bool)` →\r\n   `Time::Format(uint64, bool fractions, bool forceMinutes, bool forceHours, bool short)`.\r\n   Verified pattern, shared-member list, deprecated fixes, and the\r\n   `nvg::LoadFont` root-path pitfall: `references/crossgame-tmnext-mp4.md`.\r\n 8. **Never hardcode memory offsets you did not verify on THIS build.** `Dev::`\r\n read/write/hook is fully available on MP4, but offsets are build-specific.\r\n Discover them with a live correlation panel, freeze them behind a load-time\r\n self-check (re-read a known field and compare it to a reflected value), and\r\n fall back to read-only mode when the check fails — a bad `SetOffset` into a\r\n physics object crashes the game, not just the plugin. Procedure:\r\n `references/mp4-runtime-and-dev-memory.md` §6.\r\n 9. **A plugin whose logic lives in a bundled native `.dll` is a REWRITE, not a\r\n port.** Unpack the `.op` (it is a ZIP) and check whether the `.as` files are\r\n just an `Import::GetLibrary` shell. If so, say that up front instead of\r\n promising a port — the offsets target a different executable and no sources\r\n ship in the package. Analysis recipe + verdict/brief template:\r\n `references/op-package-analysis-and-porting.md`.\r\n 10. **Prefer dynamic, runtime-observed game state over static/parsed lists.**\r\n The user explicitly asks for this (\"statyczne checkpointy → zrób dynamiczne\").\r\n Example: bucket by the live `CurTriggerIndex` the driver actually crosses\r\n rather than a precomputed checkpoint table; it adapts to any map, multilap\r\n included. See `references/mp4-runtime-and-dev-memory.md` §7. Offer the static\r\n form as an explicitly-labelled *fallback* in the same module, never as the\r\n primary design — the user accepts \"static if dynamic proves impossible\", which\r\n is not the same as being handed static first.\r\n 11. **In-plugin AngelScript beats any external process for reading game\r\n state.** When asked \"can this be done in Python / .NET instead\", the deciding\r\n fact is pointer acquisition: Openplanet hands you the live nod via `GetApp()`\r\n for free, so `Dev::GetOffset(nod, off)` is one line. An external Python\r\n `ctypes`/`pymem` or .NET process must first *find* that same object by scanning\r\n process memory — hours of work for something already solved. Answer with that\r\n comparison (a 3-row table works well), then keep Python for offline tooling:\r\n reflection-DB queries, code generation, Kanban seeding, and the static\r\n pre-flight gate. Do not accept \"rewrite it in Python\" as a way to avoid .NET\r\n when the real answer is \"neither — it belongs in the plugin\".\r\n 12. **Stage delivery as Tier 1 (safe) then Tier 2 (memory writes).** Tier 1 =\r\n read-only reflection + teleport-style restore, ships immediately and cannot\r\n crash the game. Tier 2 = velocity/physics restore via `Dev::SetOffset`, gated\r\n behind a load-time offset self-check with automatic Tier-1 fallback. Ship and\r\n verify Tier 1 first; it also proves the nod chain before you build anything on\r\n top of it. A read-only telemetry tab is the cheapest possible proof that the\r\n whole context chain resolves — build it first and ask the user to confirm the\r\n numbers move before writing further modules.\r\n\r\n## Project documentation convention (where knowledge lives)\r\n\r\nFor OpenPlanet plugin projects **on disk**, the user's preferred layout:\r\n\r\n- **`IDEA.md` (project root) = project catalog only.** List *what each plugin/project\r\n  does* (name → one-line description + module map). It is NOT a dumping ground for\r\n  API knowledge or verified facts.\r\n- **API knowledge → `README.md` + this skill's `references/`** (esp.\r\n  `references/mp4-api-verified.md`). Put the verified member list, error→fix table,\r\n  and the \"no `MapForge.*` SDK / no Lua-C# / `MwFastBuffer` is a value type\" facts in\r\n  README (a summary) and in `references/`.\r\n- Do **not** keep API facts in `IDEA.md`; keep `IDEA.md` a plain index so a future\r\n  session finds projects fast without re-learning the API.\r\n\r\n## Plugin skeleton (callback style)\r\n\r\n```\r\nMyPlugin/\r\n├── info.toml        # metadata (see references/launch-and-verify.md §info.toml)\r\n└── src/\r\n    ├── Main.as      # Main(), Render(), RenderMenu(), OnDestroyed()\r\n    ├── Core/        # context/gateway, module manager\r\n    └── Modules/     # one feature per file\r\n```\r\n\r\n```angelscript\r\n// Main.as — minimal\r\n[Setting name=\"Show window\"] bool S_ShowWindow = true;\r\n\r\nvoid Main() { /* init; yield in loops */ }\r\n\r\nvoid RenderMenu() {\r\n    if (UI::MenuItem(\"\\\\$0f0\" + Icons::Wrench + \" MyPlugin\", \"\", S_ShowWindow))\r\n        S_ShowWindow = !S_ShowWindow;\r\n}\r\n\r\nvoid Render() {\r\n    if (!S_ShowWindow) return;\r\n    if (UI::Begin(\"MyPlugin\", S_ShowWindow)) {\r\n        // guard editor access every frame:\r\n        auto app = cast<CGameCtnApp>(GetApp());\r\n        auto editor = cast<CGameCtnEditorFree>(app.Editor);\r\n        if (editor is null) UI::Text(\"Open the map editor.\");\r\n        else { /* modules */ }\r\n    }\r\n    UI::End();\r\n}\r\n```\r\n\r\n`info.toml` minimum:\r\n\r\n```toml\r\n[meta]\r\nname = \"MyPlugin\"\r\nauthor = \"you\"\r\ncategory = \"Map Editing\"\r\nversion = \"1.0.0\"\r\n\r\n[script]\r\ntimeout = 5000\r\n```\r\n\r\n## Top MP4 compile-error killers (details in references/mp4-api-verified.md)\r\n\r\n- `tostring(uint)` ambiguous → `Text::Format(\"%d\", int(v))`\r\n- no `char()` / char literals → byte math (`'A'`=65, `'Z'`=90)\r\n- `MwFastBuffer` is a value type → never `Buf is null`\r\n- ternary `cond ? x.Name : \"?\"` with `wstring Name` → `string(x.Name)` both branches\r\n- `UI::InputInt3`, `UI::BeginTable`, `UI::Columns`, flag args on\r\n  `CollapsingHeader`/`BeginTabBar` → don't exist; use `BeginChild`+`SameLine`\r\n- no `string::ToLower` → manual byte compare helper\r\n\r\n## Reference index (load on demand)\r\n\r\n| File | When to load |\r\n|---|---|\r\n| `references/mp4-api-verified.md` | **Always for MP4 EDITOR work** — verified member list + error→fix table |\r\n| `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 |\r\n| `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). |\r\n| `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 |\r\n| `references/mp4-api-mismatches.md` | More MP4 error→fix cases (time API, ISO 8601, session notes) |\r\n| `references/mp4-supplement.md` | MP4/TM2 platform supplement (differences vs TM2020) |\r\n| `references/mp4-ui-rendering.md` | MP4 UI limits (no images, layout primitives that work) |\r\n| `references/api-quirks-and-pitfalls.md` | General AngelScript/Openplanet quirks (both games) |\r\n| `references/patterns-performance-ui.md` | Performance, diagnostic UI, NanoVG, JSON persistence, public API namespace |\r\n| `references/api-namespaces.md` | Namespace/function reference (UI, Time, Text, IO, Net, Json, nvg, Math…) |\r\n| `references/launch-and-verify.md` | info.toml reference + launch/kill/reload/log-verify command recipes |\r\n| `references/mapforge-architecture.md` | Proven MP4 editor-plugin skeleton (module registry + safe context) |\r\n| `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. |\r\n| `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. |\r\n| `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. |\r\n| `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`). |\r\n| `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). |\r\n| `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. |\r\n| `references/plugin-standardization.md` | Naming/structure conventions |\r\n| `references/openplanet-ui-menu-and-scaling.md` | Menu integration + UI scaling |\r\n| `references/plugin-cleanup-recipes.md` | Removing features/plugins cleanly, visibility recipes |\r\n| `references/feedback-map-uid.md` | ManiaPlanet feedback page map-UID extraction |\r\n| `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 |\r\n| `references/openplanet-docs-dump.md` | Raw Openplanet docs dump (search only when the above fail) |\r\n| `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. |\r\n| `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. |\n\nFile v3.0.0:_meta.json\n\n{\n  \"ownerId\": \"kn70db4t9jcx94v2xfcg0pymyd87f4nt\",\n  \"slug\": \"openplanet-plugin-dev\",\n  \"version\": \"3.0.0\",\n  \"publishedAt\": 1786967913878\n}\n\nFile v3.0.0:references/api-namespaces.md\n\n# Openplanet API Reference - Namespaces\n\nSource: https://openplanet.dev/docs/api\n\n## List of all namespaces (as of Openplanet 1.29.5)\n\n| Namespace | Description |\n|-----------|-------------|\n| `// Global namespace` | Global functions |\n| `namespace UI` | UI tools (ImGui) |\n| `namespace nvg` | NanoVG drawing |\n| `namespace Audio` | Audio playback |\n| `namespace Auth` | Third-party API authentication |\n| `namespace Crypto` | Cryptography and hashing |\n| `namespace Display` | Display functions |\n| `namespace Fids` | Game files and folders |\n| `namespace IO` | Filesystem I/O |\n| `namespace Icons` | Icon helpers |\n| `namespace Json` | JSON serialization |\n| `namespace Math` | Math functions |\n| `namespace Net` | Networking and sockets |\n| `namespace Path` | File path operations |\n| `namespace Permissions` | Trackmania permission checks |\n| `namespace Regex` | Regular expressions |\n| `namespace SQLite` | SQLite database access |\n| `namespace Settings` | Openplanet settings |\n| `namespace Tests` | Testing utilities |\n| `namespace Text` | Text parsing and formatting |\n| `namespace Time` | Date and time |\n| `namespace XML` | XML deserialization |\n| `namespace mat3` | 3x3 matrix |\n| `namespace mat4` | 4x4 matrix |\n| `namespace string` | String static functions |\n| `namespace Reflection` | Type reflection |\n| `namespace Discord` | Discord rich presence |\n| `namespace Meta` | Openplanet meta API |\n| `namespace Import` | DLL import |\n| `namespace Dev` | Advanced memory access |\n| `namespace Internal` | Internal/built-in use only |\n\n## UI Namespace - Key Functions (AngelScript)\n\nFrom global namespace:\n- `bool UI::Begin(string name, bool* open = nil, UI.WindowFlags flags = 0)` - Begin window\n- `void UI::End()` - End window\n- `bool UI::BeginChild(string id, vec2 size = {0,0}, bool border = false, UI.WindowFlags flags = 0)`\n- `void UI::EndChild()`\n- `void UI::Text(string text)`\n- `void UI::TextColored(vec4 color, string text)` - NOTE: Does NOT exist in Openplanet AngelScript! Use PushStyleColor instead\n- `void UI::TextDisabled(string text)`\n- `void UI::TextWrapped(string text)`\n- `void UI::LabelText(string label, string text)`\n- `bool UI::Button(string label, vec2 size = {0,0})`\n- `bool UI::SmallButton(string label)`\n- `bool UI::Checkbox(string label, bool* value)`\n- `bool UI::RadioButton(string label, bool active)`\n- `bool UI::InputText(string label, string* buffer, int bufferSize, UI.InputTextFlags flags = 0)` - Returns true if text changed\n- `bool UI::Combo(string label, int* currentItem, string[] items)`\n- `bool UI::SliderFloat(string label, float* value, float min, float max, string format = \"%.3f\")`\n- `bool UI::SliderInt(string label, int* value, int min, int max, string format = \"%d\")`\n- `bool UI::DragFloat(string label, float* value, float speed = 1.0, float min = 0, float max = 0)`\n- `bool UI::ColorEdit3(string label, float[3] color, UI.ColorEditFlags flags = 0)`\n- `bool UI::ColorEdit4(string label, float[4] color, UI.ColorEditFlags flags = 0)`\n- `bool UI::TreeNode(string label)`\n- `void UI::TreePop()`\n- `bool UI::Selectable(string label, bool selected = false)`\n- `bool UI::ListBox(string label, int* currentItem, string[] items)`\n- `void UI::ProgressBar(float fraction, vec2 size = {-1,0}, string overlay = \"\")`\n- `void UI::BulletText(string text)`\n- `void UI::Separator()`\n- `void UI::SameLine()`\n- `void UI::Spacing()`\n- `void UI::Dummy(vec2 size)`\n- `void UI::PushItemWidth(float width)`\n- `void UI::PopItemWidth()`\n- `void UI::SetNextWindowPos(int x, int y, UI.Cond condition = UI.Cond::FirstUseEver)`\n- `void UI::SetNextWindowSize(int w, int h, UI.Cond condition = UI.Cond::FirstUseEver)`\n- `void UI::PushStyleColor(UI.Col idx, vec4 color)`\n- `void UI::PopStyleColor()`\n- `void UI::PushFontSize(float size)`\n- `void UI::PopFontSize()`\n- `void UI::BeginTabBar(string id)`\n- `void UI::EndTabBar()`\n- `bool UI::BeginTabItem(string label)`\n- `void UI::EndTabItem()`\n- `void UI::BeginTable(string id, int columns, UI.TableFlags flags = 0)`\n- `void UI::EndTable()`\n- `void UI::TableNextRow()`\n- `void UI::TableSetColumnIndex(int col)`\n- `void UI::Image(string texturePath, vec2 size)`\n\n## UI::InputText - CRITICAL\n\nThe documentation shows:\n```angelscript\nbool UI::InputText(string label, string* buffer, int bufferSize, UI.InputTextFlags flags = 0)\n```\n\nBut in practice (Openplanet 1.29.5), the compiler shows these overloads:\n```angelscript\nstring UI::InputText(const string&in label, string str, int flags = UI::InputTextFlags::None, UI::InputTextCallback@ callback = null)\nstring UI::InputText(const string&in label, string str, bool&out changed, int flags = UI::InputTextFlags::None, UI::InputTextCallback@ callback = null)\n```\n\nBoth return `string`, NOT `bool`. The `bool&out changed` parameter is set to true when Enter is pressed.\n\n**Working pattern:**\n```angelscript\nbool changed = false;\nUI::InputText(\"##Input\", g_Text, changed, UI::InputTextFlags::EnterReturnsTrue);\nif (changed) {\n    // Enter was pressed, g_Text contains the input\n}\n```\n\n## UI::Col enum values\n\n- `UI::Col::Text`\n- `UI::Col::TextDisabled`\n- `UI::Col::WindowBg`\n- `UI::Col::ChildBg`\n- `UI::Col::PopupBg`\n- `UI::Col::Border`\n- `UI::Col::FrameBg`\n- `UI::Col::FrameBgHovered`\n- `UI::Col::FrameBgActive`\n- `UI::Col::TitleBg`\n- `UI::Col::TitleBgActive`\n- `UI::Col::MenuBarBg`\n- `UI::Col::ScrollbarBg`\n- `UI::Col::ScrollbarGrab`\n- `UI::Col::ScrollbarGrabHovered`\n- `UI::Col::ScrollbarGrabActive`\n- `UI::Col::CheckMark`\n- `UI::Col::SliderGrab`\n- `UI::Col::SliderGrabActive`\n- `UI::Col::Button`\n- `UI::Col::ButtonHovered`\n- `UI::Col::ButtonActive`\n- `UI::Col::Header`\n- `UI::Col::HeaderHovered`\n- `UI::Col::HeaderActive`\n- `UI::Col::Separator`\n- `UI::Col::SeparatorHovered`\n- `UI::Col::SeparatorActive`\n- `UI::Col::ResizeGrip`\n- `UI::Col::Tab`\n- `UI::Col::TabHovered`\n- `UI::Col::TabActive`\n- `UI::Col::PlotLines`\n- `UI::Col::PlotLinesHovered`\n- `UI::Col::PlotHistogram`\n- `UI::Col::PlotHistogramHovered`\n- `UI::Col::TextSelectedBg`\n- `UI::Col::ModalWindowDimBg`\n\n## UI::WindowFlags enum values\n\n- `UI::WindowFlags::None`\n- `UI::WindowFlags::NoTitleBar`\n- `UI::WindowFlags::NoResize`\n- `UI::WindowFlags::NoMove`\n- `UI::WindowFlags::NoScrollbar`\n- `UI::WindowFlags::NoScrollWithMouse`\n- `UI::WindowFlags::NoCollapse`\n- `UI::WindowFlags::AlwaysAutoResize`\n- `UI::WindowFlags::NoBackground`\n- `UI::WindowFlags::NoSavedSettings`\n- `UI::WindowFlags::NoMouseInputs`\n- `UI::WindowFlags::MenuBar`\n- `UI::WindowFlags::HorizontalScrollbar`\n- `UI::WindowFlags::NoFocusOnAppearing`\n- `UI::WindowFlags::NoBringToFrontOnFocus`\n- `UI::WindowFlags::AlwaysVerticalScrollbar`\n- `UI::WindowFlags::AlwaysHorizontalScrollbar`\n- `UI::WindowFlags::AlwaysUseWindowPadding`\n- `UI::WindowFlags::NoNavInputs`\n- `UI::WindowFlags::NoNavFocus`\n\n## UI::InputTextFlags enum values\n\n- `UI::InputTextFlags::None`\n- `UI::InputTextFlags::CharsDecimal`\n- `UI::InputTextFlags::CharsHexadecimal`\n- `UI::InputTextFlags::CharsUppercase`\n- `UI::InputTextFlags::CharsNoBlank`\n- `UI::InputTextFlags::AutoSelectAll`\n- `UI::InputTextFlags::EnterReturnsTrue`\n- `UI::InputTextFlags::CallbackCompletion`\n- `UI::InputTextFlags::CallbackHistory`\n- `UI::InputTextFlags::CallbackAlways`\n- `UI::InputTextFlags::CallbackCharFilter`\n- `UI::InputTextFlags::AllowTabInput`\n- `UI::InputTextFlags::CtrlEnterForNewLine`\n- `UI::InputTextFlags::NoHorizontalScroll`\n- `UI::InputTextFlags::AlwaysOverwrite`\n- `UI::InputTextFlags::ReadOnly`\n- `UI::InputTextFlags::Password`\n- `UI::InputTextFlags::NoUndoRedo`\n\n## UI::Cond enum values\n\n- `UI::Cond::None`\n- `UI::Cond::Always`\n- `UI::Cond::Once`\n- `UI::Cond::FirstUseEver`\n- `UI::Cond::Appearing`\n\n## UI::TableFlags enum values\n\n- `UI::TableFlags::None`\n- `UI::TableFlags::Resizable`\n- `UI::TableFlags::Reorderable`\n- `UI::TableFlags::Hideable`\n- `UI::TableFlags::Sortable`\n- `UI::TableFlags::NoSavedSettings`\n- `UI::TableFlags::ContextMenuInBody`\n- `UI::TableFlags::RowBg`\n- `UI::TableFlags::BordersInnerH`\n- `UI::TableFlags::BordersOuterH`\n- `UI::TableFlags::BordersInnerV`\n- `UI::TableFlags::BordersOuterV`\n- `UI::TableFlags::BordersH`\n- `UI::TableFlags::BordersV`\n- `UI::TableFlags::BordersInner`\n- `UI::TableFlags::BordersOuter`\n- `UI::TableFlags::Borders`\n- `UI::TableFlags::NoBordersInBody`\n- `UI::TableFlags::NoBordersInBodyUntilResize`\n- `UI::TableFlags::SizingFixedFit`\n- `UI::TableFlags::SizingFixedSame`\n- `UI::TableFlags::SizingStretchProp`\n- `UI::TableFlags::SizingStretchSame`\n- `UI::TableFlags::NoHostExtendX`\n- `UI::TableFlags::NoHostExtendY`\n- `UI::TableFlags::NoKeepColumnsVisible`\n- `UI::TableFlags::PreciseWidths`\n- `UI::TableFlags::NoClip`\n- `UI::TableFlags::PadOuterX`\n- `UI::TableFlags::NoPadOuterX`\n- `UI::TableFlags::NoPadInnerX`\n- `UI::TableFlags::ScrollX`\n- `UI::TableFlags::ScrollY`\n- `UI::TableFlags::SortMulti`\n- `UI::TableFlags::SortTristate`\n\n## Time Namespace\n\nProperties:\n- `int64 Time::Stamp` - Current Unix timestamp in seconds\n- `uint64 Time::Now` - Milliseconds since game start\n- `uint64 Time::FrameCount` - Frame counter\n\nFunctions:\n- `string Time::FormatString(const string&in format, int64 stamp = -1)` - strftime format, local time\n- `string Time::FormatStringUTC(const string&in format, int64 stamp = -1)` - strftime format, UTC\n- `int64 Time::ParseFormatString(const string&in format, const string&in stamp)` - Parse time from string\n- `string Time::Format(uint64 time, bool fractions = true, bool forceMinutes = true, bool forceHours = false, bool short = false)` - Race time format (e.g. \"1:01.234\")\n- `uint64 Time::ParseRelativeTime(const string&in time)` - Parse race time to ms\n- `Time::Info Time::Parse(int64 stamp = -1)` - Parse to Time::Info (local)\n- `Time::Info Time::ParseUTC(int64 stamp = -1)` - Parse to Time::Info (UTC)\n\nTime::Info members (PascalCase!):\n- `int Year`\n- `int Month`\n- `int Day`\n- `int Hour`\n- `int Minute`\n- `int Second`\n- `int DayOfWeek` (0=Sunday)\n\n## Text Namespace\n\nFunctions:\n- `int Text::ParseInt(const string&in str, int base = 10)`\n- `int64 Text::ParseInt64(const string&in str, int base = 10)`\n- `uint Text::ParseUInt(const string&in str, int base = 10)`\n- `float Text::ParseFloat(const string&in str)`\n- `double Text::ParseDouble(const string&in str)`\n- `vec4 Text::ParseHexColor(string str)` - e.g. \"#FF0000\"\n- `string Text::Format(const string&in format, <type> value)` - Single value only!\n- `string Text::StripFormatCodes(const string&in s)` - Strip Mania formatting codes ($f00 etc)\n- `string Text::FormatGameColor(const vec3&in rgb)` - Format as $f00\n- `string Text::FormatOpenplanetColor(const vec3&in rgb)` - Format as \\$f00\n- `string Text::EncodeHex(const string&in, bool upper = false)`\n- `string Text::DecodeHex(const string&in)`\n- `string Text::EncodeBase64(const string&in, bool url = false)`\n- `string Text::DecodeBase64(const string&in, bool url = false)`\n\n## IO Namespace\n\nFunctions:\n- `string IO::FromStorageFolder(const string&in filename)` - Plugin storage path\n- `string IO::FromDataFolder(const string&in filename)` - Openplanet data folder\n- `string IO::FromAppFolder(const string&in filename)` - Game install folder\n- `string IO::FromUserGameFolder(const string&in filename)` - User game folder\n- `bool IO::FileExists(const string&in filename)`\n- `uint64 IO::FileSize(const string&in filename)`\n- `int64 IO::FileCreatedTime(const string&in filename)`\n- `int64 IO::FileModifiedTime(const string&in filename)`\n- `void IO::Delete(const string&in filename)`\n- `void IO::Copy(const string&in path, const string&in target)`\n- `void IO::Move(const string&in path, const string&in target)`\n- `bool IO::FolderExists(const string&in path)`\n- `void IO::CreateFolder(const string&in path, bool recursive = true)`\n- `void IO::DeleteFolder(const string&in path, bool recursive = false)`\n- `string[]@ IO::IndexFolder(const string&in path, bool recursive)`\n\n## Meta Namespace\n\nFunctions:\n- `Meta::Plugin@ Meta::ExecutingPlugin()` - Current plugin handle\n- `Meta::Plugin@[]@ Meta::AllPlugins()` - All loaded plugins\n- `Meta::Plugin@ Meta::GetPluginFromID(const string&in id)` - Get plugin by ID\n- `void Meta::UnloadPlugin(Plugin@ plugin)` - Queue plugin unload\n- `void Meta::ReloadPlugin(Plugin@ plugin)` - Queue plugin reload\n- `void Meta::OpenSettings(Plugin@ plugin = null)` - Open settings window\n- `void Meta::SaveSettings()` - Force save settings\n- `bool Meta::IsDeveloperMode()` - Check dev mode\n- `string Meta::OpenplanetVersion()` - Get Openplanet version\n- `string Meta::OpenplanetBuildInfo()` - Get build info\n- `void Meta::Terminate()` - Exit game\n\n## Net Namespace\n\nFunctions:\n- `Net::HttpRequest@ Net::HttpGet(const string&in url)`\n- `Net::HttpRequest@ Net::HttpPost(const string&in url, const string&in data = \"\", const string&in contentType = \"application/x-www-form-urlencoded\")`\n- `Net::HttpRequest@ Net::HttpHead(const string&in url)`\n- `Net::HttpRequest@ Net::HttpPut(const string&in url, const string&in data = \"\", const string&in contentType = \"application/x-www-form-urlencoded\")`\n- `Net::HttpRequest@ Net::HttpDelete(const string&in url)`\n- `Net::HttpRequest@ Net::HttpPatch(const string&in url, const string&in data = \"\", const string&in contentType = \"application/x-www-form-urlencoded\")`\n- `string Net::UrlEncode(const string&in str)`\n- `string Net::UrlDecode(const string&in str)`\n\n## Json Namespace\n\nClasses:\n- `class Json::Value` - JSON value node\n\nFunctions:\n- `Json::Value@ Json::Object()` - Create JSON object\n- `Json::Value@ Json::Array()` - Create JSON array\n- `Json::Value@ Json::Parse(const string&in json)` - Parse JSON string\n- `string Json::Write(const Json::Value@ value, bool pretty = false)` - Serialize to string\n- `Json::Value@ Json::FromFile(const string&in filename)` - Load from file\n- `void Json::ToFile(const string&in filename, const Json::Value@ value, bool pretty = false)` - Save to file\n\n## nvg (NanoVG) Namespace\n\nKey functions:\n- `nvg::Texture@ nvg::LoadTexture(const string&in filename, int flags = 0)`\n- `int nvg::LoadFont(const string&in filename, bool fallbackIcons = false, bool fallbackArial = false)`\n- `void nvg::Save()` / `void nvg::Restore()` / `void nvg::Reset()`\n- `void nvg::BeginPath()` / `void nvg::EndPath()` (implied)\n- `void nvg::Fill()` / `void nvg::Stroke()`\n- `void nvg::FillColor(const vec4&in color)` / `void nvg::StrokeColor(const vec4&in color)`\n- `void nvg::FillPaint(const nvg::Paint&in paint)` / `void nvg::StrokePaint(const nvg::Paint&in paint)`\n- `void nvg::StrokeWidth(float size)`\n- `void nvg::Rect(float x, float y, float w, float h)`\n- `void nvg::RoundedRect(float x, float y, float w, float h, float r)`\n- `void nvg::Circle(const vec2&in center, float r)`\n- `void nvg::Ellipse(const vec2&in center, float rx, float ry)`\n- `void nvg::Arc(const vec2&in center, float r, float a0, float a1, nvg::Winding dir)`\n- `void nvg::MoveTo(const vec2&in pos)` / `void nvg::LineTo(const vec2&in pos)`\n- `void nvg::BezierTo(const vec2&in c1, const vec2&in c2, const vec2&in pos)`\n- `float nvg::Text(float x, float y, const string&in str)`\n- `void nvg::TextBox(float x, float y, float w, const string&in str)`\n- `vec2 nvg::TextBounds(const string&in str)`\n- `void nvg::FontSize(float size)`\n- `void nvg::FontFace(int font)`\n- `void nvg::TextAlign(int align)`\n- `void nvg::Scissor(float x, float y, float w, float h)`\n- `void nvg::ResetScissor()`\n- `nvg::Paint nvg::LinearGradient(const vec2&in start, const vec2&in end, const vec4&in color1, const vec4&in color2)`\n- `nvg::Paint nvg::BoxGradient(float x, float y, float w, float h, float r, float f, const vec4&in color1, const vec4&in color2)`\n- `nvg::Paint nvg::RadialGradient(const vec2&in center, float inr, float outr, const vec4&in color1, const vec4&in color2)`\n- `nvg::Paint nvg::TexturePattern(const vec2&in origin, const vec2&in size, float angle, nvg::Texture@ texture, float alpha)`\n- `void nvg::Translate(float x, float y)`\n- `void nvg::Rotate(float angle)`\n- `void nvg::Scale(float x, float y)`\n- `void nvg::ResetTransform()`\n- `void nvg::SetTransform(const mat3&in t)`\n- `mat3 nvg::CurrentTransform()`\n\n## Math Namespace\n\nConstants:\n- `float Math::PI`\n- `float Math::PI2`\n- `double Math::PIl`\n- `double Math::PI2l`\n\nKey functions:\n- `int Math::Abs(int i)` / `float Math::Abs(float f)`\n- `float Math::Sin(float f)` / `float Math::Cos(float f)` / `float Math::Tan(float f)`\n- `float Math::Asin(float f)` / `float Math::Acos(float f)` / `float Math::Atan(float f)`\n- `float Math::Atan2(float y, float x)`\n- `float Math::Sqrt(float f)`\n- `float Math::Pow(float x, float y)`\n- `float Math::Exp(float f)`\n- `float Math::Log(float f)` / `float Math::Log2(float f)` / `float Math::Log10(float f)`\n- `float Math::Floor(float f)` / `float Math::Ceil(float f)` / `float Math::Round(float f)`\n- `float Math::ToDeg(float rad)` / `float Math::ToRad(float deg)`\n- `float Math::Rand(float min, float max)` / `int Math::Rand(int min, int max)`\n- `int Math::Min(int x, int y)` / `float Math::Min(float x, float y)`\n- `int Math::Max(int x, int y)` / `float Math::Max(float x, float y)`\n- `int Math::Clamp(int x, int min, int max)` / `float Math::Clamp(float x, float min, float max)`\n- `float Math::Lerp(const float&in min, const float&in max, float x)`\n- `vec2 Math::Lerp(const vec2&in min, const vec2&in max, float x)`\n- `vec3 Math::Lerp(const vec3&in min, const vec3&in max, float x)`\n- `vec4 Math::Lerp(const vec4&in min, const vec4&in max, float x)`\n- `float Math::Distance(const vec2&in a, const vec2&in b)` / `float Math::Distance(const vec3&in a, const vec3&in b)`\n- `float Math::Distance2(const vec2&in a, const vec2&in b)` / `float Math::Distance2(const vec3&in a, const vec3&in b)`\n- `float Math::Dot(const vec2&in a, const vec2&in b)` / `float Math::Dot(const vec3&in a, const vec3&in b)`\n- `float Math::Angle(const vec2&in a, const vec2&in b)` / `float Math::Angle(const vec3&in a, const vec3&in b)`\n- `vec3 Math::Cross(const vec3&in a, const vec3&in b)`\n- `bool Math::IsNaN(float)` / `bool Math::IsInf(float)`\n- `quat Math::Slerp(const quat&in a, const quat&in b, float x)`\n- `float Math::InvLerp(const float&in min, const float&in max, const float&in value)`\n\n## string (static) Namespace\n\nFunctions:\n- `string string::Join(const string[]&in arr, const string&in delimiter)`\n- `string string::Repeat(const string&in str, int count)`\n\n## Regex Namespace\n\nFunctions:\n- `bool Regex::IsMatch(const string&in source, const string&in pattern, int flags = Regex::Flags::ECMAScript)`\n- `bool Regex::Contains(const string&in source, const string&in pattern, int flags = Regex::Flags::ECMAScript)`\n- `string[]@ Regex::Match(const string&in source, const string&in pattern, int flags = Regex::Flags::ECMAScript)`\n- `string[]@ Regex::Search(const string&in source, const string&in pattern, int flags = Regex::Flags::ECMAScript)`\n- `Regex::SearchAllResult@ Regex::SearchAll(const string&in source, const string&in pattern, int flags = Regex::Flags::ECMAScript)`\n- `string Regex::Replace(const string&in source, const string&in pattern, const string&in replace, int flags = Regex::Flags::ECMAScript)`\n\n## Path Namespace\n\nFunctions:\n- `string Path::GetExtension(const string&in path)`\n- `bool Path::HasExtension(const string&in path)`\n- `string Path::ChangeExtension(const string&in path, const string&in extension)`\n- `string Path::RemoveExtension(const string&in path)`\n- `string Path::Join(const string&in a, const string&in b)`\n- `bool Path::Equals(const string&in a, const string&in b, bool caseSensitive = false)`\n- `string Path::GetDirectoryName(const string&in path)`\n- `string Path::GetFileName(const string&in path)`\n- `string Path::GetFileNameWithoutExtension(const string&in path)`\n- `string Path::SanitizeFileName(const string&in name)`\n\n## Fids Namespace\n\nFunctions:\n- `CSystemFidFile@ Fids::GetResource(const string&in path)`\n- `CSystemFidsFolder@ Fids::GetResourceFolder(const string&in path)`\n- `CSystemFidFile@ Fids::GetProgramData(const string&in path)`\n- `CSystemFidsFolder@ Fids::GetProgramDataFolder(const string&in path)`\n- `CSystemFidFile@ Fids::GetUser(const string&in path)`\n- `CSystemFidsFolder@ Fids::GetUserFolder(const string&in path)`\n- `CSystemFidFile@ Fids::GetGame(const string&in path)`\n- `CSystemFidsFolder@ Fids::GetGameFolder(const string&in path)`\n- `CSystemFidFile@ Fids::GetFake(const string&in path)`\n- `CSystemFidsFolder@ Fids::GetFakeFolder(const string&in path)`\n- `CMwNod@ Fids::Preload(CSystemFidFile@ fid)`\n- `bool Fids::Extract(CSystemFidFile@ fid, bool hookMethod = false)`\n- `string Fids::GetFullPath(CSystemFidFile@ fid)`\n- `void Fids::UpdateTree(CSystemFidsFolder@ fids, bool withFiles = true)`\n- `CSystemFidFile@ Fids::GetFidsFile(CSystemFidsFolder@ fids, const string&in path)`\n- `CSystemFidsFolder@ Fids::GetFidsFolder(CSystemFidsFolder@ fids, const string&in path)`\n\n## Display Namespace\n\nFunctions:\n- `int Display::GetWidth()` - Game resolution width\n- `int Display::GetHeight()` - Game resolution height\n\n## Discord Namespace\n\nFunctions:\n- `void Discord::Initialize(const string&in applicationId)`\n- `void Discord::Shutdown()`\n- `bool Discord::IsReady()`\n- `Discord::User@ Discord::GetUser()`\n- `void Discord::SetStatus(const Discord::Status&in status)`\n- `void Discord::Respond(const string&in userId, Discord::Response reply)`\n- `string Discord::GetQueuedJoin()`\n- `string Discord::GetQueuedSpectate()`\n- `int Discord::GetNumJoinRequests()`\n- `Discord::User@ Discord::GetQueuedJoinRequest()`\n\n## Dev Namespace\n\nFunctions:\n- `void Dev::Sleep(uint ms)`\n- `uint64 Dev::BaseAddress()`\n- `uint64 Dev::BaseAddressEnd()`\n- `uint64 Dev::FindPattern(const string&in pattern)`\n- `string Dev::Patch(uint64 ptr, const string&in pattern)`\n- `Dev::HookInfo@ Dev::Hook(uint64 ptr, int padding, const string&in func, int pushRegisters = 0)`\n- `void Dev::Unhook(Dev::HookInfo@ hook)`\n- `void Dev::DebugBreak()`\n- `uint64 Dev::Allocate(uint size, bool executable = false)`\n- `void Dev::Free(uint64 ptr)`\n- Various Read/Write functions for memory access\n\n## Auth Namespace\n\nClasses:\n- `class Auth::PluginAuthTask`\n\nFunctions:\n- `Auth::PluginAuthTask@ Auth::GetToken()`\n\n## Crypto Namespace\n\nFunctions:\n- `string Crypto::MD5(const string&in str)`\n- `string Crypto::Sha1(const string&in str)`\n- `string Crypto::Sha256(const string&in str)`\n- `MemoryBuffer@ Crypto::Random(int length)`\n- `string Crypto::RandomBase64(int length, bool url = false)`\n\n## Audio Namespace\n\nClasses:\n- `class Audio::Sample`\n- `class Audio::Voice`\n\nFunctions:\n- `Audio::Sample@ Audio::LoadSample(const string&in filename, bool streamed = false)`\n- `Audio::Sample@ Audio::LoadSample(MemoryBuffer&in buffer, bool streamed = false)`\n- `Audio::Sample@ Audio::LoadSampleFromAbsolutePath(const string&in filename, bool streamed = false)`\n- `Audio::Voice@ Audio::Play(Sample@ sample, float gain = 1.0f)`\n- `Audio::Voice@ Audio::Start(Sample@ sample)`\n\n## SQLite Namespace\n\nClasses:\n- `class SQLite::Database`\n- `class SQLite::Statement`\n\n## Settings Namespace\n\nClasses:\n- `class Settings::Section`\n\n## Tests Namespace\n\nClasses:\n- `class Tests::Context`\n\n## XML Namespace\n\nClasses:\n- `class XML::Node`\n- `class XML::Document`\n\n## Reflection Namespace\n\nClasses:\n- `class Reflection::MwMemberInfo`\n- `class Reflection::MwClassInfo`\n\nFunctions:\n- `const Reflection::MwClassInfo@ Reflection::GetType(const string&in name)`\n- `const Reflection::MwClassInfo@ Reflection::GetType(uint id)`\n- `const Reflection::MwClassInfo@ Reflection::TypeOf(CMwNod@ nod)`\n- `int Reflection::GetRefCount(CMwNod@ nod)`\n\n## Import Namespace\n\nClasses:\n- `class Import::Library`\n- `class Import::Function`\n- `class Import::Ref`\n\nFunctions:\n- `Import::Library@ Import::GetLibrary(const string&in path)`\n\n## mat3 Namespace\n\nFunctions:\n- `mat3 mat3::Identity()`\n- `mat3 mat3::Translate(const vec2&in v)`\n- `mat3 mat3::Rotate(float angle)`\n- `mat3 mat3::Scale(const vec2&in scale)`\n- `mat3 mat3::Scale(float scale)`\n- `mat3 mat3::Inverse(const mat3&in)`\n- `mat3 mat3::Transpose(const mat3&in)`\n\n## mat4 Namespace\n\nFunctions:\n- `mat4 mat4::Identity()`\n- `mat4 mat4::Translate(const vec3&in v)`\n- `mat4 mat4::Rotate(float angle, const vec3&in dir)`\n- `mat4 mat4::Scale(const vec3&in scale)`\n- `mat4 mat4::Scale(float scale)`\n- `mat4 mat4::Perspective(float yFov, float aspect, float nearZ, float farZ)`\n- `mat4 mat4::Inverse(const mat4&in)`\n- `mat4 mat4::LookAt(const vec3&in eye, const vec3&in center, const vec3&in up)`\n- `mat4 mat4::Transpose(const mat4&in)`\n\n### Reference: OpenPlanet-Basic-API\n\nFile v3.0.0:references/api-quirks-and-pitfalls.md\n\n## 🚨 CRITICAL — API Quirks & Pitfalls\n\n### 🔤 AngelScript Language Quirks\n\nThese bite everyone. AngelScript ≠ C++, ≠ C#, ≠ Java.\n\n#### 🔢 Integer literal suffixes\n\n`u`, `l`, `ul`, `ull` suffixes are **not supported**.\n\n```angelscript\n// ❌ ERR: Unexpected token '<identifier>'\nconst uint64 MASK = 0x3FFFFFFu;\nconst int BIG = 1ul;\n\n// ✅ OK: bare literal; compiler picks the type\nconst uint64 MASK = 0x3FFFFFF;\nconst int BIG = 1;\nconst uint64 KEY = uint64(0x123);     // explicit cast\n```\n\n#### 📐 `const` on value-type arrays\n\n`const` works for primitives, but **NOT for fixed-size arrays of value types** like `int2[]` or `vec3[]`.\n\n```angelscript\n// ❌ ERR: Expected '('\nconst int2 EDGES[12] = { int2(0,1), ... };\nconst vec3 CORNERS[8] = { ... };\n\n// ✅ OK: dynamic, no const\nint2[] g_Edges = { int2(0,1), ... };\n```\n\nAlso: `const` on `array<T>` is unreliable — keep globals un-`const` and use a `g_` prefix instead.\n\n#### 📏 Fixed-size local arrays\n\n**Local fixed-size arrays are not supported**:\n\n```angelscript\nvoid Foo() {\n    // ❌ ERR: Expected ';' Instead found identifier 'pts'\n    vec2 pts[8];\n    float depth[8];\n}\n\n// ✅ WORKAROUND: individual variables\nvoid Foo() {\n    vec2 p0, p1, p2, p3, p4, p5, p6, p7;\n    // compiler will register-allocate them\n}\n```\n\nOr use dynamic arrays:\n\n```angelscript\nvec2[] GetCorners() {\n    vec2[] cs = { vec2(0,0), vec2(1,0), ... };\n    return cs;\n}\n```\n\n#### 🗝️ `dictionary` key types\n\n**Most surprising limitation**: `dictionary` only accepts `const string&in` keys.\n\n```angelscript\ndictionary d;\n\n// ❌ ERR: No matching signatures to 'dictionary::Exists(uint64)'\nd[uint64(123)] = true;\nd[int(456)]    = true;\n\n// ✅ OK: build a string key\nd[\"123\"] = true;\nd[gx + \",\" + gy + \",\" + gz] = true;\n```\n\nIf you need a fast non-string key, roll your own hash table (parallel arrays + linear scan, or small open-addressed probe table). For most plugins, `string` keys are fine.\n\n#### 🔀 `uint` vs `int` in comparisons\n\n`array<T>.Length` returns `uint`. Comparing a signed `int` loop counter produces a warning treated as error in strict mode:\n\n```angelscript\n// ⚠️ WARN: Signed/Unsigned mismatch\nfor (int e = 0; e < arr.Length; e++) { ... }\n\n// ✅ CLEAN:\nfor (uint e = 0; e < arr.Length; e++) { ... }\n```\n\n#### 📤 `out` parameter naming\n\nMatch the parameter name **exactly**:\n\n```angelscript\n// ❌ ERR: No matching symbol 'outDepth'\nbool Project(vec3 &in p, vec2 &out screen, float &out depth) {\n    outDepth = 0.0f;\n}\n\n// ✅ OK:\nbool Project(vec3 &in p, vec2 &out screen, float &out depth) {\n    depth = 0.0f;\n}\n```\n\n#### 🎨 `int2`, `vec2`, `vec3`, `vec4` constructors\n\n```angelscript\nint2 a = int2(1, 2);\nvec3 v = vec3(1.0f, 2.0f, 3.0f);\nvec4 red = vec4(1.0f, 0.0f, 0.0f, 0.5f);\n```\n\nThey are value types — no `@` needed for storage.\n\n#### 🔄 `&inout` on primitive types is NOT allowed\n\n```angelscript\n// ❌ ERR:\nvoid ConfigRow(const string &in label, bool &inout value) { }\n\n// ✅ OK:\nvoid ConfigRow(const string &in label, bool value) { }\n```\n\n#### 📊 Array initialization — inline `int t[] = {...}` fails inside functions\n\n```angelscript\n// ❌ ERR inside functions:\n// int t[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};\n\n// ✅ OK: use array<T> with InsertLast\narray<int64> items;\nitems.InsertLast(123);\n\n// ✅ OK: pre-allocate at global scope\nint[] g_Array;\nvoid Main() { g_Array.Resize(16); }\n\n// ✅ OK: inline array init works at global scope\nint[] monthDays = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};\n```\n\n#### 🔍 `string::IndexOf` — takes exactly ONE parameter\n\n```angelscript\n// ❌ ERR:\nint idx = text.IndexOf(\"[\", startPos);\n\n// ✅ OK:\nint idx = text.IndexOf(\"[\");\n// For offset: use SubStr first\nint idx = text.SubStr(startPos).IndexOf(\"[\");\n```\n\n#### 📝 `Text::Format` takes exactly ONE value argument\n\n```angelscript\n// ❌ ERR:\nText::Format(\"%.6f (%.2f%%)\", sidereal, sidereal * 100.0);\n\n// ✅ OK:\nText::Format(\"%.6f\", sidereal) + \" (\" + Text::Format(\"%.2f%%\", sidereal * 100.0) + \")\";\n```\n\n---\n\n### ⏰ Time API Quirks\n\n#### 🅰️ Time::Info uses PascalCase, NOT lowercase\n\n`'year' is not a member of 'Time::Info'`\n\n```angelscript\n// ❌ ERR:\ninfo.year, info.month, info.day, info.hour, info.minute, info.second\n\n// ✅ OK:\ninfo.Year, info.Month, info.Day, info.Hour, info.Minute, info.Second\n```\n\n#### 📅 Weekday is NOT a member of Time::Info\n\n`info.Weekday` will fail. Use **Zeller's formula** (0=Sun..6=Sat):\n\n```angelscript\nint GetDayOfWeek(int y, int m, int d) {\n    if (m < 3) { m += 12; y -= 1; }\n    int K = y % 100;\n    int J = y / 100;\n    int h = (d + (13 * (m + 1)) / 5 + K + K / 4 + J / 4 + 5 * J) % 7;\n    return (h + 6) % 7; // 0=Sun\n}\n```\n\n#### 🕐 Time functions\n\n```angelscript\nint64 now = Time::Stamp;                          // Epoch seconds\nuint64 gameTime = Time::Now;                      // ms since game start\nstring formatted = Time::FormatString(\"%H:%M\", now);  // strftime format\nTime::Info info = Time::Parse(now);               // Local time\n```\n\n---\n\n### 🖥️ UI API Quirks\n\n#### 🔤 No UI::Font enum — use PushFontSize\n\n```angelscript\n// ✅ OK:\nUI::PushFontSize(22.0);\nUI::Text(\"Big text\");\nUI::PopFontSize();\n\n// ❌ ERR (does not exist):\nUI::PushFont(UI::Font::OpenSansBold);\n```\n\n#### 🎨 No UI::TextColored — use PushStyleColor\n\n```angelscript\n// ✅ OK:\nUI::PushStyleColor(UI::Col::Text, vec4(0.3f, 1.0f, 0.5f, 1.0f));\nUI::Text(\"Green text\");\nUI::PopStyleColor();\n\n// ❌ ERR:\nUI::TextColored(color, \"text\");\n```\n\n#### 📍 Window position uses int coords\n\n```angelscript\n// ✅ OK (cast floats to int):\nUI::SetNextWindowPos(int(posX), int(posY), UI::Cond::Appearing);\n```\n\n#### 🪟 UI::Begin takes a bool reference\n\n```angelscript\nbool S_WindowOpen = false;\nif (!UI::Begin(\"My Window\", S_WindowOpen, UI::WindowFlags::NoSavedSettings)) {\n    UI::End();\n    return;\n}\n```\n\n#### ⌨️ UI::InputText — return type vs. bool&out\n\n```angelscript\n// ❌ ERR — InputText ALWAYS returns string, can't use in if():\nif (UI::InputText(\"##Input\", g_Text, changed, flags)) { }\n\n// ✅ OK:\nbool changed = false;\nUI::InputText(\"##Input\", g_Text, changed, flags);\nif (changed) { /* Enter was pressed */ }\n```\n\n---\n\n### 🖌️ NanoVG Essentials\n\nThe drawing API is per-frame immediate-mode. State persists until changed.\n\n```angelscript\nnvg::BeginPath();\nnvg::MoveTo(p1);\nnvg::LineTo(p2);\nnvg::Stroke();          // ← actually draws the path\n\nnvg::FontSize(13.0f);\nnvg::FillColor(vec4(1.0f, 1.0f, 1.0f, 0.9f));\nnvg::TextAlign(nvg::Align::Middle | nvg::Align::Center);\nnvg::Text(p, \"label\");\n```\n\n- `nvg::BeginPath()` is **required** for `LineTo`/`Rect`/`Circle`/etc.\n- `nvg::Text()` does **NOT** need `BeginPath`.\n- For minimum state changes, batch draws that share a state.\n\n---\n\n### 📷 Camera API\n\n#### 🎯 Camera::ToScreenSpace returns `vec2`, not `vec3`\n\n```angelscript\nvec2 screenPos = Camera::ToScreenSpace(worldPos);   // 2D only\n// NO z/depth component!\n```\n\nFor behind-camera test:\n\n```angelscript\nif (Camera::IsBehind(worldPos)) {\n    // skip this point\n}\n```\n\n⚠️ Don't assume a `vec3` overload exists. The `vec2` return is the only signature.\n\n#### 🚗 VehicleState::ViewingPlayerState() returns CSmPlayer\n\n```angelscript\nauto state = VehicleState::ViewingPlayerState();\nif (state is null) return;       // null when not in a vehicle / not in a map\nvec3 pos = state.Position;        // world-space player position\n```\n\nUse `is null` — AngelScript's null-comparison operator for handles.\n\n---\n\n### 🧱 Trackmania Block Dimensions\n\nStandard blocks are **32 × 32 × 8** (X × Z × Y) in world units.\n\n```angelscript\nconst float BLOCK_XZ = 32.0f;\nconst float BLOCK_Y  = 8.0f;\n\nint gx = int(Math::Floor(worldPos.x / BLOCK_XZ));\nint gy = int(Math::Floor(worldPos.y / BLOCK_Y));\nint gz = int(Math::Floor(worldPos.z / BLOCK_XZ));\n```\n\n---\n\nFile v3.0.0:references/blockable-input-callbacks-mp4.md\n\n# BlockableInputCallbacks (BIC) on ManiaPlanet 4 — verified facts (2026-08)\r\n\r\nThe `ackepta/BlockableInputCallbacks` optimizer (already shipped as\r\n`Plugins/BlockableInputCallbacks.op`, siteid 893, v0.1.0) is the input layer used\r\nby trainers (e.g. `TrackmaniaTrainer`). Facts verified by unpacking the `.op`\r\n(zip) before using it:\r\n\r\n## manifest shape (uses `[script]` module/exports, NOT `dependencies`)\r\n```toml\r\n[meta]\r\nname = \"BlockableInputCallbacks\"\r\nauthor = \"achepta\"\r\nversion = \"0.1.0\"\r\nsiteid = 893\r\n\r\n[script]\r\nmodule = \"BlockableInputCallbacks\"\r\nexports = [ \"Export.as\" ]\r\nshared_exports = [ \"Shared.as\" ]\r\n```\r\nDependency plugin window: a plugin that imports it writes\r\n`[script] dependencies = [\"BlockableInputCallbacks\"]` in ITS info.toml.\r\n\r\n## import surface (Export.as)\r\n```angelscript\r\nnamespace BlockableInputCallbacks {\r\n    import int RegisterCallback(const string &in name, IInputCallback@ callback) from \"BlockableInputCallbacks\";\r\n    import int RegisterCallback(const string &in name, IInputCallback@ callback, float activationThreshold) from \"BlockableInputCallbacks\";\r\n    import void UnregisterCallback(int id) from \"BlockableInputCallbacks\";\r\n    import void UnregisterCallbacks(const string &in name) from \"BlockableInputCallbacks\";\r\n    import void SetInputBlocked(PadInput input, bool blocked) from \"BlockableInputCallbacks\";\r\n    import void SetInputBlocked(KeyboardInput input, bool blocked) from \"BlockableInputCallbacks\";\r\n    import KeyboardInput VirtualKeyToKeyboardInput(VirtualKey key) from \"BlockableInputCallbacks\";\r\n    import VirtualKey KeyboardInputToVirtualKey(KeyboardInput input) from \"BlockableInputCallbacks\";\r\n}\r\n```\r\n\r\n## verified enum values (Shared.as)\r\n- `PadInput`: `None = 255`, then per-pad buttons.\r\n- `KeyboardInput`: `None = 0x00`; function keys **F5 = 0x3F, F6 = 0x40, F8 = 0x42**\r\n  (F7 = 0x41 by sequence).\r\n- `InputDecision`, `PadInputEvent` shared enums — usable in the importing plugin.\r\n\r\n## pitfalls\r\n- Do NOT assume TM2020's REML can't bind F5/F6/F8 the same way: verify the enum\r\n  against `Shared.as` in THIS plugin's `.op` before relying on it (values above\r\n  are for v0.1.0).\r\n- The callbacks are into-game with a `IInputCallback@` interface — if the plugin\r\n  is missing from `Plugins/` at runtime the import fails the whole plugin pencil.\r\n- Simpler zero-dependency fallback for a trainer: plain `OnKeyPress(bool down,\r\n  VirtualKey key)` with `[Setting hidden] VirtualKey` — no BIC needed unless the\r\n  key must also be BLOCKED from reaching the game.\n\nFile v3.0.0:references/cross-game-tmnt-next-mp4.md\n\n# Cross-game plugin compatibility: TMNEXT + MP4\r\n\r\nVerified facts for writing ONE Openplanet AngelScript plugin that compiles and\r\nruns on BOTH Trackmania (2020) [TMNEXT] and Maniaplanet 4 [MP4]. (From the\r\n`tm-green-timer` MP4 port, Aug 2026 — confirmed against `Openplanet4.json` and\r\n`OpenplanetNext.json`, and a clean in-game load with 0 `ERR :`.)\r\n\r\n## Preprocessor defines (Openplanet injects these per-game automatically)\r\n- `TMNEXT` — current game is Trackmania (2020)\r\n- `MP4`    — current game is Maniaplanet 4 (4.0 and 4.1)\r\n- Also available: `MP40`, `MP41`, `UNITED`, `MP3`, `TURBO` (see Openplanet docs).\r\n- No `info.toml` game flag is needed; the compiler selects the right branch.\r\n\r\n```angelscript\r\n#if TMNEXT\r\n    // TMNEXT-only API here\r\n#elif MP4\r\n    // MP4-only API here\r\n#else\r\n    // fallback (treat like MP4)\r\n#endif\r\n```\r\n\r\n## Common base class\r\n`CGameCtnApp` is the base class for both games (Openplanet docs: \"Openplanet\r\nuses the base class CGameCtnApp\"). On TMNEXT, `CGameManiaApp` DERIVES from\r\n`CGameCtnApp`, so `GetApp()` returns a `CGameCtnApp` on both. Write shared code\r\nagainst `CGameCtnApp` so it compiles everywhere.\r\n\r\n## Members present on BOTH (safe to use unconditionally)\r\nFrom `ns.Game.CGameCtnApp.m[]` in both reflection DBs:\r\n- `Switcher`          : CGameSwitcher@   (`ModuleStack` : MwFastBuffer<CGameSwitcher@>)\r\n- `Editor`            : CGameCtnEditor@\r\n- `CurrentPlayground` : CGamePlayground@\r\n- `RootMap`           : CGameCtnChallenge@\r\n- `CurrentCampaign`   : CGameCtnCampaign@\r\n\r\n## TMNEXT-only members (gate behind `#if TMNEXT`)\r\n- `app.LoadProgress` (CGameLoadProgress@) + `NGameLoadProgress::EState::Displayed`\r\n- `cast<CTrackManiaMenus>(app.Switcher.ModuleStack[0])` — NOTE: `CTrackManiaMenus`\r\n  does NOT exist on MP4 (only `CGameCtnMenus` does, and even that isn't needed).\r\n\r\n## MP4 menu/loading detection (no LoadProgress/Menu API)\r\nOn MP4 there is no `LoadProgress::EState` and no `CTrackManiaMenus`. Detect\r\n\"in menu\" vs \"loading\" with the shared API. **KEEP THE TWO STATES SEPARATE** —\r\na combined `inMenuOrLoading` check collapses `Pause In Menu` and\r\n`Pause While Loading` into one behavior and always reports \"Loading Screen\",\r\nwhich Copilot flagged in the green-timer port. Correct form:\r\n```angelscript\r\nbool inMenu = app.Switcher.ModuleStack.Length < 1;          // empty stack = in a menu\r\nbool inLoading = !inMenu && app.RootMap is null;            // stack non-empty but no map = loading\r\nif (S_PauseInMenu && inMenu) { tmpPauseReason = \"in Menu\"; return true; }\r\nif (S_PauseWhileLoading && inLoading) { tmpPauseReason = \"Loading Screen\"; return true; }\r\n```\r\nThis goes in both the `#elif MP4` and `#else` branches.\r\n\r\n## Deprecated / legacy calls to fix while porting (verified, 2026-08)\r\nThese compile but emit `DEPRECATED`/`WARN` lines in `Openplanet.log`; fixing\r\nthem yields a fully clean load (0 `ERR :` AND 0 `DEPRECATED`):\r\n- `nvg::LoadFont(\"Exo-Bold.ttf\", true, true)` → `nvg::LoadFont(\"Exo-Bold.ttf\")`.\r\n  The `fallbackIcons`/`fallbackArial` bools are deprecated; drop them (fallback\r\n  fonts are now always included). Note: the ROOT-PATH pitfall below is separate —\r\n  you still must place the font at the plugin root.\r\n- `Time::Format(int(ms), false, true, true)` (4 args) →\r\n  `Time::Format(uint64(ms), false, true, true, false)` (5 args). On MP4\r\n  `Time::Format` signature is `(uint64 time, bool fractions, bool forceMinutes,\r\n  bool forceHours, bool short)` — the original 4-arg TMNEXT form is missing the\r\n  trailing `short` bool. Use `uint64(...)` cast so the same source compiles on\r\n  both games. Fix all call sites (timer display + Saved Timers history).\r\n\r\n## Font loading pitfall (`nvg::LoadFont`)\r\n`nvg::LoadFont(\"Exo-Bold.ttf\")` resolves the path RELATIVE TO THE PLUGIN ROOT,\r\nNOT to `src/`. In a dev-folder layout the `.as` files live in `src/` but the\r\nfont must sit at the plugin root (next to `info.toml`) or it silently falls back\r\nto the default font. In a packaged `.op`, everything is flattened to the root, so\r\na font in `src/` works there but NOT in the dev folder.\r\nFix: keep font files at the plugin root and copy them there in `build.sh`:\r\n- release: add the font to the 7z list (`7z a $BUILD_NAME ./$pluginSrc/* ./Font.ttf ...`)\r\n- dev: `cp -LR -v ./Font.ttf $_build_dest/`\r\n\r\n## Verification for cross-game plugins (no offline compiler)\r\n1. Verify each branch's natives against the matching reflection DB before writing.\r\n2. Build the `.op` and/or copy the folder plugin into the game's `Plugins/` dir.\r\n3. Run the game with `/openplanet:developer` (DeveloperMode=true in Settings.ini),\r\n   kill + relaunch for a clean recompile, then grep `Openplanet.log` for\r\n   `Loaded plugin '<Name>'` and 0 `ERR :` / `Script compilation failed`.\r\n4. **Static scan the branch you cannot run:** extract the `#elif MP4` block and\r\n   grep it against `Openplanet4.json` — confirm every `app.X` / `Switcher.X`\r\n   member exists, and that NO TMNEXT-only token (`CTrackManiaMenus`,\r\n   `NGameLoadProgress`, `LoadProgress`) leaks into that branch (strip `//`\r\n   comments first, or a mention inside a comment will false-positive).\n\nFile v3.0.0:references/crossgame-tmnext-mp4.md\n\n# Cross-game porting: TMNEXT (Trackmania 2020) <-> MP4 (Maniaplanet 4)\r\n\r\nVerified while porting `tm-green-timer` (XertroV) so it runs on BOTH games\r\nwithout changing TM2020 behaviour. Openplanet injects `TMNEXT` and `MP4`\r\npreprocessor defines per game, so gate game-specific code with `#if` / `#elif`.\r\n\r\n## When to use\r\nA plugin compiles on TMNEXT but fails on MP4 (or misbehaves) because it\r\nreferences a TMNEXT-only class/namespace. Classic culprits that do NOT exist on\r\nMP4: `CTrackManiaMenus`, `NGameLoadProgress::EState`, `app.LoadProgress`.\r\n\r\n## Verified SHARED members (exist on both via `CGameCtnApp`)\r\n- `app.Switcher.ModuleStack` (MwFastBuffer<CGameSwitcher@>)\r\n- `app.RootMap` (CGameCtnChallenge@) — null when no map is loaded\r\n- `app.Editor` (CGameCtnEditor@)\r\n- `app.CurrentPlayground` (CGamePlayground@)\r\n\r\nUse these for cross-game logic; they compile on both builds. `CGameCtnApp` is\r\nthe common base class per the Openplanet docs, so `GetApp()` returns it on both.\r\n\r\n## Pattern\r\n```angelscript\r\n#if TMNEXT\r\n    // original TMNEXT-only code — keep byte-for-byte, do not touch\r\n    if (S_PauseInMenu && (app.Switcher.ModuleStack.Length < 1 || cast<CTrackManiaMenus>(app.Switcher.ModuleStack[0]) !is null)) { ... }\r\n    if (S_PauseWhileLoading && app.LoadProgress.State == NGameLoadProgress::EState::Displayed) { ... }\r\n#elif MP4\r\n    bool inMenu = app.Switcher.ModuleStack.Length < 1;\r\n    bool inLoading = !inMenu && app.RootMap is null;\r\n    if (S_PauseInMenu && inMenu) { tmpPauseReason = \"in Menu\"; return true; }\r\n    if (S_PauseWhileLoading && inLoading) { tmpPauseReason = \"Loading Screen\"; return true; }\r\n#else\r\n    // same heuristic as MP4 for any other Openplanet game\r\n    ...\r\n#endif\r\n```\r\n\r\n## RULE: never change the TMNEXT branch\r\nThe user requires TM2020 behaviour to stay identical. Only ADD an `#elif MP4`\r\nbranch; do not edit the `#if TMNEXT` block. `CTrackManiaMenus` /\r\n`NGameLoadProgress` must appear ONLY inside `#if TMNEXT` (Copilot-style reviews\r\nwill flag them if they leak into the MP4 branch).\r\n\r\n## Menu/loading detection on MP4 (verified)\r\n- `app.Switcher.ModuleStack.Length < 1`  -> in a MENU\r\n- `!inMenu && app.RootMap is null`      -> map still LOADING\r\nThis replaces `cast<CTrackManiaMenus>` (menu) and `LoadProgress::EState::Displayed`\r\n(loading), neither of which exists on MP4.\r\n\r\n## Deprecated / legacy API fixes found while porting (apply on MP4)\r\n- `nvg::LoadFont(\"x.ttf\", true, true)` -> `nvg::LoadFont(\"x.ttf\")`.\r\n  (`fallbackIcons`/`fallbackArial` are deprecated; logs `DEPRECATED: nvg::LoadFont ...`.)\r\n- `Time::Format(int(t), false, true, true)` (4 args) ->\r\n  `Time::Format(uint64(t), false, true, true, false)` (5 args on MP4:\r\n  time, fractions, forceMinutes, forceHours, short).\r\n- `if (i == activeIndex)` where `i` is `uint`, `activeIndex` is `int` ->\r\n  `if (int(i) == activeIndex)` (fixes `Signed/Unsigned mismatch` WARN).\r\n\r\n## Font loading in dev-folder layout\r\n`nvg::LoadFont(\"Exo-Bold.ttf\")` resolves relative to the PLUGIN ROOT, not `src/`.\r\nIn a dev-folder plugin keep the font at `<Plugin>/Exo-Bold.ttf`, NOT\r\n`<Plugin>/src/Exo-Bold.ttf` (the latter only works in a flattened `.op`).\r\nUpdate `build.sh` to copy the font to the root of both dev and release outputs.\r\n\r\n## Shipping: `.op` is a ZIP, not 7z\r\n`build.sh release` calls `7z a ...`. If `7z` is absent the build silently\r\nproduces no `.op`. The `.op` file is a plain ZIP archive with the plugin's `.as`\r\nfiles at the ROOT (plus info.toml, LICENSE, README, fonts). Build manually with\r\npython when 7z is missing:\r\n```python\r\nimport zipfile, os\r\nout = 'green-timer-0.2.5.op'\r\nwith zipfile.ZipFile(out, 'w', zipfile.ZIP_DEFLATED) as z:\r\n    for name, path in [('GreenTimer.as', 'src/GreenTimer.as'),\r\n                        ('Exo-Bold.ttf', 'Exo-Bold.ttf'),\r\n                        ('info.toml', 'info.toml'),\r\n                        ('LICENSE', 'LICENSE'),\r\n                        ('README.md', 'README.md'),\r\n                        ('nvg.as', 'src/nvg.as'), ('Main.as', 'src/Main.as'),\r\n                        ('History.as', 'src/History.as')]:\r\n        z.write(path, name)\r\n# verify version inside: zipfile.ZipFile(out).read('info.toml')\r\n```\r\n\r\n## Verification recipe (no offline compiler)\r\nAngelScript for MP4 has NO offline compiler. Prove a clean load by\r\nkill+relaunch+greplog (see openplanet-compile-verify-loop). After a cross-game\r\nport, ALSO statically check: extract the MP4 branch and grep for forbidden\r\nTMNEXT-only symbols, and confirm every used `app.X` member exists in\r\n`Openplanet4.json` (`ns.Game.CGameCtnApp.m[]`). A reusable checker lives at\r\n`scripts/verify-mp4-branch.py` (under this skill).\n\nFile v3.0.0:references/feedback-map-uid.md\n\n# ManiaPlanet Feedback Page — Map UID Extraction\n\nThe ManiaPlanet feedback page at `https://feedback.prod.live.maniaplanet.com/votes/display/{ID}` displays maps with their UIDs embedded in thumbnail image URLs.\n\n## URL Pattern\n\nThumbnail images follow the format:\n```\nhttps://files-v4.live.maniaplanet.com/maps/{hash}/{UID}.jpg\n```\n\nThe UID is a base64-like string (e.g., `pdHcfgrPuzYKYG84amT6KREpj97`).\n\n## JavaScript Extraction\n\nRun in browser console while on the feedback page to extract all UID-name pairs:\n\n```javascript\n(() => {\n  const imgs = document.querySelectorAll('img');\n  const h6s = document.querySelectorAll('h6');\n  const items = [];\n  const mapNames = [];\n  h6s.forEach(h => {\n    const t = h.textContent.trim();\n    if (t && t !== 'YES/NO' && t !== '5 STARS')\n      mapNames.push(t);\n  });\n  imgs.forEach(img => {\n    const src = img.src || '';\n    const m = src.match(/\\/maps\\/[a-f0-9]+\\/([a-zA-Z0-9_\\-]+)\\.(jpg|png)/);\n    if (m) items.push({uid: m[1], name: ''});\n  });\n  for (let i = 0; i < items.length && i < mapNames.length; i++)\n    items[i].name = mapNames[i];\n  return JSON.stringify(items);\n})()\n```\n\nReturns JSON: `[{\"uid\": \"...\", \"name\": \"...\"}, ...]`\n\n## Notes\n\n- Number of img elements with `/maps/` in src = number of maps on the page.\n- Map names are in h6 elements. Skip \"YES/NO\" and \"5 STARS\" headers.\n- UID order matches map name h6 order.\n- UIDs can construct direct thumbnail URLs or lookup maps on other platforms.\n\n### Reference: plugin-cleanup-workflow\n\nFile v3.0.0:references/launch-and-verify.md\n\n# info.toml Reference\n\nThe `info.toml` file specifies metadata for an Openplanet plugin. Format: [TOML](https://toml.io/en/latest).\n\n## [meta] table (Required)\n\n| Key | Type | Required | Description |\n|-----|------|----------|-------------|\n| `name` | String | Recommended | Display name of the plugin |\n| `author` | String | Recommended | Plugin author |\n| `version` | String | Yes | Version string, required for website submission |\n| `category` | String | Recommended | Grouping category (e.g. \"Utility\", \"Galaxy\", \"GUI\") |\n| `siteid` | Integer | No | Auto-added during plugin review |\n| `blocks` | String[] | No | Plugin identifiers to block (e.g., block old versions) |\n| `perms` | String | Deprecated | Use Permissions API instead (`free`, `paid`, `full`) |\n\n## [game] table (Optional)\n\n| Key | Type | Description |\n|-----|------|-------------|\n| `min_version` | String | Minimum game build date: `\"2022-02-03\"` or `\"2022-02-03 18:03\"` |\n| `max_version` | String | Maximum game build date (same format) |\n\n## [script] table (Optional)\n\n| Key | Type | Description |\n|-----|------|-------------|\n| `timeout` | Integer | Callback execution timeout in ms. `0` = disabled (no infinite-loop protection) |\n| `imports` | String[] | Script files from `Openplanet/Scripts/` to include (e.g., `[\"Dialogs.as\"]`) |\n| `exports` | String[] | Files to export to dependent plugins (compiled into dependents, not self) |\n| `shared_exports` | String[] | Like exports, but also compiled into this plugin |\n| `dependencies` | String[] | Required plugin UIDs (e.g., `[\"VehicleState\"]`). Plugin won't load if missing |\n| `export_dependencies` | String[] | Dependencies to also export to any plugin depending on this one |\n| `optional_dependencies` | String[] | Optional plugin UIDs — plugin loads without them, no compile error |\n| `defines` | String[] | Preprocessor defines (e.g., `[\"DEBUG\"]`) |\n| `module` | String | Force a specific module name (defaults to plugin identifier) |\n\n## Minimal Example\n\n```toml\n[meta]\nname = \"Minimal Plugin\"\nauthor = \"tomekdot\"\nversion = \"1.0.0\"\ncategory = \"Utility\"\n```\n\n## Full Example\n\n```toml\n[meta]\nname = \"Calendar & Events\"\nauthor = \"tomekdot\"\nversion = \"1.0.0\"\ncategory = \"Utility\"\n\n[game]\nmin_version = \"2023-01-01\"\n\n[script]\ntimeout = 5000\ndefines = [\"BETA\"]\n```\n\n### Reference: launch-commands\n\n# Openplanet Plugin Dev — launch & verify commands\n\nCopy-paste recipes for the MP4 dev loop. Paths are Windows; run the terminal\ncommands from the Hermes bash shell (POSIX paths work).\n\n## Launch the game (via cua-driver, hidden — no focus steal)\n\nMCP tool:\n```\nmcp__cua_driver__launch_app(path=\"C:\\\\Program Files (x86)\\\\ManiaPlanet\\\\ManiaPlanet.exe\")\n```\nThis opens ManiaPlanet in the background. First boot goes through Ubisoft\nConnect login + shader cache and takes ~20–40s. The window lands at z_index 0\n(behind other windows) — bring it forward with `Alt-Tab`, or raise it via\ncua-driver if you need to drive it.\n\n## Kill the game (releases any .op lock, forces a clean reload)\n\n```\ntaskkill /F /IM ManiaPlanet.exe\n```\nAlways kill before copying a fresh `.op` zip, otherwise the file is locked.\nFolder plugins don't need this for dev edits (they read source live).\n\n## Reload plugins in-game (MP4 quirk)\n\n1. Click the game window (give it focus).\n2. Press **F3** → opens the Openplanet overlay.\n3. THEN press **Ctrl+Shift+R** → reloads plugins.\n   (`Ctrl+Shift+R` alone does nothing on MP4 until F3 is open.)\nA full restart (kill + relaunch) is the most reliable reload.\n\n## Verify the plugin compiled & loaded (read the log, don't fight focus)\n\n```\n# clear log first for a clean run\n> \"C:\\Users\\tomekdot\\Openplanet4\\Openplanet.log\"\n# then after launch, check for compile errors\ngrep \"ERR :\" \"C:\\Users\\tomekdot\\Openplanet4\\Openplanet.log\" | grep -oP \"Plugins/[^/]+/\"\n# should be EMPTY for your plugin\n# confirm it loaded + debug log fired\ngrep -iE \"TMNews|maniacalendar\" \"C:\\Users\\tomekdot\\Openplanet4\\Openplanet.log\"\n# lag check (should be EMPTY)\ngrep \"laggy\" \"C:\\Users\\tomekdot\\Openplanet4\\Openplanet.log\"\n```\n\n## Sanity-check a plugin before launching (brace/paren balance)\n\n```bash\npython -c \"s=open(r'C:\\Users\\tomekdot\\Openplanet4\\Plugins\\<name>\\Main.as',encoding='utf-8').read();print('braces',s.count('{'),s.count('}'));print('parens',s.count('('),s.count(')'));print('brackets',s.count('['),s.count(']'))\"\n```\nAll three pairs must be balanced. (`python` exists; `python3` does NOT.)\n\n## Poll whether the game process is still alive\n\n```\ntasklist 2>/dev/null | grep -i maniaplanet\n```\n\n## Open the plugin UI in-game\n\n- **F3** → Openplanet overlay → Plugins list (toggle checkboxes / Settings).\n- For YouTube/news plugins: enable **Show Debug** in Settings, open the plugin\n  tab, confirm the channel log / video count is populated.\n\n### Reference: mp4-api-mismatches\n\nFile v3.0.0:references/mapforge-architecture.md\n\n# MapForge — working plugin skeleton (built July 2026)\r\n\r\nFull working example at `C:\\Users\\tomekdot\\Openplanet4\\Plugins\\MapForge\\`. Module-registry\r\narchitecture, deliberately different from Editor++ (no memory patching layer).\r\n\r\n## File map\r\n\r\n| File | Role |\r\n|---|---|\r\n| `info.toml` | metadata |\r\n| `src/Main.as` | entry: Main() registers modules; RenderMenu() toggle; RenderInterface() gated by EditorContext.Refresh() |\r\n| `src/Core/EditorContext.as` | null-safe gateway: App/Editor/Map/Cursor/PluginMapType handles; GetBlock/GetItem bounds-checked; RequestRefresh() = try AutoSave(); StripFormatCodes() helper |\r\n| `src/Core/ModuleManager.as` | abstract `MFModule` (TabName/TabIcon/Render/Update) + registry rendering UI::BeginTabBar, each module's Render wrapped in try/catch |\r\n| `src/Modules/BlockInspector.as` | filtered block list, coord/dir editing, recent-block quick access (copies block name to clipboard — `CursorBlockModel` has no value setter on MP4) |\r\n| `src/Modules/ItemInspector.as` | AbsolutePositionInMap, Yaw/Pitch/Roll sliders (deg UI ↔ rad storage), Scale, live-refresh toggle |\r\n| `src/Modules/CursorTools.as` | cursor Coord view/set clamped to Map.Size, reset to center, Dir rotate with 0..3 wraparound |\r\n| `src/Modules/CheckpointTools.as` | scans blocks+items for WaypointSpecialProperty into a table; Order editing; group-link shared Order; renumber 1..N filtered by tag contains \"checkpoint\" |\r\n| `src/Modules/Randomizer.as` | random item Yaw / position jitter / block Dir, name filter, position clamped to map bounds |\r\n| `src/Modules/DevTools.as` | hidden fields (MobilIndex, IVariant, AnchorTreeId, BlockUnitCoord) + `ExploreNod(label, nod)` integration with Openplanet Nod Explorer |\r\n\r\n## Key API details verified/used\r\n\r\n- `CGameCtnBlock::ECardinalDirections(int)` cast for BlockDir; `CGameCursorBlock::ECardinalDir(int)` for cursor Dir.\r\n- `it.ItemModel.CollectionId.GetName()`, `it.AnchorTreeId.GetName()` for MwId names.\r\n- `map.EdChallengeId` = UID; `map.TMObjective_AuthorTime` (uint(-1) = unset).\r\n- `Icons::` namespace for FontAwesome glyphs in labels; `##suffix` for unique ImGui IDs in loops.\r\n- Openplanet UI wrappers return the new value (e.g. `m_filter = UI::InputText(\"label\", m_filter)`), unlike raw ImGui out-params.\r\n\r\n## Extension pattern\r\n\r\nNew feature = subclass MFModule + one `g_modules.Register(...)` line in Main(). All game access through EditorContext only.\n\nFile v3.0.0:references/mp4-api-mismatches.md\n\n# MP4 API Mismatches — Verified Error List & Fixes\n\nCaptured while porting 3 red TM2020 plugins to ManiaPlanet 4 (MP4). Every entry\nbelow was hit in a real `Openplanet.log` compile and the fix was confirmed to\nremove the error. Read the error line as:\n`C:/Users/.../Plugins/<name>/<file>.as (LINE, COL) :  ERR : <message>`\n\n## Quick error → fix index\n\n| Error message (truncated) | Real fix on MP4 |\n|---|---|\n| `No matching symbol 'UI::Combo'` | Arrow-button selector (◀ / ▶ with `UI::Text`) |\n| `No matching symbol 'UI::Spacing'` | `UI::Text(\"\")` or `UI::Dummy(vec2(0,8))` |\n| `No matching symbol 'UI::BulletText'` | `UI::Text(\"• \" + s)` |\n| `No matching symbol 'UI::SetColumnWidth'` | Delete the call (no column-width API) |\n| `'Expression must be of boolean type, instead found void'` on `if (UI::BeginTabBar(...))` / `if (UI::BeginMenuBar())` | These return `void` in MP4 — drop the `if`, keep `UI::EndTabBar()`/`EndMenuBar()` (mind braces, see pitfall) |\n| `No matching signatures to 'Time::Parse(const string&)'` | `Time::Parse` takes no string; use the `_DaysFromCivil`/`StampFromUTC` snippet below |\n| `No matching symbol 'Time::FromUTC'` | Does not exist; build timestamp manually (snippet below) |\n| `No matching signatures to 'Text::Format(const string, int, int, int)'` | `Text::Format` takes ONE arg after the format (`Text::Format(\"%d\", x)`); concatenate the rest |\n| `'MapCheckpointPos' is not a member of 'CTrackManiaRaceInterface'` | TM2020-only; default `g_TotalCheckpoints = 1` or guard `#if TMNEXT` |\n| `No matching signatures to 'string::IndexOf(const string, int)'` | `string::IndexOf` takes ONE arg. For `s.IndexOf(needle, from)` use `s.SubStr(from).IndexOf(needle) + from` |\n| `'Type' is not a member of 'Json::Value'` (on `x.Type`) | Use `x.GetType()` (method), keep `Json::Type::Array`/`::Object`/`::String` enums |\n| `No matching signatures to 'Math::RandSeed(...)'` / `Math::Rand()` | `Math::Rand(0, 1000000)` (needs min,max). For a seed, loop `Math::Rand(0,1000000)` N times |\n| `No matching symbol 's.Substring'` | `s.SubStr(i, len)` |\n| `not a member` on `art.Article.BlockModel` / `art.BlockModel` / `art.CursorBlockModel` | `CGameCtnArticleNodeArticle` → `.Article` (a `CGameCtnArticle`); block name is `art.Article.Name` (wstring → `string(...)`) |\n| type mismatch on `PlaceBlock(bi, pos, Dir)` | wrap dir: `CGameEditorPluginMap::ECardinalDirections(int(dir))` (N=0,E=1,S=2,W=3) |\n| `No matching signatures to 'Json::Array(a,b,c)'` | `Json::Array()` 0-arg + `.Add(Json::Value(x))` |\n| `non-function type int64` on `Time::Stamp()` | `Time::Stamp` is a field, not a call → drop the parens |\n| `Identifier 'EMapElemColor' / 'EMapElemColorPalette' is not a data type` | TM2020-only enums; define local enums OR guard with `#if TMNEXT` |\n| `No matching symbol 'Permissions::OpenAdvancedMapEditor'` | TM2020-only |\n| `'ExperimentalFeatures' is not a member of 'CGameCtnEditorFree'` | TM2020-only |\n| `'NextMapElemColor' / 'MapElemColorPalette' is not a member of 'CGameEditorPluginMap'` | TM2020-only |\n| `No matching symbol 'PlaceGhostBlock'` / `RemoveGhostBlock` / `PlaceMacroblock_AirMode` | TM2020-only |\n| `No matching signatures to 'CreateMacroblockInstance(..., EMapElemColor&, ...)'` | TM2020 signature; MP4 variant takes no color arg |\n\n## ISO 8601 → Unix timestamp (MP4-safe, no Time::FromUTC)\n\nDrop-in replacement for `Time::Parse(iso)`. Handles `YYYY-MM-DDTHH:MM:SS`\n(optional `Z` / `+HH:MM` ignored). Returns milliseconds.\n\n```angelscript\n// Hinnant's days_from_civil algorithm (MP4-safe, no Time::FromUTC)\nint _DaysFromCivil(int y, int m, int d) {\n    y -= m <= 2 ? 1 : 0;\n    int era = (y >= 0 ? y : y - 399) / 400;\n    int yoe = y - era * 400;\n    int doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1;\n    int doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;\n    return era * 146097 + doe - 719468;\n}\n\nint64 StampFromUTC(int Y, int M, int D, int h, int mi, int s) {\n    int days = _DaysFromCivil(Y, M, D);\n    return int64(days) * 86400 + int64(h) * 3600 + int64(mi) * 60 + int64(s);\n}\n\nint64 ParseIsoToMs(const string &in iso) {\n    if (iso.Length < 19) return 0;\n    int Y  = Text::ParseInt(iso.SubStr(0, 4));\n    int M  = Text::ParseInt(iso.SubStr(5, 2));\n    int D  = Text::ParseInt(iso.SubStr(8, 2));\n    int h  = Text::ParseInt(iso.SubStr(11, 2));\n    int mi = Text::ParseInt(iso.SubStr(14, 2));\n    int s  = Text::ParseInt(iso.SubStr(17, 2));\n    return StampFromUTC(Y, M, D, h, mi, s) * 1000;\n}\n```\n\n## Verify-in-loop\n\n1. `taskkill /F /IM ManiaPlanet.exe`\n2. patch source / rebuild `.op` (or edit extracted `Plugins/<name>/` folder directly)\n3. `> Openplanet4/Openplanet.log`  (clear, so old timestamps don't fool you)\n4. relaunch game (`python tools/debug_launch.py --wait 90` for TM2TrackGen, or start ManiaPlanet.exe)\n5. `grep \"ERR :\" Openplanet4/Openplanet.log | grep -i <pluginname>`\n\nA stale `.op` held by the running game will re-show the SAME old errors even after\nyou patched the source — that is the file lock, not a broken patch. Kill the game.\n\n---\n\n## Session additions (2nd pass)\n\n### The \"known-good plugin\" oracle technique\n\nWhen unsure whether an API exists on MP4, **don't trust the abridged API txt** and\ndon't guess — grep a plugin that already loads clean on MP4. `event-calendar-dev`\nis the best oracle in this install:\n\n- `grep -rn \"Time::FromUTC\\|Json::Type\\|GetType\" .../event-calendar-dev/` told us\n  `Json::Type` (via `.GetType()`) and `Time::FromUTC` usage patterns are valid.\n- This caught a wrong turn: an earlier edit changed `Json::Type` → `Json::JsonType`\n  (wrong), but grepping `event-calendar-dev` showed `Json::Type::String` is correct.\n  Always confirm against a working plugin before committing a fix.\n\n### Brace-matching pitfall (void UI calls)\n\nChanging `if (UI::BeginTabBar(\"X\")) { ... UI::EndTabBar(); }` to the MP4-safe\n`UI::BeginTabBar(\"X\"); { ... UI::EndTabBar(); }` requires keeping the manual `{`\nyou added CLOSED. The safe edit:\n\n```\n// BEFORE (TM2020)\nif (UI::BeginTabBar(\"MainTabs\")) {\n    if (UI::BeginTabItem(...)) { ... }\n    UI::EndTabBar();\n}\n// AFTER (MP4) — keep the { and its matching }\nUI::BeginTabBar(\"MainTabs\"); {\n    if (UI::BeginTabItem(...)) { ... }\n    UI::EndTabBar();\n}\n```\n\nIf you drop the `if` but forget to re-add the `{`, you get `Unexpected end of file`\nor `Expected ';'` / `Instead found identifier 'Main'` because the trailing\n`UI::End()` / namespace close gets mis-scoped. After ANY brace edit, run a balance\ncheck: `python -c \"s=open('Main.as',encoding='utf-8').read();print(s.count('{'),s.count('}'))\"`\n— they must be equal. (`python3` is NOT on this machine; use `python`.)\n\n### Disabling a plugin — the `info.toml` trap\n\nWriting `disabled = true` under `[meta]` in `info.toml` is **IGNORED** by\nOpenplanet — the plugin still loads (and still shows red if broken). To stop a\nTM2020-only plugin from loading on MP4:\n\n- Move the folder: `mv Plugins/<name> Plugins-Archive/<name>` (Openplanet never\n  loads from `Plugins-Archive/`). This is how `TrackGeneratorExtended` (deep\n  TM2020-only) was retired in favour of the MP4-native `TM2TrackGen`.\n- Or disable via the Openplanet UI (F3 → Plugins), which writes\n  `Plugins-<name>=false` into `Openplanet4.json` / `Settings.ini`.\n\n### Reference: mp4-ui-rendering\n\nFile v3.0.0:references/mp4-api-verified.md\n\n# MP4 AngelScript verified API facts (from the MapForge build, July 2026)\n\nThese were hit compiling a real ManiaPlanet 4 plugin and confirmed against the\nreflection DB Openplanet writes to `Openplanet4/Openplanet4.json` on launch\n(`ns.Game.<ClassName>.m[]` — each entry has `n`=member name, `t`=type). Treat\nthat JSON as ground truth: grep it before trusting any member name. All of these\nproduce COMPILE ERRORS if you use the TM2020 form.\n\n| You wrote | MP4 reality | Correct form |\n|---|---|---|\n| `char(c)` / `uint('A')` char literal | `char` type and char-literal casts do NOT exist | bytes as `uint`; A-Z = 65..90; append `input.SubStr(i,1)` to build strings |\n| `Map.Blocks is null` / `Map.AnchoredObjects is null` | `MwFastBuffer<T>` is a **value type**, never a handle | guard only `if (Map is null)`, then `i >= Map.Blocks.Length` |\n| `tostring(uintValue)` | `tostring(uint)` is AMBIGUOUS (uint→int and uint→float both match) | `Text::Format(\"%d\", int(v))` — cast to int first; wrap in `I2S(uint)` |\n| `s.ToLower()` / `string::ToLower` | no such method on MP4 | manual `LCByte` A-Z→a-z compare; use a `ContainsCI(haystack, needle)` helper |\n| `UI::InputInt3(label, int3)` | does NOT exist (`UI::InputFloat3` DOES) | three `UI::InputInt(\"X\",x); UI::SameLine();` for Y, Z |\n| `UI::BeginTable(id, cols, flags)` | returns `void` on MP4; table API unreliable | `UI::BeginChild` + `UI::SameLine` panels, or scrollable `BeginChild` list |\n| `UI::Columns(n)` / `UI::NextColumn()` | unreliable/absent on MP4 | `BeginChild` + `SameLine` |\n| `UI::CollapsingHeader(label, UI::TreeNodeFlags::X)` | single `string` arg only | `UI::CollapsingHeader(label)` |\n| `UI::BeginTabBar(id, UI::TabBarFlags::X)` | `string id` only | `UI::BeginTabBar(id);` |\n| `b.IsClip` on `CGameCtnBlock` | member does NOT exist (only `IsGround`) | show IsGround only |\n| `it.IVariant` on `CGameCtnAnchoredObject` | member does NOT exist (`Scale` DOES) | use `Scale`; drop `IVariant` |\n| `CGameCursorBlock::ECardinalDir(d)` | enum is `ECardinalDirEnum` | `CGameCursorBlock::ECardinalDirEnum(d)` |\n| `ctx.PluginMapType.CursorBlockModel = model` | `CGameCtnBlockInfo` has no value `opAssign` | cannot set directly; `IO::SetClipboard(model.Name)` or use editor input |\n| `it.ItemModel.CollectionId.GetName()` | `CollectionId` is `UnnamedEnum` (no GetName) | show `it.ItemModel.Name` only |\n| `cond ? obj.Name : \"?\"` when `Name` is `wstring` | `wstring` vs `string` → no common ternary type; `string(\"?\")` alone still ambiguous | `cond ? string(obj.Name) : string(\"?\")` (cast wstring branch to string). Plain `+` auto-converts wstring, so non-ternary `UI::Text(\"x \"+obj.Name)` is fine. `CGameCtnBlockInfo.Name` is `wstring` on MP4. |\n| `CGameCtnBlockInfo.Name` type | it is `wstring` (not `string`) on MP4 | use `string(b.BlockModel.Name)` when you need a string |\n| plain folder plugin refuses to load | needs developer signature mode | launch `ManiaPlanet.exe /openplanet:developer` |\n\n## Members CONFIRMED present on MP4 (safe to use)\n- `CGameCtnBlock`: `CoordX/Y/Z` (uint), `BlockDir` (ECardinalDirections), `IsGround`, `BlockInfoVariantIndex`, `MobilIndex`, `WaypointSpecialProperty` (CGameWaypointSpecialProperty@), `BlockModel` (CGameCtnBlockInfo@), `Coord` (nat3)\n- `CGameCtnAnchoredObject`: `BlockUnitCoord` (nat3), `AnchorTreeId` (MwId, has GetName()), `AbsolutePositionInMap` (vec3), `Yaw/Pitch/Roll` (float, radians), `Scale` (float), `ItemModel` (CGameItemModel@), `WaypointSpecialProperty`\n- `CGameCursorBlock`: `Coord` (nat3), `Dir` (ECardinalDirEnum), `AdditionalDir` (EAdditionalDirEnum)\n- `CGameEditorPluginMap`: `CursorBlockModel` (readable), `GetBlockModelFromName(name)` → CGameCtnBlockInfo@, `Blocks` (MwFastBuffer<CGameCtnBlock@>), `AutoSave()`\n- `CGameCtnChallenge`: `EdChallengeId` (string), `Size` (nat3), `Blocks`, `AnchoredObjects`, `AuthorNickName` (wstring), `MapName` (wstring)\n- `CGameWaypointSpecialProperty`: `Tag` (string), `Order` (uint)\n- Helpers that exist: `ExploreNod(label, nod)`, `IO::SetClipboard`, `Math::Rand(min,max)` (both args), `Math::ToRad/ToDeg`, `Text::Format`\n\n## Verified MUTATION API (CGameEditorPluginMap) — the only valid edit surface\n\nEditor++ (TM2020) and old skill references hallucinate `CGameCtnEditorBase.AddBlock`,\n`ItemNew`, `AddItem` — **these do NOT exist on MP4**. All map mutation goes through\n`CGameEditorPluginMap` (reachable as `Editor.PluginMapType`, typed as the parent class).\n`EditorContext.PluginMapType` is `CGameEditorPluginMap@`.\n\n| Action | Verified call | Notes |\n|---|---|---|\n| Undo / Redo | `pm.Undo()` / `pm.Redo()` → bool | no args |\n| Place block by NAME+coord | `pm.GetBlockModelFromName(str)` → CGameCtnBlockInfo@; `pm.PlaceBlock(model, int3, ECardinalDirections)` → bool | model from name works — no cursor setter needed |\n| Preview placement | `pm.CanPlaceBlock(model, int3, ECardinalDirections, bool OnGround, uint Variant)` → bool | |\n| Remove block | `pm.RemoveBlock(int3 Coord)` → bool | by coordinate, not handle |\n| Remove item | `pm.RemoveItem(CGameCtnEditorScriptSpecialProperty@)` → bool | **fragile**: `Items` buffer is `CGameCtnEditorScriptAnchoredObject@`, type mismatch; resolve via `cast<CGameCtnEditorScriptSpecialProperty>(it.WaypointSpecialProperty)`, often null on MP4 |\n| Bulk delete | `RemoveAllBlocks()`, `RemoveAllObjects()`, `RemoveAllTerrain()`, `RemoveAll()` | no args |\n| Copy/paste | `CopyPaste_Copy/Cut/Remove/SelectAll/ResetSelection()`, `CopyPaste_Symmetrize()` → bool | |\n| Validate / test | `Validate()`, `TestMapFromStart()`, `TestMapFromCoord(int3, ECardinalDirections)`, `TestMapWithMode(wstring)` | |\n| Save | `SaveMap(wstring FileName)` | plain string accepted via implicit conversion |\n| Camera (writable) | `CameraVAngle`, `CameraHAngle`, `CameraToTargetDistance`, `CameraTargetPosition` (vec3), `CameraPosition` (vec3) | floats/vec3 |\n| Modes (writable) | `PlaceMode` (EPlaceMode), `EditMode` (EditMode), `UndergroundMode` (bool), `BlockStockMode` (bool) | |\n\nEnum values (verified):\n- `ECardinalDirections` (on `CGameEditorPluginMap`): `North, East, South, West` = 0..3.\n- `EPlaceMode`: Unknown, Terraform, Block, Macroblock, Skin, CopyPaste, Test, Plugin, CustomSelection, OffZone, BlockProperty, Path, GhostBlock, Item, Light.\n- `EditMode`: Unknown, Place, FreeLook, Erase, Pick, SelectionAdd, SelectionRemove.\n\nValue types are constructible: `int3(x,y,z)`, `nat3(x,y,z)`, `vec3(x,y,z)`.\n\n## Reflection-DB grep recipe\n```\npython -c \"import json,io; d=json.load(io.open(r'C:/Users/tomekdot/Openplanet4/Openplanet4.json',encoding='utf-8')); c=d['ns']['Game']['CGameCtnBlock']['m']; print('\\n'.join('%s  %s'%(m['n'],m['t']) for m in c))\"\n```\n\n## Items, variants, terrain, map metadata (verified 2026-07-27)\n- `pm.Items` -> `MwFastBuffer<CGameCtnEditorScriptAnchoredObject@>` (editable wrapper).\n  Each item exposes: `AbsolutePositionInMap` (vec3), `Yaw`/`Pitch`/`Roll` (float),\n  `Scale` (float), `ItemModel` (`CGameItemModel@`).\n- `RemoveItem(CGameCtnEditorScriptSpecialProperty@ Item) -> bool`.\n- **NO `PlaceItem` / `GetItem` / `AddItem` exist anywhere.** Cannot create new items\n  programmatically — only iterate/modify/remove existing. \"Scatter new items\" must be\n  done via blocks (`PlaceBlock`) instead.\n- `CGameCtnBlock` variant fields: `BlockInfoVariantIndex` (uint), `MobilVariantIndex` (uint).\n  `CGameCtnBlock.Coord` (nat3), `Dir` (`ECardinalDirections`), `BlockModel`, `Skin`, `IsGround`.\n- `CGameCtnBlockInfo` flags present: `IsTerrain` (bool), `IsRoad` (bool), `Name` (wstring).\n  Does NOT expose `IsGround`/`IsSkinnable`/`IsDeco`/`IsWall`/`IsPlatform`/`Variants`.\n- Skinnability: use `pm.IsBlockModelSkinnable(CGameCtnBlockInfo@) -> bool`,\n  `pm.GetBlockModelSkin(model, uint) -> wstring`, `pm.SetBlockSkin(CGameCtnBlock@, wstring) -> void`.\n- `pm.SaveMap(wstring FileName) -> void` (FULL path; creates file). `pm.AutoSave()`, `pm.Validate()` (no args).\n- Terrain: `pm.PlaceTerrainBlocks(CGameCtnBlockInfo@, int3 Start, int3 End) -> bool`;\n  `pm.RemoveTerrainBlocks(int3 Start, int3 End) -> bool`;\n  `pm.GetBlockGroundHeight(CGameCtnBlockInfo@, int X, int Z, ECardinalDirections) -> uint`.\n- Reach map: `pm.Map` -> `CGameCtnChallenge`: `Blocks`, `AnchoredObjects`, `Size` (nat3),\n  `MapName`, `AuthorLogin`, `AuthorNickName`, `Comments`.\n- `Time::` / `IO::` / `UI::` / `Json::` are Openplanet BUILT-INS, NOT in the Nadeo\n  reflection DB. The REAL signatures (verified in OpenplanetCore.json, 2026-07-27)\n  differ from older skill notes — use these:\n  - IO: `FromUserGameFolder(string)->string`, `FromAppFolder`, `FromDataFolder`,\n    `FromStorageFolder`, `FileExists`, `FileSize`, `FolderExists`, `CreateFolder(path, bool recursive=true)`,\n    `DeleteFolder`, `Delete`, `Copy`, `Move`, `IndexFolder(path, bool recursive)->string[]@`,\n    `SetClipboard(string)`. **NO `WriteFile` / `FileWrite` / `GetUserDir` / `DirectoryList` /\n    `CreateDirectory`** — there is NO raw-text file writer in this OP build.\n  - Time: `Time::Now` (uint64), `Time::FormatString(string fmt, int64 stamp=-1)->string`\n    (strftime-style, e.g. \"%Y-%m-%d_%H-%M-%S\"). `Time::Format(uint64, bool...)` exists but\n    takes bools, NOT a format string.\n  - Json: `Json::Parse(string)->Value@`, `Json::ToFile(string, const Value@, bool pretty)`,\n    `Json::FromFile(string)->Value@`, `Json::Object()`, `Json::Array()`.\n  - UI: `BeginCombo(label, current)->bool` + `EndCombo()` + `Selectable(label, bool)`,\n    `BeginTable(id, columns, flags)`, `BeginTabBar`/`BeginTabItem`. **NO `UI::ListBox` /\n    `UI::CollapsingHeader(label, flags)`** (older notes were wrong).\n  - Icons: `Icons::*` IS valid (FontAwesome subset) but ONLY specific names compile —\n    verified-valid examples: Wrench, Cube, Cubes, Th, PaintBrush, History, MousePointer,\n    Clipboard, VideoCamera, Eye, Crosshairs, ArrowDown, MapMarker, SearchPlus, SearchMinus,\n    Eraser, Plus, FolderOpen, List, FileCodeO, Clock, Exchange, Leaf, Repeat, Refresh,\n    Undo, Gamepad, Tree, Random, Search, Trash, Sitemap, Retweet, PuzzlePiece, Play, Pencil,\n    Link, Flask, FlagCheckered, Check, Bolt, BarChart, ArrowsAlt. **INVALID: `Icons::Save`,\n    `Icons::Brush`** (use text-only or a valid one).\n  - `string(uint)` and `char` are ILLEGAL. Build strings via `Text::Format(\"%d\", int(v))`\n    or `SubStr` concatenation; never `string(uint8)`.\n  - `MwFastBuffer<T@>` is a VALUE type — you CANNOT take a handle `@` to it.\n    Write `MwFastBuffer<CGameCtnBlock@> blocks = pm.Map.Blocks;` (value), NOT\n    `MwFastBuffer<...>@ blocks = ...`. Handles on the BUFFER error with\n    \"Object handle is not supported for this type\" / \"Can't convert to int\".\n  - `array<T>@` sorting: use `arr.Sort(function(a,b){ return a.x < b.x; })` — there is\n    NO `arr.sort(...)` (lowercase). The comparator must return bool (a less-than b).\n  - `pm.Items` (CGameCtnEditorScriptAnchoredObject) exposes only `Position` (vec3),\n    READ-ONLY (no set accessor). You CANNOT edit item transform (Yaw/Scale/Position)\n    programmatically on MP4. Item mode = inspect only.\n  - `CGameCtnBlock.CoordX/Y/Z` (uint) and `Coord` (nat3) both exist; for math use\n    `float(b.CoordX)` etc. `GetBlock` takes `int3`, NOT `nat3`.\n\nArchive v2.2.1: 40 files, 206132 bytes\n\nFiles: references/api-namespaces.md (24292b), references/api-quirks-and-pitfalls.md (7832b), references/blockable-input-callbacks-mp4.md (2489b), references/cross-game-tmnt-next-mp4.md (5055b), references/crossgame-tmnext-mp4.md (4571b), references/feedback-map-uid.md (1501b), references/info-toml-reference.md (2293b), references/launch-and-verify.md (4822b), references/maniaplanet-feedback-extraction.md (1460b), references/mapforge-architecture.md (2401b), references/mp4-api-mismatches.md (7277b), references/mp4-api-verified.md (11175b), references/mp4-runtime-and-dev-memory.md (6671b), references/mp4-supplement.md (12281b), references/mp4-trainer-scaffold.md (6142b), references/mp4-ui-rendering.md (6155b), references/op-package-analysis-and-porting.md (3554b), references/OpenPlanet-API-Reference.md (24254b), references/OpenPlanet-Basic-API.md (41885b), references/Openplanet-Changelog-API.md (15882b), references/openplanet-core-json.md (5436b), references/openplanet-docs-dump.md (193517b), references/OpenPlanet-Global-API.md (72662b), references/Openplanet-Starter-API.md (60354b), references/openplanet-ui-menu-and-scaling.md (4129b), references/patterns-performance-ui.md (18803b), references/plugin-cleanup-recipes.md (8228b), references/plugin-skeleton.as (1211b), references/plugin-standardization.md (6305b), references/review-evidence-precedents.md (6546b), references/review-failure-ledger.md (4629b), references/static-verify-workflow.md (5025b), references/time-api.md (2536b), scripts/hermes-verify-mapforge.sh (1745b), scripts/static-preflight-plugin.py (7548b), scripts/verify-mp4-branch.py (2282b), skill-card.md (3295b), SKILL.md (16520b), templates/ui-window-template.as (2667b), _meta.json (140b)\n\nFile v2.2.1:SKILL.md\n\n---\nname: openplanet-plugin-dev\ndescription: Create, debug, structure, and run Openplanet AngelScript plugins for Trackmania 2020 (TMNEXT) and ManiaPlanet 4 (TM2/MP4). Covers API quirks, AngelScript pitfalls, performance patterns, the launch/verify loop, MP4 API-mismatch fixes, and proven templates. Use when building, debugging, reviewing, or launching Openplanet plugins.\nversion: 3.0.0\nmetadata:\n  openclaw:\n    tags: [openplanet, trackmania, angelscript, plugin, game-modding]\n---\n\n# Openplanet Plugin Development\n\nCore guide for Openplanet AngelScript plugins on **TM2020 (TMNEXT)** and\n**ManiaPlanet 4 (MP4/TM2)**. Deep material lives in `references/` — load only\nwhat the task needs (index at the bottom).\n\n## Golden rules\n\n1. **Verify API members against the reflection DB before writing code.**\n   - **Game Nod classes** live in `<GameDir>/Openplanet4.json` under\n     `ns.Game.<Class>.m[]` (entries `n`=name, `t`=type, `p`=parent). Walk it with\n     the procedure in `references/static-verify-workflow.md`.\n   - **Openplanet script namespaces** (`UI::`, `IO::`, `Time::`, `Json::`,\n     `Math::`, `Text::`, `Net::`, `Audio::`, `Icons::`, `getExceptionInfo()`) are\n     **NOT in `Openplanet4.json`**. They live in **`OpenplanetCore.json`** (same\n     game dir) under `functions[]` (filter by `ns`), `classes[]`, `enums[]`.\n     Extraction recipes + verified signatures: `references/openplanet-core-json.md`.\n     **Before assuming a built-in exists (e.g. `IO::WriteFile`, `UI::ListBox`,\n     `Time::Format(string)`), grep `OpenplanetCore.json`** — false assumptions\n     here cost a full kill+relaunch recompile cycle each time.\n   - MP4 game-class specifics: `references/mp4-api-verified.md` (editor) and\n     `references/mp4-runtime-and-dev-memory.md` (in-race / playground).\n   - **A class can appear in the DB with an EMPTY `m[]`** (e.g.\n     `ns.Scene.CSceneVehicleVis` is `{\"i\":...,\"s\":3464}` with no members). That is\n     not a lookup failure — it means the class has no scriptable named members and\n     the only way in is raw `Dev::GetOffset*` / `Dev::SetOffset` against its byte\n     size. Report that honestly instead of inventing member names.\n2. **Target game matters.** TM2020 examples often DON'T compile on MP4 (missing\n   UI overloads, missing members, wstring vs string). MP4 specifics:\n   `references/mp4-api-verified.md`, `references/mp4-api-mismatches.md`,\n   `references/mp4-supplement.md`, `references/mp4-ui-rendering.md`.\n3. **Unsigned folder plugins need Developer signature mode.** Launch with\n   `ManiaPlanet.exe /openplanet:developer` (or F3 → Developer → Signature Mode).\n   Otherwise the log shows `Invalid file hash` + `not suitable for the current\n   signature mode` and the plugin silently doesn't appear.\n4. **Debug via the log, not the GUI.** Clear `Openplanet.log`, launch, wait\n   ~50 s, then grep for `ERR :` / `compilation failed` / `Loaded plugin`.\n   Full command recipes: `references/launch-and-verify.md`.\n5. **A running game holds the compiled state.** After editing sources, kill the\n   game (`taskkill /F /IM ManiaPlanet.exe`) and relaunch for a clean recompile —\n   stale errors in the log mean the old build, not a failed patch.\n6. **Null-safe editor access.** Route all `App.Editor` access through one\n   gateway that re-checks every handle each frame; wrap each UI module in\n   try/catch. Proven skeleton: `references/mapforge-architecture.md`.\n7. **Cross-game (TMNEXT + MP4) plugins.** One plugin can target BOTH games via\n   the predefined `TMNEXT` / `MP4` preprocessor defines (Openplanet injects them\n   per-game; no `info.toml` flag needed). `CGameCtnApp` is the shared base, so\n   `app.Switcher.ModuleStack`, `app.RootMap`, `app.Editor`, `app.CurrentPlayground`\n   work on BOTH. Gate TMNEXT-only API (`cast<CTrackManiaMenus>`,\n   `NGameLoadProgress::EState`, `app.LoadProgress`) behind `#if TMNEXT` and add an\n   `#elif MP4` branch. **Keep TMNEXT and MP4 pause reasons/settings SEPARATE** —\n   do NOT collapse `Pause In Menu` + `Pause While Loading` into one\n   `inMenuOrLoading` check (Copilot flagged exactly this in the green-timer port:\n   a combined `ModuleStack<1 || RootMap is null` with `return true` always reports\n   \"Loading Screen\" and lets \"menu\" setting pause during load). Correct MP4 form:\n   `bool inMenu = app.Switcher.ModuleStack.Length < 1;`\n   `bool inLoading = !inMenu && app.RootMap is null;` then two separate `if`s.\n   Deprecated calls to fix while porting: `nvg::LoadFont(\"x.ttf\", true, true)` →\n   `nvg::LoadFont(\"x.ttf\")`; `Time::Format(int, bool, bool, bool)` →\n   `Time::Format(uint64, bool fractions, bool forceMinutes, bool forceHours, bool short)`.\n   Verified pattern, shared-member list, deprecated fixes, and the\n   `nvg::LoadFont` root-path pitfall: `references/crossgame-tmnext-mp4.md`.\n 8. **Never hardcode memory offsets you did not verify on THIS build.** `Dev::`\n read/write/hook is fully available on MP4, but offsets are build-specific.\n Discover them with a live correlation panel, freeze them behind a load-time\n self-check (re-read a known field and compare it to a reflected value), and\n fall back to read-only mode when the check fails — a bad `SetOffset` into a\n physics object crashes the game, not just the plugin. Procedure:\n `references/mp4-runtime-and-dev-memory.md` §6.\n 9. **A plugin whose logic lives in a bundled native `.dll` is a REWRITE, not a\n port.** Unpack the `.op` (it is a ZIP) and check whether the `.as` files are\n just an `Import::GetLibrary` shell. If so, say that up front instead of\n promising a port — the offsets target a different executable and no sources\n ship in the package. Analysis recipe + verdict/brief template:\n `references/op-package-analysis-and-porting.md`.\n 10. **Prefer dynamic, runtime-observed game state over static/parsed lists.**\n The user explicitly asks for this (\"statyczne checkpointy → zrób dynamiczne\").\n Example: bucket by the live `CurTriggerIndex` the driver actually crosses\n rather than a precomputed checkpoint table; it adapts to any map, multilap\n included. See `references/mp4-runtime-and-dev-memory.md` §7. Offer the static\n form as an explicitly-labelled *fallback* in the same module, never as the\n primary design — the user accepts \"static if dynamic proves impossible\", which\n is not the same as being handed static first.\n 11. **In-plugin AngelScript beats any external process for reading game\n state.** When asked \"can this be done in Python / .NET instead\", the deciding\n fact is pointer acquisition: Openplanet hands you the live nod via `GetApp()`\n for free, so `Dev::GetOffset(nod, off)` is one line. An external Python\n `ctypes`/`pymem` or .NET process must first *find* that same object by scanning\n process memory — hours of work for something already solved. Answer with that\n comparison (a 3-row table works well), then keep Python for offline tooling:\n reflection-DB queries, code generation, Kanban seeding, and the static\n pre-flight gate. Do not accept \"rewrite it in Python\" as a way to avoid .NET\n when the real answer is \"neither — it belongs in the plugin\".\n 12. **Stage delivery as Tier 1 (safe) then Tier 2 (memory writes).** Tier 1 =\n read-only reflection + teleport-style restore, ships immediately and cannot\n crash the game. Tier 2 = velocity/physics restore via `Dev::SetOffset`, gated\n behind a load-time offset self-check with automatic Tier-1 fallback. Ship and\n verify Tier 1 first; it also proves the nod chain before you build anything on\n top of it. A read-only telemetry tab is the cheapest possible proof that the\n whole context chain resolves — build it first and ask the user to confirm the\n numbers move before writing further modules.\n\n## Project documentation convention (where knowledge lives)\n\nFor OpenPlanet plugin projects **on disk**, the user's preferred layout:\n\n- **`IDEA.md` (project root) = project catalog only.** List *what each plugin/project\n  does* (name → one-line description + module map). It is NOT a dumping ground for\n  API knowledge or verified facts.\n- **API knowledge → `README.md` + this skill's `references/`** (esp.\n  `references/mp4-api-verified.md`). Put the verified member list, error→fix table,\n  and the \"no `MapForge.*` SDK / no Lua-C# / `MwFastBuffer` is a value type\" facts in\n  README (a summary) and in `references/`.\n- Do **not** keep API facts in `IDEA.md`; keep `IDEA.md` a plain index so a future\n  session finds projects fast without re-learning the API.\n\n## Plugin skeleton (callback style)\n\n```\nMyPlugin/\n├── info.toml        # metadata (see references/launch-and-verify.md §info.toml)\n└── src/\n    ├── Main.as      # Main(), Render(), RenderMenu(), OnDestroyed()\n    ├── Core/        # context/gateway, module manager\n    └── Modules/     # one feature per file\n```\n\n```angelscript\n// Main.as — minimal\n[Setting name=\"Show window\"] bool S_ShowWindow = true;\n\nvoid Main() { /* init; yield in loops */ }\n\nvoid RenderMenu() {\n    if (UI::MenuItem(\"\\\\$0f0\" + Icons::Wrench + \" MyPlugin\", \"\", S_ShowWindow))\n        S_ShowWindow = !S_ShowWindow;\n}\n\nvoid Render() {\n    if (!S_ShowWindow) return;\n    if (UI::Begin(\"MyPlugin\", S_ShowWindow)) {\n        // guard editor access every frame:\n        auto app = cast<CGameCtnApp>(GetApp());\n        auto editor = cast<CGameCtnEditorFree>(app.Editor);\n        if (editor is null) UI::Text(\"Open the map editor.\");\n        else { /* modules */ }\n    }\n    UI::End();\n}\n```\n\n`info.toml` minimum:\n\n```toml\n[meta]\nname = \"MyPlugin\"\nauthor = \"you\"\ncategory = \"Map Editing\"\nversion = \"1.0.0\"\n\n[script]\ntimeout = 5000\n```\n\n## Top MP4 compile-error killers (details in references/mp4-api-verified.md)\n\n- `tostring(uint)` ambiguous → `Text::Format(\"%d\", int(v))`\n- no `char()` / char literals → byte math (`'A'`=65, `'Z'`=90)\n- `MwFastBuffer` is a value type → never `Buf is null`\n- ternary `cond ? x.Name : \"?\"` with `wstring Name` → `string(x.Name)` both branches\n- `UI::InputInt3`, `UI::BeginTable`, `UI::Columns`, flag args on\n  `CollapsingHeader`/`BeginTabBar` → don't exist; use `BeginChild`+`SameLine`\n- no `string::ToLower` → manual byte compare helper\n\n## Reference index (load on demand)\n\n| File | When to load |\n|---|---|\n| `references/mp4-api-verified.md` | **Always for MP4 EDITOR work** — verified member list + error→fix table |\n| `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 |\n| `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). |\n| `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 |\n| `references/mp4-api-mismatches.md` | More MP4 error→fix cases (time API, ISO 8601, session notes) |\n| `references/mp4-supplement.md` | MP4/TM2 platform supplement (differences vs TM2020) |\n| `references/mp4-ui-rendering.md` | MP4 UI limits (no images, layout primitives that work) |\n| `references/api-quirks-and-pitfalls.md` | General AngelScript/Openplanet quirks (both games) |\n| `references/patterns-performance-ui.md` | Performance, diagnostic UI, NanoVG, JSON persistence, public API namespace |\n| `references/api-namespaces.md` | Namespace/function reference (UI, Time, Text, IO, Net, Json, nvg, Math…) |\n| `references/launch-and-verify.md` | info.toml reference + launch/kill/reload/log-verify command recipes |\n| `references/mapforge-architecture.md` | Proven MP4 editor-plugin skeleton (module registry + safe context) |\n| `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. |\n| `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. |\n| `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. |\n| `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`). |\n| `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). |\n| `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. |\n| `references/plugin-standardization.md` | Naming/structure conventions |\n| `references/openplanet-ui-menu-and-scaling.md` | Menu integration + UI scaling |\n| `references/plugin-cleanup-recipes.md` | Removing features/plugins cleanly, visibility recipes |\n| `references/feedback-map-uid.md` | ManiaPlanet feedback page map-UID extraction |\n| `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 |\n| `references/openplanet-docs-dump.md` | Raw Openplanet docs dump (search only when the above fail) |\n| `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. |\n| `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. |\n\nFile v2.2.1:_meta.json\n\n{\n  \"ownerId\": \"kn70db4t9jcx94v2xfcg0pymyd87f4nt\",\n  \"slug\": \"openplanet-plugin-dev\",\n  \"version\": \"2.2.1\",\n  \"publishedAt\": 1786966754316\n}\n\nFile v2.2.1:references/api-namespaces.md\n\n# Openplanet API Reference - Namespaces\n\nSource: https://openplanet.dev/docs/api\n\n## List of all namespaces (as of Openplanet 1.29.5)\n\n| Namespace | Description |\n|-----------|-------------|\n| `// Global namespace` | Global functions |\n| `namespace UI` | UI tools (ImGui) |\n| `namespace nvg` | NanoVG drawing |\n| `namespace Audio` | Audio playback |\n| `namespace Auth` | Third-party API authentication |\n| `namespace Crypto` | Cryptography and hashing |\n| `namespace Display` | Display functions |\n| `namespace Fids` | Game files and folders |\n| `namespace IO` | Filesystem I/O |\n| `namespace Icons` | Icon helpers |\n| `namespace Json` | JSON serialization |\n| `namespace Math` | Math functions |\n| `namespace Net` | Networking and sockets |\n| `namespace Path` | File path operations |\n| `namespace Permissions` | Trackmania permission checks |\n| `namespace Regex` | Regular expressions |\n| `namespace SQLite` | SQLite database access |\n| `namespace Settings` | Openplanet settings |\n| `namespace Tests` | Testing utilities |\n| `namespace Text` | Text parsing and formatting |\n| `namespace Time` | Date and time |\n| `namespace XML` | XML deserialization |\n| `namespace mat3` | 3x3 matrix |\n| `namespace mat4` | 4x4 matrix |\n| `namespace string` | String static functions |\n| `namespace Reflection` | Type reflection |\n| `namespace Discord` | Discord rich presence |\n| `namespace Meta` | Openplanet meta API |\n| `namespace Import` | DLL import |\n| `namespace Dev` | Advanced memory access |\n| `namespace Internal` | Internal/built-in use only |\n\n## UI Namespace - Key Functions (AngelScript)\n\nFrom global namespace:\n- `bool UI::Begin(string name, bool* open = nil, UI.WindowFlags flags = 0)` - Begin window\n- `void UI::End()` - End window\n- `bool UI::BeginChild(string id, vec2 size = {0,0}, bool border = false, UI.WindowFlags flags = 0)`\n- `void UI::EndChild()`\n- `void UI::Text(string text)`\n- `void UI::TextColored(vec4 color, string text)` - NOTE: Does NOT exist in Openplanet AngelScript! Use PushStyleColor instead\n- `void UI::TextDisabled(string text)`\n- `void UI::TextWrapped(string text)`\n- `void UI::LabelText(string label, string text)`\n- `bool UI::Button(string label, vec2 size = {0,0})`\n- `bool UI::SmallButton(string label)`\n- `bool UI::Checkbox(string label, bool* value)`\n- `bool UI::RadioButton(string label, bool active)`\n- `bool UI::InputText(string label, string* buffer, int bufferSize, UI.InputTextFlags flags = 0)` - Returns true if text changed\n- `bool UI::Combo(string label, int* currentItem, string[] items)`\n- `bool UI::SliderFloat(string label, float* value, float min, float max, string format = \"%.3f\")`\n- `bool UI::SliderInt(string label, int* value, int min, int max, string format = \"%d\")`\n- `bool UI::DragFloat(string label, float* value, float speed = 1.0, float min = 0, float max = 0)`\n- `bool UI::ColorEdit3(string label, float[3] color, UI.ColorEditFlags flags = 0)`\n- `bool UI::ColorEdit4(string label, float[4] color, UI.ColorEditFlags flags = 0)`\n- `bool UI::TreeNode(string label)`\n- `void UI::TreePop()`\n- `bool UI::Selectable(string label, bool selected = false)`\n- `bool UI::ListBox(string label, int* currentItem, string[] items)`\n- `void UI::ProgressBar(float fraction, vec2 size = {-1,0}, string overlay = \"\")`\n- `void UI::BulletText(string text)`\n- `void UI::Separator()`\n- `void UI::SameLine()`\n- `void UI::Spacing()`\n- `void UI::Dummy(vec2 size)`\n- `void UI::PushItemWidth(float width)`\n- `void UI::PopItemWidth()`\n- `void UI::SetNextWindowPos(int x, int y, UI.Cond condition = UI.Cond::FirstUseEver)`\n- `void UI::SetNextWindowSize(int w, int h, UI.Cond condition = UI.Cond::FirstUseEver)`\n- `void UI::PushStyleColor(UI.Col idx, vec4 color)`\n- `void UI::PopStyleColor()`\n- `void UI::PushFontSize(float size)`\n- `void UI::PopFontSize()`\n- `void UI::BeginTabBar(string id)`\n- `void UI::EndTabBar()`\n- `bool UI::BeginTabItem(string label)`\n- `void UI::EndTabItem()`\n- `void UI::BeginTable(string id, int columns, UI.TableFlags flags = 0)`\n- `void UI::EndTable()`\n- `void UI::TableNextRow()`\n- `void UI::TableSetColumnIndex(int col)`\n- `void UI::Image(string texturePath, vec2 size)`\n\n## UI::InputText - CRITICAL\n\nThe documentation shows:\n```angelscript\nbool UI::InputText(string label, string* buffer, int bufferSize, UI.InputTextFlags flags = 0)\n```\n\nBut in practice (Openplanet 1.29.5), the compiler shows these overloads:\n```angelscript\nstring UI::InputText(const string&in label, string str, int flags = UI::InputTextFlags::None, UI::InputTextCallback@ callback = null)\nstring UI::InputText(const string&in label, string str, bool&out changed, int flags = UI::InputTextFlags::None, UI::InputTextCallback@ callback = null)\n```\n\nBoth return `string`, NOT `bool`. The `bool&out changed` parameter is set to true when Enter is pressed.\n\n**Working pattern:**\n```angelscript\nbool changed = false;\nUI::InputText(\"##Input\", g_Text, changed, UI::InputTextFlags::EnterReturnsTrue);\nif (changed) {\n    // Enter was pressed, g_Text contains the input\n}\n```\n\n## UI::Col enum values\n\n- `UI::Col::Text`\n- `UI::Col::TextDisabled`\n- `UI::Col::WindowBg`\n- `UI::Col::ChildBg`\n- `UI::Col::PopupBg`\n- `UI::Col::Border`\n- `UI::Col::FrameBg`\n- `UI::Col::FrameBgHovered`\n- `UI::Col::FrameBgActive`\n- `UI::Col::TitleBg`\n- `UI::Col::TitleBgActive`\n- `UI::Col::MenuBarBg`\n- `UI::Col::ScrollbarBg`\n- `UI::Col::ScrollbarGrab`\n- `UI::Col::ScrollbarGrabHovered`\n- `UI::Col::ScrollbarGrabActive`\n- `UI::Col::CheckMark`\n- `UI::Col::SliderGrab`\n- `UI::Col::SliderGrabActive`\n- `UI::Col::Button`\n- `UI::Col::ButtonHovered`\n- `UI::Col::ButtonActive`\n- `UI::Col::Header`\n- `UI::Col::HeaderHovered`\n- `UI::Col::HeaderActive`\n- `UI::Col::Separator`\n- `UI::Col::SeparatorHovered`\n- `UI::Col::SeparatorActive`\n- `UI::Col::ResizeGrip`\n- `UI::Col::Tab`\n- `UI::Col::TabHovered`\n- `UI::Col::TabActive`\n- `UI::Col::PlotLines`\n- `UI::Col::PlotLinesHovered`\n- `UI::Col::PlotHistogram`\n- `UI::Col::PlotHistogramHovered`\n- `UI::Col::TextSelectedBg`\n- `UI::Col::ModalWindowDimBg`\n\n## UI::WindowFlags enum values\n\n- `UI::WindowFlags::None`\n- `UI::WindowFlags::NoTitleBar`\n- `UI::WindowFlags::NoResize`\n- `UI::WindowFlags::NoMove`\n- `UI::WindowFlags::NoScrollbar`\n- `UI::WindowFlags::NoScrollWithMouse`\n- `UI::WindowFlags::NoCollapse`\n- `UI::WindowFlags::AlwaysAutoResize`\n- `UI::WindowFlags::NoBackground`\n- `UI::WindowFlags::NoSavedSettings`\n- `UI::WindowFlags::NoMouseInputs`\n- `UI::WindowFlags::MenuBar`\n- `UI::WindowFlags::HorizontalScrollbar`\n- `UI::WindowFlags::NoFocusOnAppearing`\n- `UI::WindowFlags::NoBringToFrontOnFocus`\n- `UI::WindowFlags::AlwaysVerticalScrollbar`\n- `UI::WindowFlags::AlwaysHorizontalScrollbar`\n- `UI::WindowFlags::AlwaysUseWindowPadding`\n- `UI::WindowFlags::NoNavInputs`\n- `UI::WindowFlags::NoNavFocus`\n\n## UI::InputTextFlags enum values\n\n- `UI::InputTextFlags::None`\n- `UI::InputTextFlags::CharsDecimal`\n- `UI::InputTextFlags::CharsHexadecimal`\n- `UI::InputTextFlags::CharsUppercase`\n- `UI::InputTextFlags::CharsNoBlank`\n- `UI::InputTextFlags::AutoSelectAll`\n- `UI::InputTextFlags::EnterReturnsTrue`\n- `UI::InputTextFlags::CallbackCompletion`\n- `UI::InputTextFlags::CallbackHistory`\n- `UI::InputTextFlags::CallbackAlways`\n- `UI::InputTextFlags::CallbackCharFilter`\n- `UI::InputTextFlags::AllowTabInput`\n- `UI::InputTextFlags::CtrlEnterForNewLine`\n- `UI::InputTextFlags::NoHorizontalScroll`\n- `UI::InputTextFlags::AlwaysOverwrite`\n- `UI::InputTextFlags::ReadOnly`\n- `UI::InputTextFlags::Password`\n- `UI::InputTextFlags::NoUndoRedo`\n\n## UI::Cond enum values\n\n- `UI::Cond::None`\n- `UI::Cond::Always`\n- `UI::Cond::Once`\n- `UI::Cond::FirstUseEver`\n- `UI::Cond::Appearing`\n\n## UI::TableFlags enum values\n\n- `UI::TableFlags::None`\n- `UI::TableFlags::Resizable`\n- `UI::TableFlags::Reorderable`\n- `UI::TableFlags::Hideable`\n- `UI::TableFlags::Sortable`\n- `UI::TableFlags::NoSavedSettings`\n- `UI::TableFlags::ContextMenuInBody`\n- `UI::TableFlags::RowBg`\n- `UI::TableFlags::BordersInnerH`\n- `UI::TableFlags::BordersOuterH`\n- `UI::TableFlags::BordersInnerV`\n- `UI::TableFlags::BordersOuterV`\n- `UI::TableFlags::BordersH`\n- `UI::TableFlags::BordersV`\n- `UI::TableFlags::BordersInner`\n- `UI::TableFlags::BordersOuter`\n- `UI::TableFlags::Borders`\n- `UI::TableFlags::NoBordersInBody`\n- `UI::TableFlags::NoBordersInBodyUntilResize`\n- `UI::TableFlags::SizingFixedFit`\n- `UI::TableFlags::SizingFixedSame`\n- `UI::TableFlags::SizingStretchProp`\n- `UI::TableFlags::SizingStretchSame`\n- `UI::TableFlags::NoHostExtendX`\n- `UI::TableFlags::NoHostExtendY`\n- `UI::TableFlags::NoKeepColumnsVisible`\n- `UI::TableFlags::PreciseWidths`\n- `UI::TableFlags::NoClip`\n- `UI::TableFlags::PadOuterX`\n- `UI::TableFlags::NoPadOuterX`\n- `UI::TableFlags::NoPadInnerX`\n- `UI::TableFlags::ScrollX`\n- `UI::TableFlags::ScrollY`\n- `UI::TableFlags::SortMulti`\n- `UI::TableFlags::SortTristate`\n\n## Time Namespace\n\nProperties:\n- `int64 Time::Stamp` - Current Unix timestamp in seconds\n- `uint64 Time::Now` - Milliseconds since game start\n- `uint64 Time::FrameCount` - Frame counter\n\nFunctions:\n- `string Time::FormatString(const string&in format, int64 stamp = -1)` - strftime format, local time\n- `string Time::FormatStringUTC(const string&in format, int64 stamp = -1)` - strftime format, UTC\n- `int64 Time::ParseFormatString(const string&in format, const string&in stamp)` - Parse time from string\n- `string Time::Format(uint64 time, bool fractions = true, bool forceMinutes = true, bool forceHours = false, bool short = false)` - Race time format (e.g. \"1:01.234\")\n- `uint64 Time::ParseRelativeTime(const string&in time)` - Parse race time to ms\n- `Time::Info Time::Parse(int64 stamp = -1)` - Parse to Time::Info (local)\n- `Time::Info Time::ParseUTC(int64 stamp = -1)` - Parse to Time::Info (UTC)\n\nTime::Info members (PascalCase!):\n- `int Year`\n- `int Month`\n- `int Day`\n- `int Hour`\n- `int Minute`\n- `int Second`\n- `int DayOfWeek` (0=Sunday)\n\n## Text Namespace\n\nFunctions:\n- `int Text::ParseInt(const string&in str, int base = 10)`\n- `int64 Text::ParseInt64(const string&in str, int base = 10)`\n- `uint Text::ParseUInt(const string&in str, int base = 10)`\n- `float Text::ParseFloat(const string&in str)`\n- `double Text::ParseDouble(const string&in str)`\n- `vec4 Text::ParseHexColor(string str)` - e.g. \"#FF0000\"\n- `string Text::Format(const string&in format, <type> value)` - Single value only!\n- `string Text::StripFormatCodes(const string&in s)` - Strip Mania formatting codes ($f00 etc)\n- `string Text::FormatGameColor(const vec3&in rgb)` - Format as $f00\n- `string Text::FormatOpenplanetColor(const vec3&in rgb)` - Format as \\$f00\n- `string Text::EncodeHex(const string&in, bool upper = false)`\n- `string Text::DecodeHex(const string&in)`\n- `string Text::EncodeBase64(const string&in, bool url = false)`\n- `string Text::DecodeBase64(const string&in, bool url = false)`\n\n## IO Namespace\n\nFunctions:\n- `string IO::FromStorageFolder(const string&in filename)` - Plugin storage path\n- `string IO::FromDataFolder(const string&in filename)` - Openplanet data folder\n- `string IO::FromAppFolder(const string&in filename)` - Game install folder\n- `string IO::FromUserGameFolder(const string&in filename)` - User game folder\n- `bool IO::FileExists(const string&in filename)`\n- `uint64 IO::FileSize(const string&in filename)`\n- `int64 IO::FileCreatedTime(const string&in filename)`\n- `int64 IO::FileModifiedTime(const string&in filename)`\n- `void IO::Delete(const string&in filename)`\n- `void IO::Copy(const string&in path, const string&in target)`\n- `void IO::Move(const string&in path, const string&in target)`\n- `bool IO::FolderExists(const string&in path)`\n- `void IO::CreateFolder(const string&in path, bool recursive = true)`\n- `void IO::DeleteFolder(const string&in path, bool recursive = false)`\n- `string[]@ IO::IndexFolder(const string&in path, bool recursive)`\n\n## Meta Namespace\n\nFunctions:\n- `Meta::Plugin@ Meta::ExecutingPlugin()` - Current plugin handle\n- `Meta::Plugin@[]@ Meta::AllPlugins()` - All loaded plugins\n- `Meta::Plugin@ Meta::GetPluginFromID(const string&in id)` - Get plugin by ID\n- `void Meta::UnloadPlugin(Plugin@ plugin)` - Queue plugin unload\n- `void Meta::ReloadPlugin(Plugin@ plugin)` - Queue plugin reload\n- `void Meta::OpenSettings(Plugin@ plugin = null)` - Open settings window\n- `void Meta::SaveSettings()` - Force save settings\n- `bool Meta::IsDeveloperMode()` - Check dev mode\n- `string Meta::OpenplanetVersion()` - Get Openplanet version\n- `string Meta::OpenplanetBuildInfo()` - Get build info\n- `void Meta::Terminate()` - Exit game\n\n## Net Namespace\n\nFunctions:\n- `Net::HttpRequest@ Net::HttpGet(const string&in url)`\n- `Net::HttpRequest@ Net::HttpPost(const string&in url, const string&in data = \"\", const string&in contentType = \"application/x-www-form-urlencoded\")`\n- `Net::HttpRequest@ Net::HttpHead(const string&in url)`\n- `Net::HttpRequest@ Net::HttpPut(const string&in url, const string&in data = \"\", const string&in contentType = \"application/x-www-form-urlencoded\")`\n- `Net::HttpRequest@ Net::HttpDelete(const string&in url)`\n- `Net::HttpRequest@ Net::HttpPatch(const string&in url, const string&in data = \"\", const string&in contentType = \"application/x-www-form-urlencoded\")`\n- `string Net::UrlEncode(const string&in str)`\n- `string Net::UrlDecode(const string&in str)`\n\n## Json Namespace\n\nClasses:\n- `class Json::Value` - JSON value node\n\nFunctions:\n- `Json::Value@ Json::Object()` - Create JSON object\n- `Json::Value@ Json::Array()` - Create JSON array\n- `Json::Value@ Json::Parse(const string&in json)` - Parse JSON string\n- `string Json::Write(const Json::Value@ value, bool pretty = false)` - Serialize to string\n- `Json::Value@ Json::FromFile(const string&in filename)` - Load from file\n- `void Json::ToFile(const string&in filename, const Json::Value@ value, bool pretty = false)` - Save to file\n\n## nvg (NanoVG) Namespace\n\nKey functions:\n- `nvg::Texture@ nvg::LoadTexture(const string&in filename, int flags = 0)`\n- `int nvg::LoadFont(const string&in filename, bool fallbackIcons = false, bool fallbackArial = false)`\n- `void nvg::Save()` / `void nvg::Restore()` / `void nvg::Reset()`\n- `void nvg::BeginPath()` / `void nvg::EndPath()` (implied)\n- `void nvg::Fill()` / `void nvg::Stroke()`\n- `void nvg::FillColor(const vec4&in color)` / `void nvg::StrokeColor(const vec4&in color)`\n- `void nvg::FillPaint(const nvg::Paint&in paint)` / `void nvg::StrokePaint(const nvg::Paint&in paint)`\n- `void nvg::StrokeWidth(float size)`\n- `void nvg::Rect(float x, float y, float w, float h)`\n- `void nvg::RoundedRect(float x, float y, float w, float h, float r)`\n- `void nvg::Circle(const vec2&in center, float r)`\n- `void nvg::Ellipse(const vec2&in center, float rx, float ry)`\n- `void nvg::Arc(const vec2&in center, float r, float a0, float a1, nvg::Winding dir)`\n- `void nvg::MoveTo(const vec2&in pos)` / `void nvg::LineTo(const vec2&in pos)`\n- `void nvg::BezierTo(const vec2&in c1, const vec2&in c2, const vec2&in pos)`\n- `float nvg::Text(float x, float y, const string&in str)`\n- `void nvg::TextBox(float x, float y, float w, const string&in str)`\n- `vec2 nvg::TextBounds(const string&in str)`\n- `void nvg::FontSize(float size)`\n- `void nvg::FontFace(int font)`\n- `void nvg::TextAlign(int align)`\n- `void nvg::Scissor(float x, float y, float w, float h)`\n- `void nvg::ResetScissor()`\n- `nvg::Paint nvg::LinearGradient(const vec2&in start, const vec2&in end, const vec4&in color1, const vec4&in color2)`\n- `nvg::Paint nvg::BoxGradient(float x, float y, float w, float h, float r, float f, const vec4&in color1, const vec4&in color2)`\n- `nvg::Paint nvg::RadialGradient(const vec2&in center, float inr, float outr, const vec4&in color1, const vec4&in color2)`\n- `nvg::Paint nvg::TexturePattern(const vec2&in origin, const vec2&in size, float angle, nvg::Texture@ texture, float alpha)`\n- `void nvg::Translate(float x, float y)`\n- `void nvg::Rotate(float angle)`\n- `void nvg::Scale(float x, float y)`\n- `void nvg::ResetTransform()`\n- `void nvg::SetTransform(const mat3&in t)`\n- `mat3 nvg::CurrentTransform()`\n\n## Math Namespace\n\nConstants:\n- `float Math::PI`\n- `float Math::PI2`\n- `double Math::PIl`\n- `double Math::PI2l`\n\nKey functions:\n- `int Math::Abs(int i)` / `float Math::Abs(float f)`\n- `float Math::Sin(float f)` / `float Math::Cos(float f)` / `float Math::Tan(float f)`\n- `float Math::Asin(float f)` / `float Math::Acos(float f)` / `float Math::Atan(float f)`\n- `float Math::Atan2(float y, float x)`\n- `float Math::Sqrt(float f)`\n- `float Math::Pow(float x, float y)`\n- `float Math::Exp(float f)`\n- `float Math::Log(float f)` / `float Math::Log2(float f)` / `float Math::Log10(float f)`\n- `float Math::Floor(float f)` / `float Math::Ceil(float f)` / `float Math::Round(float f)`\n- `float Math::ToDeg(float rad)` / `float Math::ToRad(float deg)`\n- `float Math::Rand(float min, float max)` / `int Math::Rand(int min, int max)`\n- `int Math::Min(int x, int y)` / `float Math::Min(float x, float y)`\n- `int Math::Max(int x, int y)` / `float Math::Max(float x, float y)`\n- `int Math::Clamp(int x, int min, int max)` / `float Math::Clamp(float x, float min, float max)`\n- `float Math::Lerp(const float&in min, const float&in max, float x)`\n- `vec2 Math::Lerp(const vec2&in min, const vec2&in max, float x)`\n- `vec3 Math::Lerp(const vec3&in min, const vec3&in max, float x)`\n- `vec4 Math::Lerp(const vec4&in min, const vec4&in max, float x)`\n- `float Math::Distance(const vec2&in a, const vec2&in b)` / `float Math::Distance(const vec3&in a, const vec3&in b)`\n- `float Math::Distance2(const vec2&in a, const vec2&in b)` / `float Math::Distance2(const vec3&in a, const vec3&in b)`\n- `float Math::Dot(const vec2&in a, const vec2&in b)` / `float Math::Dot(const vec3&in a, const vec3&in b)`\n- `float Math::Angle(const vec2&in a, const vec2&in b)` / `float Math::Angle(const vec3&in a, const vec3&in b)`\n- `vec3 Math::Cross(const vec3&in a, const vec3&in b)`\n- `bool Math::IsNaN(float)` / `bool Math::IsInf(float)`\n- `quat Math::Slerp(const quat&in a, const quat&in b, float x)`\n- `float Math::InvLerp(const float&in min, const float&in max, const float&in value)`\n\n## string (static) Namespace\n\nFunctions:\n- `string string::Join(const string[]&in arr, const string&in delimiter)`\n- `string string::Repeat(const string&in str, int count)`\n\n## Regex Namespace\n\nFunctions:\n- `bool Regex::IsMatch(const string&in source, const string&in pattern, int flags = Regex::Flags::ECMAScript)`\n- `bool Regex::Contains(const string&in source, const string&in pattern, int flags = Regex::Flags::ECMAScript)`\n- `string[]@ Regex::Match(const string&in source, const string&in pattern, int flags = Regex::Flags::ECMAScript)`\n- `string[]@ Regex::Search(const string&in source, const string&in pattern, int flags = Regex::Flags::ECMAScript)`\n- `Regex::SearchAllResult@ Regex::SearchAll(const string&in source, const string&in pattern, int flags = Regex::Flags::ECMAScript)`\n- `string Regex::Replace(const string&in source, const string&in pattern, const string&in replace, int flags = Regex::Flags::ECMAScript)`\n\n## Path Namespace\n\nFunctions:\n- `string Path::GetExtension(const string&in path)`\n- `bool Path::HasExtension(const string&in path)`\n- `string Path::ChangeExtension(const string&in path, const string&in extension)`\n- `string Path::RemoveExtension(const string&in path)`\n- `string Path::Join(const string&in a, const string&in b)`\n- `bool Path::Equals(const string&in a, const string&in b, bool caseSensitive = false)`\n- `string Path::GetDirectoryName(const string&in path)`\n- `string Path::GetFileName(const string&in path)`\n- `string Path::GetFileNameWithoutExtension(const string&in path)`\n- `string Path::SanitizeFileName(const string&in name)`\n\n## Fids Namespace\n\nFunctions:\n- `CSystemFidFile@ Fids::GetResource(const string&in path)`\n- `CSystemFidsFolder@ Fids::GetResourceFolder(const string&in path)`\n- `CSystemFidFile@ Fids::GetProgramData(const string&in path)`\n- `CSystemFidsFolder@ Fids::GetProgramDataFolder(const string&in path)`\n- `CSystemFidFile@ Fids::GetUser(const string&in path)`\n- `CSystemFidsFolder@ Fids::GetUserFolder(const string&in path)`\n- `CSystemFidFile@ Fids::GetGame(const string&in path)`\n- `CSystemFidsFolder@ Fids::GetGameFolder(const string&in path)`\n- `CSystemFidFile@ Fids::GetFake(const string&in path)`\n- `CSystemFidsFolder@ Fids::GetFakeFolder(const string&in path)`\n- `CMwNod@ Fids::Preload(CSystemFidFile@ fid)`\n- `bool Fids::Extract(CSystemFidFile@ fid, bool hookMethod = false)`\n- `string Fids::GetFullPath(CSystemFidFile@ fid)`\n- `void Fids::UpdateTree(CSystemFidsFolder@ fids, bool withFiles = true)`\n- `CSystemFidFile@ Fids::GetFidsFile(CSystemFidsFolder@ fids, const string&in path)`\n- `CSystemFidsFolder@ Fids::GetFidsFolder(CSystemFidsFolder@ fids, const string&in path)`\n\n## Display Namespace\n\nFunctions:\n- `int Display::GetWidth()` - Game resolution width\n- `int Display::GetHeight()` - Game resolution height\n\n## Discord Namespace\n\nFunctions:\n- `void Discord::Initialize(const string&in applicationId)`\n- `void Discord::Shutdown()`\n- `bool Discord::IsReady()`\n- `Discord::User@ Discord::GetUser()`\n- `void Discord::SetStatus(const Discord::Status&in status)`\n- `void Discord::Respond(const string&in userId, Discord::Response reply)`\n- `string Discord::GetQueuedJoin()`\n- `string Discord::GetQueuedSpectate()`\n- `int Discord::GetNumJoinRequests()`\n- `Discord::User@ Discord::GetQueuedJoinRequest()`\n\n## Dev Namespace\n\nFunctions:\n- `void Dev::Sleep(uint ms)`\n- `uint64 Dev::BaseAddress()`\n- `uint64 Dev::BaseAddressEnd()`\n- `uint64 Dev::FindPattern(const string&in pattern)`\n- `string Dev::Patch(uint64 ptr, const string&in pattern)`\n- `Dev::HookInfo@ Dev::Hook(uint64 ptr, int padding, const string&in func, int pushRegisters = 0)`\n- `void Dev::Unhook(Dev::HookInfo@ hook)`\n- `void Dev::DebugBreak()`\n- `uint64 Dev::Allocate(uint size, bool executable = false)`\n- `void Dev::Free(uint64 ptr)`\n- Various Read/Write functions for memory access\n\n## Auth Namespace\n\nClasses:\n- `class Auth::PluginAuthTask`\n\nFunctions:\n- `Auth::PluginAuthTask@ Auth::GetToken()`\n\n## Crypto Namespace\n\nFunctions:\n- `string Crypto::MD5(const string&in str)`\n- `string Crypto::Sha1(const string&in str)`\n- `string Crypto::Sha256(const string&in str)`\n- `MemoryBuffer@ Crypto::Random(int length)`\n- `string Crypto::RandomBase64(int length, bool url = false)`\n\n## Audio Namespace\n\nClasses:\n- `class Audio::Sample`\n- `class Audio::Voice`\n\nFunctions:\n- `Audio::Sample@ Audio::LoadSample(const string&in filename, bool streamed = false)`\n- `Audio::Sample@ Audio::LoadSample(MemoryBuffer&in buffer, bool streamed = false)`\n- `Audio::Sample@ Audio::LoadSampleFromAbsolutePath(const string&in filename, bool streamed = false)`\n- `Audio::Voice@ Audio::Play(Sample@ sample, float gain = 1.0f)`\n- `Audio::Voice@ Audio::Start(Sample@ sample)`\n\n## SQLite Namespace\n\nClasses:\n- `class SQLite::Database`\n- `class SQLite::Statement`\n\n## Settings Namespace\n\nClasses:\n- `class Settings::Section`\n\n## Tests Namespace\n\nClasses:\n- `class Tests::Context`\n\n## XML Namespace\n\nClasses:\n- `class XML::Node`\n- `class XML::Document`\n\n## Reflection Namespace\n\nClasses:\n- `class Reflection::MwMemberInfo`\n- `class Reflection::MwClassInfo`\n\nFunctions:\n- `const Reflection::MwClassInfo@ Reflection::GetType(const string&in name)`\n- `const Reflection::MwClassInfo@ Reflection::GetType(uint id)`\n- `const Reflection::MwClassInfo@ Reflection::TypeOf(CMwNod@ nod)`\n- `int Reflection::GetRefCount(CMwNod@ nod)`\n\n## Import Namespace\n\nClasses:\n- `class Import::Library`\n- `class Import::Function`\n- `class Import::Ref`\n\nFunctions:\n- `Import::Library@ Import::GetLibrary(const string&in path)`\n\n## mat3 Namespace\n\nFunctions:\n- `mat3 mat3::Identity()`\n- `mat3 mat3::Translate(const vec2&in v)`\n- `mat3 mat3::Rotate(float angle)`\n- `mat3 mat3::Scale(const vec2&in scale)`\n- `mat3 mat3::Scale(float scale)`\n- `mat3 mat3::Inverse(const mat3&in)`\n- `mat3 mat3::Transpose(const mat3&in)`\n\n## mat4 Namespace\n\nFunctions:\n- `mat4 mat4::Identity()`\n- `mat4 mat4::Translate(const vec3&in v)`\n- `mat4 mat4::Rotate(float angle, const vec3&in dir)`\n- `mat4 mat4::Scale(const vec3&in scale)`\n- `mat4 mat4::Scale(float scale)`\n- `mat4 mat4::Perspective(float yFov, float aspect, float nearZ, float farZ)`\n- `mat4 mat4::Inverse(const mat4&in)`\n- `mat4 mat4::LookAt(const vec3&in eye, const vec3&in center, const vec3&in up)`\n- `mat4 mat4::Transpose(const mat4&in)`\n\n### Reference: OpenPlanet-Basic-API\n\nFile v2.2.1:references/api-quirks-and-pitfalls.md\n\n## 🚨 CRITICAL — API Quirks & Pitfalls\n\n### 🔤 AngelScript Language Quirks\n\nThese bite everyone. AngelScript ≠ C++, ≠ C#, ≠ Java.\n\n#### 🔢 Integer literal suffixes\n\n`u`, `l`, `ul`, `ull` suffixes are **not supported**.\n\n```angelscript\n// ❌ ERR: Unexpected token '<identifier>'\nconst uint64 MASK = 0x3FFFFFFu;\nconst int BIG = 1ul;\n\n// ✅ OK: bare literal; compiler picks the type\nconst uint64 MASK = 0x3FFFFFF;\nconst int BIG = 1;\nconst uint64 KEY = uint64(0x123);     // explicit cast\n```\n\n#### 📐 `const` on value-type arrays\n\n`const` works for primitives, but **NOT for fixed-size arrays of value types** like `int2[]` or `vec3[]`.\n\n```angelscript\n// ❌ ERR: Expected '('\nconst int2 EDGES[12] = { int2(0,1), ... };\nconst vec3 CORNERS[8] = { ... };\n\n// ✅ OK: dynamic, no const\nint2[] g_Edges = { int2(0,1), ... };\n```\n\nAlso: `const` on `array<T>` is unreliable — keep globals un-`const` and use a `g_` prefix instead.\n\n#### 📏 Fixed-size local arrays\n\n**Local fixed-size arrays are not supported**:\n\n```angelscript\nvoid Foo() {\n    // ❌ ERR: Expected ';' Instead found identifier 'pts'\n    vec2 pts[8];\n    float depth[8];\n}\n\n// ✅ WORKAROUND: individual variables\nvoid Foo() {\n    vec2 p0, p1, p2, p3, p4, p5, p6, p7;\n    // compiler will register-allocate them\n}\n```\n\nOr use dynamic arrays:\n\n```angelscript\nvec2[] GetCorners() {\n    vec2[] cs = { vec2(0,0), vec2(1,0), ... };\n    return cs;\n}\n```\n\n#### 🗝️ `dictionary` key types\n\n**Most surprising limitation**: `dictionary` only accepts `const string&in` keys.\n\n```angelscript\ndictionary d;\n\n// ❌ ERR: No matching signatures to 'dictionary::Exists(uint64)'\nd[uint64(123)] = true;\nd[int(456)]    = true;\n\n// ✅ OK: build a string key\nd[\"123\"] = true;\nd[gx + \",\" + gy + \",\" + gz] = true;\n```\n\nIf you need a fast non-string key, roll your own hash table (parallel arrays + linear scan, or small open-addressed probe table). For most plugins, `string` keys are fine.\n\n#### 🔀 `uint` vs `int` in comparisons\n\n`array<T>.Length` returns `uint`. Comparing a signed `int` loop counter produces a warning treated as error in strict mode:\n\n```angelscript\n// ⚠️ WARN: Signed/Unsigned mismatch\nfor (int e = 0; e < arr.Length; e++) { ... }\n\n// ✅ CLEAN:\nfor (uint e = 0; e < arr.Length; e++) { ... }\n```\n\n#### 📤 `out` parameter naming\n\nMatch the parameter name **exactly**:\n\n```angelscript\n// ❌ ERR: No matching symbol 'outDepth'\nbool Project(vec3 &in p, vec2 &out screen, float &out depth) {\n    outDepth = 0.0f;\n}\n\n// ✅ OK:\nbool Project(vec3 &in p, vec2 &out screen, float &out depth) {\n    depth = 0.0f;\n}\n```\n\n#### 🎨 `int2`, `vec2`, `vec3`, `vec4` constructors\n\n```angelscript\nint2 a = int2(1, 2);\nvec3 v = vec3(1.0f, 2.0f, 3.0f);\nvec4 red = vec4(1.0f, 0.0f, 0.0f, 0.5f);\n```\n\nThey are value types — no `@` needed for storage.\n\n#### 🔄 `&inout` on primitive types is NOT allowed\n\n```angelscript\n// ❌ ERR:\nvoid ConfigRow(const string &in label, bool &inout value) { }\n\n// ✅ OK:\nvoid ConfigRow(const string &in label, bool value) { }\n```\n\n#### 📊 Array initialization — inline `int t[] = {...}` fails inside functions\n\n```angelscript\n// ❌ ERR inside functions:\n// int t[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};\n\n// ✅ OK: use array<T> with InsertLast\narray<int64> items;\nitems.InsertLast(123);\n\n// ✅ OK: pre-allocate at global scope\nint[] g_Array;\nvoid Main() { g_Array.Resize(16); }\n\n// ✅ OK: inline array init works at global scope\nint[] monthDays = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};\n```\n\n#### 🔍 `string::IndexOf` — takes exactly ONE parameter\n\n```angelscript\n// ❌ ERR:\nint idx = text.IndexOf(\"[\", startPos);\n\n// ✅ OK:\nint idx = text.IndexOf(\"[\");\n// For offset: use SubStr first\nint idx = text.SubStr(startPos).IndexOf(\"[\");\n```\n\n#### 📝 `Text::Format` takes exactly ONE value argument\n\n```angelscript\n// ❌ ERR:\nText::Format(\"%.6f (%.2f%%)\", sidereal, sidereal * 100.0);\n\n// ✅ OK:\nText::Format(\"%.6f\", sidereal) + \" (\" + Text::Format(\"%.2f%%\", sidereal * 100.0) + \")\";\n```\n\n---\n\n### ⏰ Time API Quirks\n\n#### 🅰️ Time::Info uses PascalCase, NOT lowercase\n\n`'year' is not a member of 'Time::Info'`\n\n```angelscript\n// ❌ ERR:\ninfo.year, info.month, info.day, info.hour, info.minute, info.second\n\n// ✅ OK:\ninfo.Year, info.Month, info.Day, info.Hour, info.Minute, info.Second\n```\n\n#### 📅 Weekday is NOT a member of Time::Info\n\n`info.Weekday` will fail. Use **Zeller's formula** (0=Sun..6=Sat):\n\n```angelscript\nint GetDayOfWeek(int y, int m, int d) {\n    if (m < 3) { m += 12; y -= 1; }\n    int K = y % 100;\n    int J = y / 100;\n    int h = (d + (13 * (m + 1)) / 5 + K + K / 4 + J / 4 + 5 * J) % 7;\n    return (h + 6) % 7; // 0=Sun\n}\n```\n\n#### 🕐 Time functions\n\n```angelscript\nint64 now = Time::Stamp;                          // Epoch seconds\nuint64 gameTime = Time::Now;                      // ms since game start\nstring formatted = Time::FormatString(\"%H:%M\", now);  // strftime format\nTime::Info info = Time::Parse(now);               // Local time\n```\n\n---\n\n### 🖥️ UI API Quirks\n\n#### 🔤 No UI::Font enum — use PushFontSize\n\n```angelscript\n// ✅ OK:\nUI::PushFontSize(22.0);\nUI::Text(\"Big text\");\nUI::PopFontSize();\n\n// ❌ ERR (does not exist):\nUI::PushFont(UI::Font::OpenSansBold);\n```\n\n#### 🎨 No UI::TextColored — use PushStyleColor\n\n```angelscript\n// ✅ OK:\nUI::PushStyleColor(UI::Col::Text, vec4(0.3f, 1.0f, 0.5f, 1.0f));\nUI::Text(\"Green text\");\nUI::PopStyleColor();\n\n// ❌ ERR:\nUI::TextColored(color, \"text\");\n```\n\n#### 📍 Window position uses int coords\n\n```angelscript\n// ✅ OK (cast floats to int):\nUI::SetNextWindowPos(int(posX), int(posY), UI::Cond::Appearing);\n```\n\n#### 🪟 UI::Begin takes a bool reference\n\n```angelscript\nbool S_WindowOpen = false;\nif (!UI::Begin(\"My Window\", S_WindowOpen, UI::WindowFlags::NoSavedSettings)) {\n    UI::End();\n    return;\n}\n```\n\n#### ⌨️ UI::InputText — return type vs. bool&out\n\n```angelscript\n// ❌ ERR — InputText ALWAYS returns string, can't use in if():\nif (UI::InputText(\"##Input\", g_Text, changed, flags)) { }\n\n// ✅ OK:\nbool changed = false;\nUI::InputText(\"##Input\", g_Text, changed, flags);\nif (changed) { /* Enter was pressed */ }\n```\n\n---\n\n### 🖌️ NanoVG Essentials\n\nThe drawing API is per-frame immediate-mode. State persists until changed.\n\n```angelscript\nnvg::BeginPath();\nnvg::MoveTo(p1);\nnvg::LineTo(p2);\nnvg::Stroke();          // ← actually draws the path\n\nnvg::FontSize(13.0f);\nnvg::FillColor(vec4(1.0f, 1.0f, 1.0f, 0.9f));\nnvg::TextAlign(nvg::Align::Middle | nvg::Align::Center);\nnvg::Text(p, \"label\");\n```\n\n- `nvg::BeginPath()` is **required** for `LineTo`/`Rect`/`Circle`/etc.\n- `nvg::Text()` does **NOT** need `BeginPath`.\n- For minimum state changes, batch draws that share a state.\n\n---\n\n### 📷 Camera API\n\n#### 🎯 Camera::ToScreenSpace returns `vec2`, not `vec3`\n\n```angelscript\nvec2 screenPos = Camera::ToScreenSpace(worldPos);   // 2D only\n// NO z/depth component!\n```\n\nFor behind-camera test:\n\n```angelscript\nif (Camera::IsBehind(worldPos)) {\n    // skip this point\n}\n```\n\n⚠️ Don't assume a `vec3` overload exists. The `vec2` return is the only signature.\n\n#### 🚗 VehicleState::ViewingPlayerState() returns CSmPlayer\n\n```angelscript\nauto state = VehicleState::ViewingPlayerState();\nif (state is null) return;       // null when not in a vehicle / not in a map\nvec3 pos = state.Position;        // world-space player position\n```\n\nUse `is null` — AngelScript's null-comparison operator for handles.\n\n---\n\n### 🧱 Trackmania Block Dimensions\n\nStandard blocks are **32 × 32 × 8** (X × Z × Y) in world units.\n\n```angelscript\nconst float BLOCK_XZ = 32.0f;\nconst float BLOCK_Y  = 8.0f;\n\nint gx = int(Math::Floor(worldPos.x / BLOCK_XZ));\nint gy = int(Math::Floor(worldPos.y / BLOCK_Y));\nint gz = int(Math::Floor(worldPos.z / BLOCK_XZ));\n```\n\n---\n\nFile v2.2.1:references/blockable-input-callbacks-mp4.md\n\n# BlockableInputCallbacks (BIC) on ManiaPlanet 4 — verified facts (2026-08)\n\nThe `ackepta/BlockableInputCallbacks` optimizer (already shipped as\n`Plugins/BlockableInputCallbacks.op`, siteid 893, v0.1.0) is the input layer used\nby trainers (e.g. `TrackmaniaTrainer`). Facts verified by unpacking the `.op`\n(zip) before using it:\n\n## manifest shape (uses `[script]` module/exports, NOT `dependencies`)\n```toml\n[meta]\nname = \"BlockableInputCallbacks\"\nauthor = \"achepta\"\nversion = \"0.1.0\"\nsiteid = 893\n\n[script]\nmodule = \"BlockableInputCallbacks\"\nexports = [ \"Export.as\" ]\nshared_exports = [ \"Shared.as\" ]\n```\nDependency plugin window: a plugin that imports it writes\n`[script] dependencies = [\"BlockableInputCallbacks\"]` in ITS info.toml.\n\n## import surface (Export.as)\n```angelscript\nnamespace BlockableInputCallbacks {\n    import int RegisterCallback(const string &in name, IInputCallback@ callback) from \"BlockableInputCallbacks\";\n    import int RegisterCallback(const string &in name, IInputCallback@ callback, float activationThreshold) from \"BlockableInputCallbacks\";\n    import void UnregisterCallback(int id) from \"BlockableInputCallbacks\";\n    import void UnregisterCallbacks(const string &in name) from \"BlockableInputCallbacks\";\n    import void SetInputBlocked(PadInput input, bool blocked) from \"BlockableInputCallbacks\";\n    import void SetInputBlocked(KeyboardInput input, bool blocked) from \"BlockableInputCallbacks\";\n    import KeyboardInput VirtualKeyToKeyboardInput(VirtualKey key) from \"BlockableInputCallbacks\";\n    import VirtualKey KeyboardInputToVirtualKey(KeyboardInput input) from \"BlockableInputCallbacks\";\n}\n```\n\n## verified enum values (Shared.as)\n- `PadInput`: `None = 255`, then per-pad buttons.\n- `KeyboardInput`: `None = 0x00`; function keys **F5 = 0x3F, F6 = 0x40, F8 = 0x42**\n  (F7 = 0x41 by sequence).\n- `InputDecision`, `PadInputEvent` shared enums — usable in the importing plugin.\n\n## pitfalls\n- Do NOT assume TM2020's REML can't bind F5/F6/F8 the same way: verify the enum\n  against `Shared.as` in THIS plugin's `.op` before relying on it (values above\n  are for v0.1.0).\n- The callbacks are into-game with a `IInputCallback@` interface — if the plugin\n  is missing from `Plugins/` at runtime the import fails the whole plugin pencil.\n- Simpler zero-dependency fallback for a trainer: plain `OnKeyPress(bool down,\n  VirtualKey key)` with `[Setting hidden] VirtualKey` — no BIC needed unless the\n  key must also be BLOCKED from reaching the game.\n\nFile v2.2.1:references/cross-game-tmnt-next-mp4.md\n\n# Cross-game plugin compatibility: TMNEXT + MP4\n\nVerified facts for writing ONE Openplanet AngelScript plugin that compiles and\nruns on BOTH Trackmania (2020) [TMNEXT] and Maniaplanet 4 [MP4]. (From the\n`tm-green-timer` MP4 port, Aug 2026 — confirmed against `Openplanet4.json` and\n`OpenplanetNext.json`, and a clean in-game load with 0 `ERR :`.)\n\n## Preprocessor defines (Openplanet injects these per-game automatically)\n- `TMNEXT` — current game is Trackmania (2020)\n- `MP4`    — current game is Maniaplanet 4 (4.0 and 4.1)\n- Also available: `MP40`, `MP41`, `UNITED`, `MP3`, `TURBO` (see Openplanet docs).\n- No `info.toml` game flag is needed; the compiler selects the right branch.\n\n```angelscript\n#if TMNEXT\n    // TMNEXT-only API here\n#elif MP4\n    // MP4-only API here\n#else\n    // fallback (treat like MP4)\n#endif\n```\n\n## Common base class\n`CGameCtnApp` is the base class for both games (Openplanet docs: \"Ope\n\nArchive v2.2.0: 3 files, 11172 bytes\n\nFiles: skill-card.md (1917b), SKILL.md (25149b), _meta.json (140b)\n\nArchive v2.1.0: 3 files, 9519 bytes\n\nFiles: skill-card.md (1896b), SKILL.md (20094b), _meta.json (140b)\n\nArchive v2.0.0: 3 files, 9036 bytes\n\nFiles: skill-card.md (1869b), SKILL.md (19040b), _meta.json (140b)\n\nArchive v1.5.0: 3 files, 6294 bytes\n\nFiles: skill-card.md (2205b), SKILL.md (11234b), _meta.json (140b)\n\nArchive v1.0.4: 3 files, 10624 bytes\n\nFiles: skill-card.md (2335b), SKILL.md (23617b), _meta.json (140b)\n\nArchive v1.0.3: 3 files, 10508 bytes\n\nFiles: skill-card.md (2032b), SKILL.md (23633b), _meta.json (140b)\n\nArchive v1.0.2: 3 files, 9392 bytes\n\nFiles: skill-card.md (1623b), SKILL.md (21199b), _meta.json (140b)\n\nArchive v1.0.1: 3 files, 9237 bytes\n\nFiles: skill-card.md (1856b), SKILL.md (20481b), _meta.json (140b)","readmeExcerpt":"Skill: openplanet-plugin-dev Owner: tomekdot Summary: Create, debug, structure, and run Openplanet AngelScript plugins for Trackmania 2020 (TMNEXT) and ManiaPlanet 4 (TM2/MP4). Covers API quirks, AngelScript pitfalls, performance patterns, the launch/verify loop, MP4 API-mismatch fixes, and proven templates. Tags: latest:3.0.0 Version history: v3.0.0 | 2026-08-17T11:58:33.878Z | user Removed all bundled API reference","codeSnippets":[],"executableExamples":[{"language":"angelscript","snippet":"bool UI::InputText(string label, string* buffer, int bufferSize, UI.InputTextFlags flags = 0)"},{"language":"angelscript","snippet":"string UI::InputText(const string&in label, string str, int flags = UI::InputTextFlags::None, UI::InputTextCallback@ callback = null)\nstring UI::InputText(const string&in label, string str, bool&out changed, int flags = UI::InputTextFlags::None, UI::InputTextCallback@ callback = null)"},{"language":"angelscript","snippet":"bool changed = false;\nUI::InputText(\"##Input\", g_Text, changed, UI::InputTextFlags::EnterReturnsTrue);\nif (changed) {\n    // Enter was pressed, g_Text contains the input\n}"},{"language":"angelscript","snippet":"// ❌ ERR: Unexpected token '<identifier>'\nconst uint64 MASK = 0x3FFFFFFu;\nconst int BIG = 1ul;\n\n// ✅ OK: bare literal; compiler picks the type\nconst uint64 MASK = 0x3FFFFFF;\nconst int BIG = 1;\nconst uint64 KEY = uint64(0x123);     // explicit cast"},{"language":"angelscript","snippet":"// ❌ ERR: Expected '('\nconst int2 EDGES[12] = { int2(0,1), ... };\nconst vec3 CORNERS[8] = { ... };\n\n// ✅ OK: dynamic, no const\nint2[] g_Edges = { int2(0,1), ... };"},{"language":"angelscript","snippet":"void Foo() {\n    // ❌ ERR: Expected ';' Instead found identifier 'pts'\n    vec2 pts[8];\n    float depth[8];\n}\n\n// ✅ WORKAROUND: individual variables\nvoid Foo() {\n    vec2 p0, p1, p2, p3, p4, p5, p6, p7;\n    // compiler will register-allocate them\n}"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\r\nname: openplanet-plugin-dev\r\ndescription: Create, debug, structure, and run Openplanet AngelScript plugins for Trackmania 2020 (TMNEXT) and ManiaPlanet 4 (TM2/MP4). Covers API quirks, AngelScript pitfalls, performance patterns, the launch/verify loop, MP4 API-mismatch fixes, and proven templates. Use when building, debugging, reviewing, or launching Openplanet plugins.\r\nversion: 3.0.0\r\nmetadata:\r\n  openclaw:\r\n    tags: [openplanet, trackmania, angelscript, plugin, game-modding]\r\n---\r\n\r\n# Openplanet Plugin Development\r\n\r\nCore guide for Openplanet AngelScript plugins on **TM2020 (TMNEXT)** and\r\n**ManiaPlanet 4 (MP4/TM2)**. Deep material lives in `references/` — load only\r\nwhat the task needs (index at the bottom).\r\n\r\n## Golden rules\r\n\r\n1. **Verify API members against the reflection DB before writing code.**\r\n   - **Game Nod classes** live in `<GameDir>/Openplanet4.json` under\r\n     `ns.Game.<Class>.m[]` (entries `n`=name, `t`=type, `p`=parent). Walk it with\r\n     the procedure in `references/static-verify-workflow.md`.\r\n   - **Openplanet script namespaces** (`UI::`, `IO::`, `Time::`, `Json::`,\r\n     `Math::`, `Text::`, `Net::`, `Audio::`, `Icons::`, `getExceptionInfo()`) are\r\n     **NOT in `Openplanet4.json`**. They live in **`OpenplanetCore.json`** (same\r\n     game dir) under `functions[]` (filter by `ns`), `classes[]`, `enums[]`.\r\n     Extraction recipes + verified signatures: `references/openplanet-core-json.md`.\r\n     **Before assuming a built-in exists (e.g. `IO::WriteFile`, `UI::ListBox`,\r\n     `Time::Format(string)`), grep `OpenplanetCore.json`** — false assumptions\r\n     here cost a full kill+relaunch recompile cycle each time.\r\n   - MP4 game-class specifics: `references/mp4-api-verified.md` (editor) and\r\n     `references/mp4-runtime-and-dev-memory.md` (in-race / playground).\r\n   - **A class can appear in the DB with an EMPTY `m[]`** (e.g.\r\n     `ns.Scene.CSceneVehicleVis` is `{\"i\":...,\"s\":3464}` with no members). That is\r\n     not a lookup failure — it means the class has no scriptable named members and\r\n     the only way in is raw `Dev::GetOffset*` / `Dev::SetOffset` against its byte\r\n     size. Report that honestly instead of inventing member names.\r\n2. **Target game matters.** TM2020 examples often DON'T compile on MP4 (missing\r\n   UI overloads, missing members, wstring vs string). MP4 specifics:\r\n   `references/mp4-api-verified.md`, `references/mp4-api-mismatches.md`,\r\n   `references/mp4-supplement.md`, `references/mp4-ui-rendering.md`.\r\n3. **Unsigned folder plugins need Developer signature mode.** Launch with\r\n   `ManiaPlanet.exe /openplanet:developer` (or F3 → Developer → Signature Mode).\r\n   Otherwise the log shows `Invalid file hash` + `not suitable for the current\r\n   signature mode` and the plugin silently doesn't appear.\r\n4. **Debug via the log, not the GUI.** Clear `Openplanet.log`, launch, wait\r\n   ~50 s, then grep for `ERR :` / `compilation failed` / `Loaded plugin`.\r\n   Full command recipes: `references/launch-and-verif"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn70db4t9jcx94v2xfcg0pymyd87f4nt\",\n  \"slug\": \"openplanet-plugin-dev\",\n  \"version\": \"3.0.0\",\n  \"publishedAt\": 1786967913878\n}"},{"path":"references/api-namespaces.md","content":"# Openplanet API Reference - Namespaces\n\nSource: https://openplanet.dev/docs/api\n\n## List of all namespaces (as of Openplanet 1.29.5)\n\n| Namespace | Description |\n|-----------|-------------|\n| `// Global namespace` | Global functions |\n| `namespace UI` | UI tools (ImGui) |\n| `namespace nvg` | NanoVG drawing |\n| `namespace Audio` | Audio playback |\n| `namespace Auth` | Third-party API authentication |\n| `namespace Crypto` | Cryptography and hashing |\n| `namespace Display` | Display functions |\n| `namespace Fids` | Game files and folders |\n| `namespace IO` | Filesystem I/O |\n| `namespace Icons` | Icon helpers |\n| `namespace Json` | JSON serialization |\n| `namespace Math` | Math functions |\n| `namespace Net` | Networking and sockets |\n| `namespace Path` | File path operations |\n| `namespace Permissions` | Trackmania permission checks |\n| `namespace Regex` | Regular expressions |\n| `namespace SQLite` | SQLite database access |\n| `namespace Settings` | Openplanet settings |\n| `namespace Tests` | Testing utilities |\n| `namespace Text` | Text parsing and formatting |\n| `namespace Time` | Date and time |\n| `namespace XML` | XML deserialization |\n| `namespace mat3` | 3x3 matrix |\n| `namespace mat4` | 4x4 matrix |\n| `namespace string` | String static functions |\n| `namespace Reflection` | Type reflection |\n| `namespace Discord` | Discord rich presence |\n| `namespace Meta` | Openplanet meta API |\n| `namespace Import` | DLL import |\n| `namespace Dev` | Advanced memory access |\n| `namespace Internal` | Internal/built-in use only |\n\n## UI Namespace - Key Functions (AngelScript)\n\nFrom global namespace:\n- `bool UI::Begin(string name, bool* open = nil, UI.WindowFlags flags = 0)` - Begin window\n- `void UI::End()` - End window\n- `bool UI::BeginChild(string id, vec2 size = {0,0}, bool border = false, UI.WindowFlags flags = 0)`\n- `void UI::EndChild()`\n- `void UI::Text(string text)`\n- `void UI::TextColored(vec4 color, string text)` - NOTE: Does NOT exist in Openplanet AngelScript! Use PushStyleColor instead\n- `void UI::TextDisabled(string text)`\n- `void UI::TextWrapped(string text)`\n- `void UI::LabelText(string label, string text)`\n- `bool UI::Button(string label, vec2 size = {0,0})`\n- `bool UI::SmallButton(string label)`\n- `bool UI::Checkbox(string label, bool* value)`\n- `bool UI::RadioButton(string label, bool active)`\n- `bool UI::InputText(string label, string* buffer, int bufferSize, UI.InputTextFlags flags = 0)` - Returns true if text changed\n- `bool UI::Combo(string label, int* currentItem, string[] items)`\n- `bool UI::SliderFloat(string label, float* value, float min, float max, string format = \"%.3f\")`\n- `bool UI::SliderInt(string label, int* value, int min, int max, string format = \"%d\")`\n- `bool UI::DragFloat(string label, float* value, float speed = 1.0, float min = 0, float max = 0)`\n- `bool UI::ColorEdit3(string label, float[3] color, UI.ColorEditFlags flags = 0)`\n- `bool UI::ColorEdit4(string label, float[4] color, UI.ColorEditFlags flags = 0)`\n- `bool U"},{"path":"references/api-quirks-and-pitfalls.md","content":"## 🚨 CRITICAL — API Quirks & Pitfalls\n\n### 🔤 AngelScript Language Quirks\n\nThese bite everyone. AngelScript ≠ C++, ≠ C#, ≠ Java.\n\n#### 🔢 Integer literal suffixes\n\n`u`, `l`, `ul`, `ull` suffixes are **not supported**.\n\n```angelscript\n// ❌ ERR: Unexpected token '<identifier>'\nconst uint64 MASK = 0x3FFFFFFu;\nconst int BIG = 1ul;\n\n// ✅ OK: bare literal; compiler picks the type\nconst uint64 MASK = 0x3FFFFFF;\nconst int BIG = 1;\nconst uint64 KEY = uint64(0x123);     // explicit cast\n```\n\n#### 📐 `const` on value-type arrays\n\n`const` works for primitives, but **NOT for fixed-size arrays of value types** like `int2[]` or `vec3[]`.\n\n```angelscript\n// ❌ ERR: Expected '('\nconst int2 EDGES[12] = { int2(0,1), ... };\nconst vec3 CORNERS[8] = { ... };\n\n// ✅ OK: dynamic, no const\nint2[] g_Edges = { int2(0,1), ... };\n```\n\nAlso: `const` on `array<T>` is unreliable — keep globals un-`const` and use a `g_` prefix instead.\n\n#### 📏 Fixed-size local arrays\n\n**Local fixed-size arrays are not supported**:\n\n```angelscript\nvoid Foo() {\n    // ❌ ERR: Expected ';' Instead found identifier 'pts'\n    vec2 pts[8];\n    float depth[8];\n}\n\n// ✅ WORKAROUND: individual variables\nvoid Foo() {\n    vec2 p0, p1, p2, p3, p4, p5, p6, p7;\n    // compiler will register-allocate them\n}\n```\n\nOr use dynamic arrays:\n\n```angelscript\nvec2[] GetCorners() {\n    vec2[] cs = { vec2(0,0), vec2(1,0), ... };\n    return cs;\n}\n```\n\n#### 🗝️ `dictionary` key types\n\n**Most surprising limitation**: `dictionary` only accepts `const string&in` keys.\n\n```angelscript\ndictionary d;\n\n// ❌ ERR: No matching signatures to 'dictionary::Exists(uint64)'\nd[uint64(123)] = true;\nd[int(456)]    = true;\n\n// ✅ OK: build a string key\nd[\"123\"] = true;\nd[gx + \",\" + gy + \",\" + gz] = true;\n```\n\nIf you need a fast non-string key, roll your own hash table (parallel arrays + linear scan, or small open-addressed probe table). For most plugins, `string` keys are fine.\n\n#### 🔀 `uint` vs `int` in comparisons\n\n`array<T>.Length` returns `uint`. Comparing a signed `int` loop counter produces a warning treated as error in strict mode:\n\n```angelscript\n// ⚠️ WARN: Signed/Unsigned mismatch\nfor (int e = 0; e < arr.Length; e++) { ... }\n\n// ✅ CLEAN:\nfor (uint e = 0; e < arr.Length; e++) { ... }\n```\n\n#### 📤 `out` parameter naming\n\nMatch the parameter name **exactly**:\n\n```angelscript\n// ❌ ERR: No matching symbol 'outDepth'\nbool Project(vec3 &in p, vec2 &out screen, float &out depth) {\n    outDepth = 0.0f;\n}\n\n// ✅ OK:\nbool Project(vec3 &in p, vec2 &out screen, float &out depth) {\n    depth = 0.0f;\n}\n```\n\n#### 🎨 `int2`, `vec2`, `vec3`, `vec4` constructors\n\n```angelscript\nint2 a = int2(1, 2);\nvec3 v = vec3(1.0f, 2.0f, 3.0f);\nvec4 red = vec4(1.0f, 0.0f, 0.0f, 0.5f);\n```\n\nThey are value types — no `@` needed for storage.\n\n#### 🔄 `&inout` on primitive types is NOT allowed\n\n```angelscript\n// ❌ ERR:\nvoid ConfigRow(const string &in label, bool &inout value) { }\n\n// ✅ OK:\nvoid ConfigRow(const string &in label, bool value) { }\n```\n\n#### 📊 Array initiali"},{"path":"references/blockable-input-callbacks-mp4.md","content":"# BlockableInputCallbacks (BIC) on ManiaPlanet 4 — verified facts (2026-08)\r\n\r\nThe `ackepta/BlockableInputCallbacks` optimizer (already shipped as\r\n`Plugins/BlockableInputCallbacks.op`, siteid 893, v0.1.0) is the input layer used\r\nby trainers (e.g. `TrackmaniaTrainer`). Facts verified by unpacking the `.op`\r\n(zip) before using it:\r\n\r\n## manifest shape (uses `[script]` module/exports, NOT `dependencies`)\r\n```toml\r\n[meta]\r\nname = \"BlockableInputCallbacks\"\r\nauthor = \"achepta\"\r\nversion = \"0.1.0\"\r\nsiteid = 893\r\n\r\n[script]\r\nmodule = \"BlockableInputCallbacks\"\r\nexports = [ \"Export.as\" ]\r\nshared_exports = [ \"Shared.as\" ]\r\n```\r\nDependency plugin window: a plugin that imports it writes\r\n`[script] dependencies = [\"BlockableInputCallbacks\"]` in ITS info.toml.\r\n\r\n## import surface (Export.as)\r\n```angelscript\r\nnamespace BlockableInputCallbacks {\r\n    import int RegisterCallback(const string &in name, IInputCallback@ callback) from \"BlockableInputCallbacks\";\r\n    import int RegisterCallback(const string &in name, IInputCallback@ callback, float activationThreshold) from \"BlockableInputCallbacks\";\r\n    import void UnregisterCallback(int id) from \"BlockableInputCallbacks\";\r\n    import void UnregisterCallbacks(const string &in name) from \"BlockableInputCallbacks\";\r\n    import void SetInputBlocked(PadInput input, bool blocked) from \"BlockableInputCallbacks\";\r\n    import void SetInputBlocked(KeyboardInput input, bool blocked) from \"BlockableInputCallbacks\";\r\n    import KeyboardInput VirtualKeyToKeyboardInput(VirtualKey key) from \"BlockableInputCallbacks\";\r\n    import VirtualKey KeyboardInputToVirtualKey(KeyboardInput input) from \"BlockableInputCallbacks\";\r\n}\r\n```\r\n\r\n## verified enum values (Shared.as)\r\n- `PadInput`: `None = 255`, then per-pad buttons.\r\n- `KeyboardInput`: `None = 0x00`; function keys **F5 = 0x3F, F6 = 0x40, F8 = 0x42**\r\n  (F7 = 0x41 by sequence).\r\n- `InputDecision`, `PadInputEvent` shared enums — usable in the importing plugin.\r\n\r\n## pitfalls\r\n- Do NOT assume TM2020's REML can't bind F5/F6/F8 the same way: verify the enum\r\n  against `Shared.as` in THIS plugin's `.op` before relying on it (values above\r\n  are for v0.1.0).\r\n- The callbacks are into-game with a `IInputCallback@` interface — if the plugin\r\n  is missing from `Plugins/` at runtime the import fails the whole plugin pencil.\r\n- Simpler zero-dependency fallback for a trainer: plain `OnKeyPress(bool down,\r\n  VirtualKey key)` with `[Setting hidden] VirtualKey` — no BIC needed unless the\r\n  key must also be BLOCKED from reaching the game."}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":"Create, debug, structure, and run Openplanet AngelScript plugins for Trackmania 2020 (TMNEXT) and ManiaPlanet 4 (TM2/MP4). Covers API quirks, AngelScript pitfalls, performance patterns, the launch/verify loop, MP4 API-mismatch fixes, and proven templates. Skill: openplanet-plugin-dev Owner: tomekdot Summary: Create, debug, structure, and run Openplanet AngelScript plugins for Trackmania 2020 (TMNEXT) and ManiaPlanet 4 (TM2/MP4). Covers API quirks, AngelScript pitfalls, performance patterns, the launch/verify loop, MP4 API-mismatch fixes, and proven templates. Tags: latest:3.0.0 Version history: v3.0.0 | 2026-08-17T11:58:33.878Z | user Removed all bundled API reference","editorialQuality":{"score":100,"threshold":65,"status":"ready","wordCount":1929,"uniquenessScore":48,"reasons":[]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-11T06:05:19.302Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-11T06:05:19.302Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-11T08:39:50.842Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}