Skip to content

Repository files navigation

TM Control MCP

Local Openplanet bridge for controlling Trackmania from external tools and AI agents.

tm-control-mcp is an Openplanet plugin that exposes Trackmania’s map editor, main menu, and related game state over a localhost JSON TCP socket. Coding agents, scripts, and MCP-style clients can create maps, place freeblocks and items, drive menu flows, take screenshots, inject ManiaScript, and clean up after themselves — without clicking the UI by hand.

Socket 127.0.0.1:30006 (configurable)
Protocol One newline-terminated JSON request per TCP connection; newline JSON response; socket closes
Platform Trackmania (current) + Openplanet
Hard deps MLHook · MLFeedRaceData
Optional Editor++ (Editor) · Openplanet builtins VehicleState + Camera
License Dual Unlicense or CC0 1.0 — public domain / no attribution required (GitHub may show “Other”)
Status Active development (info.toml 0.4.0)
Script timeout 15000 ms (info.toml [script] timeout) — long menu/place/wait tools need headroom
Security Localhost JSON, no authSECURITY.md
Releasing Agent checklist → RELEASE.md · history → CHANGELOG.md
Migration From tm-mcptm → docs/migration-from-mcptm.md

Editor viewport capture via TakeScreenshot

Safety: the control socket is localhost-only by design and has no auth. Do not bind it to a public interface. Details: SECURITY.md.


What it provides

For AI / agent workflows

  • Readiness & waitingGetReadiness, WaitUntil, and tools/call.py --until-ready / --wait-mode so agents stop guessing “is the editor ready?”
  • Provenance tagsSetAgentTag → place → RemoveByTag cleans agent debris without nuking the whole map (ClearMapContent remains available when you mean it)
  • Placement verificationAssertPlacement (deltas, near-point, tags)
  • Structured errors — failed tools can include code, retryable, requiredMode, hint (plus the classic error string)
  • In-plugin guidesListGuides / GetGuide (menu nav, vistas, skins, cleanup, manialink runner, …)
  • ScreenshotsTakeScreenshot native viewport capture with in-plugin file detection, hideOverlay, forceRes; call.py adds Linux-side path detection

Editor control

  • Mode / map / dialogs: GetMode, OpenMapInEditor, GetMapInfo, GetMapEnvironment, SaveMapAs, GetDialog / RespondDialog
  • Cursor, selection, validation, camera (ControlCursor, ControlSelection, ControlValidation, ControlCamera, FocusCamera, …)
  • Freeblock / flying item placement via Editor++ (PlaceBlockViaEditorPlusPlus, PlaceItemViaEditorPlusPlus) — preferred over raw grid PlaceBlock for free work
  • Named macroblocks — compose in memory, batch-add blocks/items (with skins), preflight, place with offset/pivot rotation; durable JSON save/load under the Openplanet data folder
  • Inventory browse + picker control (BrowseInventoryTree, ControlInventory, SelectBlockModel / SelectItemModel / SelectMacroblockModel, ControlEditMode)
  • Deletion: recent/by-index via E++ DeleteItems / block delete; clear-all helpers

Menu automation

  • Full main-menu stack: route push (SetMenuPage), UI layer introspection, OnAction clicks (ClickMenuButton / TriggerControlOnAction)
  • Never use TriggerPageAction — that path crashes; this plugin uses the same CControlBase::OnAction dispatch as a real click
  • One-shot map create: CreateMapViaMenu (QuickStart off) or faster EditNewMap
  • Leave editor/race: BackToMainMenu

ManiaScript injection

  • RunManialinkScript injects ad-hoc ManiaScript through MLHook into menu / playground / editor (same contexts as MLHook’s UILayers browser)
  • Optional result channel: collectMs + SendCustomEvent("MLHook_Event_McpAdHoc_Result", …)

Dev / RE diagnostics

Pointer peeks, safe memory reads, gizmo/fuzz helpers, macroblock header dumps — only in DEV builds (./build.sh dev injects defines = ["DEV"]). Release .op packages omit these tools from the registry.

Ground truth tool list: {"route":"tools"} at runtime, or every MakeTool("…") in src/McpTools.as (104 tools (release; +7 DEV-only) at last count).


Requirements

  1. Trackmania (Nadeo) running under your normal install (native Windows or Proton/Wine).
  2. Openplanet for Trackmania.
  3. Plugins (install from Openplanet site or your usual channel):
    • Editor++ — optional, id Editor
      Tested with 0.8.x. Needed for free placement, inventory cache, named macroblocks, E++ delete. Without it the plugin still loads; those tools return missing_dependency.
    • MLHook — dependency id MLHook
      Tested with ≥ 0.5.4 (site id 252). Required for menu Router_Push and Manialink inject.
    • MLFeedRaceData — dependency id MLFeedRaceData
      Required for GetRaceData / GetPlayers race-mode tools.
  4. This plugin loaded as a folder plugin (dev) or .op package (release build).

Optional host tooling:

  • python3 for tools/call.py and tests
  • openplanet-lsp / tm-remote-build if you use ./build.sh dev reload (optional; can skip)

Install (dev folder plugin)

git clone https://github.com/clankercode/tm-control-mcp.git
cd tm-control-mcp

# Stage into Openplanet Plugins and reload (if RemoteBuild is available)
./build.sh dev

# If openplanet-lsp false-positives are noisy but in-game compile is green:
TM_PLUGIN_SKIP_LSP=1 ./build.sh dev

Default stage path: ~/OpenplanetNext/Plugins/tm-control-mcp
Override with OPENPLANET_DIR / PLUGINS_DIR.

Release .op package:

./build.sh release   # → tm-control-mcp-<version>.op

Enable the plugin in Openplanet’s UI. Confirm the socket:

python3 tools/call.py status
python3 tools/call.py GetMode

Openplanet settings (host / port / timeout)

This plugin exposes Server settings (and any future categories) through Openplanet’s normal settings UI and via MCP tools (ListPluginSettings / SetPluginSetting).

UI path

  1. Open the Openplanet overlay in-game.
  2. Open Settings (or Scripts → plugin settings).
  3. Select TM Control MCP (dev builds may show TM Control MCP (Dev)).
  4. Category Server:
    • Socket Host — default 127.0.0.1 (keep localhost-only)
    • Socket Port — default 30006
    • Startup Delay (ms) — delay before the listener binds
    • Trace Requests — append request/response payloads to plugin storage (request-trace.log), not Openplanet.log
    • Socket tab — live status, start/stop (the enable flag is hidden)

Host/port/enable apply live (no reload). Bind host is 127.0.0.1 only. S_TmMcpEnableSocket is hidden; use the Socket tab, TmMcp::SetSocketEnabled, or SetPluginSetting.

Script execution timeout is not a runtime setting: it is timeout = 15000 in info.toml (15s). Raise it there and rebuild/reload if a tool legitimately needs longer than 15s of continuous script time. Openplanet kills the script if a single invocation exceeds this budget.

Via MCP (after the plugin is loaded):

python3 tools/call.py ListPluginSettings '{"category":"Server"}'
python3 tools/call.py GetPluginSetting '{"varName":"S_TmMcpPort"}'
# Example: change port (applies live; point call.py --port at the new value)
python3 tools/call.py SetPluginSetting '{"varName":"S_TmMcpPort","value":30007}'
# Stop/start the TCP listener without unloading the plugin
python3 tools/call.py SetPluginSetting '{"varName":"S_TmMcpEnableSocket","value":false}'
python3 tools/call.py SetPluginSetting '{"varName":"S_TmMcpEnableSocket","value":true}'

Plugin manager tools

Tool Role
ListPlugins Meta::AllPlugins (+ optional unloaded)
GetPlugin One plugin by id/name; optional embedded settings
ControlPlugin enable / disable / setEnabled / reload / unload / load / openSettings / getLogs
ListPluginSettings List typed settings for a plugin (default: self)
GetPluginSetting / SetPluginSetting / ResetPluginSetting Read / write / reset
SavePluginSettings Meta::SaveSettings()

ControlPlugin refuses disable/unload/rebuild of itself so agents cannot brick the control channel by accident. reload of self is allowed but drops the socket.

load is RemoteBuild-compatible: pass {action:"load", id:"my-plugin"} to load Plugins/my-plugin/ (or my-plugin.op). If that plugin is already loaded it is unloaded then loaded from disk (required for .op archives — Openplanet locks them while loaded). getLogs tails Openplanet.log for compile errors (host-side in call.py when the in-game reader cannot open the locked log).

Protocol

One JSON object per connection, newline-terminated. Response is one JSON object + newline.

Status

{"route":"status"}

List tools

{"route":"tools"}

Call a tool

{"route":"call","tool":"GetMode","input":{}}

Tool names also work as routes:

{"route":"GetMapInfo","input":{}}

Typical success shape:

{"ok":true,"route":"call","data":{"tool":"GetMode","result":{"success":true,"output":{"mode":"MapEditor"}}}}

Errors may include structured fields:

{"success":false,"error":"","code":"wrong_mode","retryable":true,"requiredMode":"Editor","hint":""}

Client: tools/call.py

python3 tools/call.py status
python3 tools/call.py --pretty GetMode
python3 tools/call.py GetReadiness '{"want":"editor"}'
python3 tools/call.py --until-ready editor GetMapInfo
python3 tools/call.py --wait-mode Editor --wait-timeout 30 GetMode
python3 tools/call.py PlaceItemViaEditorPlusPlus \
  '{"itemPath":"LightCube2m","x":128,"y":64,"z":128,"yaw":15}'

Behavior:

  • Compact JSON by default (--pretty for humans)
  • Checks for a real Trackmania.exe process before connecting (--skip-process-check for raw socket debug only)
  • --strict validates tool input against the live schema when available
  • Screenshot path detection under common Proton prefixes (TM_USER_GAME_FOLDER override)

Agent-oriented quick start

# 1. Preflight
python3 tools/call.py --until-ready editor GetReadiness '{"want":"editor"}'

# 2. Tag agent work, place, verify, clean
python3 tools/call.py SetAgentTag '{"tag":"agent:demo"}'
python3 tools/call.py PlaceItemViaEditorPlusPlus \
  '{"itemPath":"LightCube2m","x":200,"y":80,"z":200}'
python3 tools/call.py AssertPlacement \
  '{"expectItemsDelta":1,"near":{"x":200,"y":80,"z":200,"radius":5},"tag":"agent:demo","tagMinCount":1}'
python3 tools/call.py RemoveByTag '{"tag":"agent:demo"}'

# 3. Named macroblock batch place
python3 tools/call.py CreateNamedMacroblock '{"name":"part-a","replace":true}'
python3 tools/call.py AddBlocksToNamedMacroblock '{"name":"part-a","blocks":[
  {"blockName":"RoadTechStraight","x":0,"y":0,"z":0,"yaw":0},
  {"blockName":"RoadTechStraight","x":32,"y":0,"z":0,"yaw":0}
]}'
python3 tools/call.py PreflightNamedMacroblockPlacement '{"name":"part-a","offsetX":128,"offsetY":64,"offsetZ":128}'
python3 tools/call.py PlaceNamedMacroblock '{"name":"part-a","offsetX":128,"offsetY":64,"offsetZ":128}'
python3 tools/call.py SaveNamedMacroblock '{"name":"part-a"}'   # durable JSON

# 4. Menu → new map (QuickStart must be off for CreateMapViaMenu)
python3 tools/call.py CreateMapViaMenu \
  '{"mapType":"race","environment":"Stadium","mood":"Day","inputDevice":"mouse","difficulty":"simple","timeoutMs":15000}'
# or faster title path (Stadium: omit deco or use 48x48Day; other envs need real deco):
python3 tools/call.py EditNewMap '{"environment":"Stadium","decoration":"48x48Day"}'

Habits that keep agents reliable

  1. Prefer GetReadiness / WaitUntil / --until-ready before mutate.
  2. Tag placements; clean with RemoveByTag instead of blind clear.
  3. Prefer E++ free placement and named macroblocks over one-by-one grid place.
  4. After menu nav: wait for pageVisible / mode change.
  5. Stadium maps: empty decoration="" can trap the vista prompt — use a real deco or omit.
  6. Do not depend on old disabled library plugins (mcp-tm / tm-mcptm).

Current tools (104 release / 111 DEV)

Ground truth: every MakeTool("…") registration in src/McpTools.as. Prefer {"route":"tools"} at runtime if this list drifts.

Mode / map / dialogs

Tool Summary
GetMode Current game mode (Menu / Race / Loading / MapEditor / ItemEditor / MediatrackerEditor / …). Editor is only the unknown-editor fallback; WaitUntil equals=Editor still matches any editor.
OpenMapInEditor Open a local map file in the editor (path). Warns if a leave-map dialog pops.
GetMapInfo Current editor map name and counts (+ bounds).
GetMapEnvironment Collection, decoration, map type/style, mood, collection-unit metadata.
SetMoodTimeOfDay Set MoodTimeOfDay01 (0..1); optional dynamicMoodIsDynamicTime. Change TOD before a second LM bake.
ComputeShadows Start ComputeShadows1 (does not wait). quality Default default; bustCache default true. Then WaitUntil condition=shadowsClear.
ControlMapObjectives Get/set race objectives: nbClones, nbLaps, isLapRace (E++).
SaveMapAs Save under user Maps (name+folder or fileName; overwrite).
GetDialog Inspect BasicDialogs state / active frame.
RespondDialog Respond: yes, no, cancel, ok, validate, hide, …

Readiness / agent provenance / assert

Tool Summary
GetReadiness Composite preflight (want=editor|menu|any|race).
WaitUntil Poll mode/dialog/editorReady/pageVisible/map counts/readiness/shadowsClear (timedOut on budget).
GetLightmapComputeState Whether the editor is computing shadows / lightmap.
SetAgentTag Default provenance tag for subsequent Place* calls (empty clears).
ListTagged List tracked tagged placements (prefix / tag: prefix).
RemoveByTag Delete live objects matching tag (re-resolve by pos+idName); optional dryRun.
ClearTagIndex Drop sidecar index only (no map mutation).
AssertPlacement Verify deltas / near / tags after place.

Editor selection / cursor / camera / edit mode

Tool Summary
ControlValidation Validation / test / playground.
ControlSelection Copy-paste / custom selection.
GetCursor Editor cursor coord + selected block name/id.
GetEditorSelectionState Placement modes, picked block, selected models, cursor, variant.
ControlCursor raise/lower/rotate/move (relative/cardinal), followCamera, RGB, …
ControlEditMode Inspect/set EditMode / PlaceMode; optional model select.
GetEditorCamera / SetEditorCamera Numeric camera target/angles/distance.
ControlCamera centerOnCursor, watchWholeMap/start/CP/finish, zoom, look, …
FocusCamera Focus on world (x,y,z) via E++ animation.
ControlPlayCamera In-play camera: read state, switch mode (free / cam 1-3 / backwards / alt), place and aim the free camera.
TakeScreenshot Native viewport screenshot with file detection (fullName), hideOverlay, forceRes.

Blocks / items (read)

Tool Summary
GetBlocks By grid/world radius, model query, freeblock filter.
GetRecentBlocks Last N blocks (freeblock pos/rot readback).
GetBlockAt Exact grid (x,y,z).
GetItems Near world pos, or all up to limit.
GetRecentItems Last N anchored items.

Inventory / models

Tool Summary
GetInventorySummary Cache counts + scan status.
FindInventory Search blocks/items/macroblocks.
RefreshInventory Rescan after mid-session content adds.
BrowseInventoryTree Read-only tree (root, path, depth, query, …).
ControlInventory status / select / openFolder via inventory SelectArticle/SelectNode.
InspectMacroblockModel Loaded MB by name/path/index.
ListMacroblockInstances Placed native MB instances.
FindBlockModels Search loaded block models.
SelectBlockModel / SetCursorBlock Set selected block model.
SelectItemModel / SelectMacroblockModel Picker helpers for item/MB.

Named macroblocks

Tool Summary
CreateNamedMacroblock Create/replace in-memory handle.
GetNamedMacroblock / ListNamedMacroblocks / ClearNamedMacroblock Inspect / list / clear.
AddBlock(s)ToNamedMacroblock Free block specs (variant, bg/fg skins).
AddItem(s)ToNamedMacroblock Flying item specs by inventory path (+ skins).
PlaceNamedMacroblock Place via E++ with offset/pivot rotation + mapPre/mapPost.
PreflightNamedMacroblockPlacement Non-mutating extents/bounds/model checks.
SaveNamedMacroblock Persist JSON under Openplanet data tm-control-mcp/named-mb/.
LoadNamedMacroblock Load durable JSON into memory (resolves models).
ListSavedNamedMacroblocks List durable JSON files.

Placement / deletion / undo

Tool Summary
CanPlaceBlock Grid/terrain place check without mutating.
PlaceBlock Grid block place.
PlaceBlockViaEditorPlusPlus Free blocks via E++ (preferred for free work).
PlaceItemViaEditorPlusPlus Flying items via E++.
RemoveBlock Remove at grid coords.
ClearBlocks / ClearItems / ClearMapContent PluginMapType remove-all helpers.
RemoveRecentBlocks / RemoveBlocksByIndex E++ block deletion.
RemoveRecentItems / RemoveItemsByIndex E++ DeleteItems (buffer fallback opt-in).
Undo / Redo Editor undo/redo.

Menu automation

Menu stack is landed. Clicks use CControlBase::OnActionnot TriggerPageAction. Poll GetActiveMenuPages / GetMode / GetDialog after nav.

Tool Summary
SetMenuPage MLHook Router_Push hierarchical route.
GetMenuPage / ListKnownMenuRoutes Mode + menu module; route catalogue.
EditNewMap Title-control new map (env + decoration + mapType). Warns if leaving a dirty map pops AskYesNo.
BackToMainMenu Unwind Editor/Race → menu. Warns and stops early on the unsaved-changes dialog.
GetUILayers / GetActiveMenuPages / GetLayerTree / GetLayerXml Layer introspection.
ListMenuManialinkControls / FindMenuButtons / FindControlsByClass / FindControlsByLabel Discovery.
InspectMenuControl / FocusMenuControl / SetMenuControlVisible Probe / focus / show-hide.
ClickMenuButton / TriggerControlOnAction Real click dispatch.
CreateMapViaMenu Full Page_MapEditorSettings click-chain → Editor (QuickStart off).

ManiaScript + guides

Tool Summary
RunManialinkScript MLHook inject (menu/in-map/in-editor/current); optional collectMs/resultEvent.
ListGuides / GetGuide In-plugin documentation topics.

DEV / RE diagnostics

Only registered when the plugin is built with defines = ["DEV"] (./build.sh dev). Release .op / release-check builds omit these tools.

Tool Summary
RunGizmoApplyBlock Free block through E++ gizmo apply path.
RunRandomFuzz Random place N blocks/items in a world bbox.
RunComputeItemsDiagnostic / DevComputeItemsPointers Macroblock compute-items probes.
DevSafeRead / DevGetPointers Safe memory read / pointer dumps.
DumpMacroblockHeader MB flags/buffers/raw header words.

Openplanet plugins & settings

Tool Summary
GetTypeInfo Live Openplanet Reflection: class + all members. Default {className}.
ControlFids Fids:: drives/get/list/preload/updateTree/extract/getFullPath/fromNod.
ListPlugins Loaded plugins (query, includeDisabled, includeUnloaded).
GetPlugin One plugin by id/name; includeSettings optional.
ControlPlugin enable/disable/reload/unload/load/openSettings/getLogs (no self-disable/unload/rebuild).
ListPluginSettings Settings for a plugin (default: this MCP plugin).
GetPluginSetting / SetPluginSetting / ResetPluginSetting Typed get/set/reset.
SavePluginSettings Persist settings to disk.

Behavioral notes

Placement and map metadata

Mutating placement tools include mapPre / mapPost (name, size, block/item/vertex counts, bounds). Prefer verifying with GetBlockAt / GetRecent* / AssertPlacement.

SaveMapAs writes under the user Maps folder; responses may include a Wine/Trackmania gamePathHint — map through your Proton prefix on Linux.

Free placement and inventory

Prefer E++ free placement and named macroblocks for generated builds. Rotation defaults to degrees (pitch/yaw/roll); use *Rad for radians. Autofocus is on by default for free place tools.

Path-based place remains the reliable headless path; ControlInventory is for picker parity / UI-native flows.

Item delete via Editor::DeleteItems

RemoveRecentItems / RemoveItemsByIndex call E++ DeleteItems (works when PluginMapType.Items is empty). Direct AnchoredObjects buffer removal is opt-in (forceBufferFallback=true) and reports undoSupported=false.

Named macroblocks + skins

Block/item specs support variant, bgSkin, fgSkin. Skins apply after successful PlaceNamedMacroblock and are verified when possible. Durable JSON is v2-capable (item skins) with v1 load compatibility. In-memory handles alone do not survive reload — use SaveNamedMacroblock.

In-play camera (ControlPlayCamera)

Openplanet exposes no typed handle to the playground camera, so this tool walks raw offsets anchored on reflected members where possible. Offsets and the values they take are lifted from shipped plugins, not guessed — see the header comment in src/PlayCamera.as for the provenance of each one.

Placing and aiming the free camera is exact. Pass x,y,z and target{x,y,z} and the camera lands on x,y,z to the millimetre with the target on the exact screen centre — verified against ProjectWorldToScreen (960, 540 of 1920x1080) at several angles including straight down. That is the mode to use for measurement work: known camera point, known look-at, known distance.

Under the hood that is the orbital form. The free camera has two mutually exclusive parameterisations:

driven by orientation
free m_FreeVal_Loc_Translation whatever the engine last built
orbital (m_TargetIsEnabled) m_TargetPos + m_Radius + m_Yaw/m_Pitch always looks at the target

pos = target - radius * (cos v * sin h, -sin v, cos v * cos h), h = yaw, v = pitch, positive pitch above the target — the same convention as the editor orbital camera in CameraMaths.md, so LookDirToOrbitalAngles is reused to solve it.

Consequences worth knowing before you trust a number:

  • pitch/yaw/roll without a target do nothing to the view. They are written and read back faithfully, but the free camera only rebuilds its orientation matrix while the player is moving the mouse, so scripted angles sit unused. Aim with a target instead.
  • A live target overrides a position write, so x,y,z on its own releases the target first and says so in steps.
  • Cams 1/2/3 cannot be moved. They are rigidly attached to the car.
  • set_mode is only consumed on a live playground tick. When the tick does not read the request the camera simply stays put, so the response carries switched: true|false and a warning; judge the result on that and on the after readback, never on success. Observed reliable with the game in the foreground and no overlay up, and observed silently dropped — every mechanism, every mode — while the Openplanet overlay and plugin windows were open over the view, with uiSequence: Playing, the window focused, the player spawned, not spectating and not force-cammed. Close the overlay before switching. Free cam itself, and placing it, kept working throughout, because the position is read by the render path rather than the tick.
  • spectator.forceCameraType is a 4-bit field: 15 is the all-bits-set "not forced" sentinel that ManiaScript writes as -1. Seeing 15 does not mean something is forcing the camera; spectator.forced says that.
  • Needs an active playground — solo/local Race, editor test runs, spectating. NO_PLAYGROUND in menus. In the map editor use SetEditorCamera / ControlCamera instead. MediaTracker clips override this camera.

Screenshots

TakeScreenshot triggers the game's native viewport capture (same path as the in-game screenshot key), waits for the file, and returns its game-side fullName + sizeBytes. Options: format (jpg default / webp / tga / dds), waitMs (default 5000; 0/noWait = fire-and-forget), hideOverlay (clean shot without HUD/overlays), forceRes + width/height (capture at a forced resolution; restored after). Crash-prone native paths (360° capture, tiled supersampling, alpha/pixel-output switching) are deliberately not exposed — see the screenshots guide (GetGuide {"topic":"screenshots"}).

call.py additionally reports detectedScreenshot.linuxPath by diffing the folder before/after (TM_USER_GAME_FOLDER override).

Menu automation details

  • Routes are hierarchical (/create/mapeditorsettings, not bare /mapeditorsettings).
  • SetMenuPage only while main-menu module is active.
  • Side-effect / playground-launch routes blocked unless allowPlaygroundLaunch:true.
  • Enter editor: EditNewMap (fast) or CreateMapViaMenu (full UI; QuickStart off).
  • Leave: BackToMainMenu, poll until mode=="Menu".

RunManialinkScript

Arg Default Notes
script (required) No outer <manialink> — MLHook wraps as MLHook_<pageUid>.
context current menu / in-map / in-editor / current.
pageUid McpAdHoc Attach id stem.
replace true Replace same uid.
persist true If false, remove after waitMs.
waitMs 150 Yield for inject queue (capped).
collectMs 0 If >0, register result hook and collect events.
resultEvent McpAdHoc_Result Script: SendCustomEvent("MLHook_Event_"+resultEvent, [...]).

Bad ManiaScript can force a game recovery restart — keep scripts small.

Stadium / decoration pitfall

For EditNewMap: Stadium — omit decoration or use 48x48Day. Non-Stadium needs a real decoration string (or use OpenMapInEditor / CreateMapViaMenu). Empty decoration="" can leave the vista prompt up.


Project layout

src/                 Openplanet Angelscript plugin (module TmMcp)
tools/call.py        CLI client + wait helpers + screenshot detection
tests/               pytest (call.py waits, camera math, …)
docs/ research/      Design notes and RE notes
AGENTS.md            Short agent-oriented project notes
info.toml            Openplanet manifest (deps: Editor, MLHook)
build.sh             dev stage+reload · release .op pack

Development

# Stage + reload into game
TM_PLUGIN_SKIP_LSP=1 ./build.sh dev

# Unit tests that do not need the game
python3 -m pytest tests/test_call_wait.py -q

# Live smoke (game + plugin running)
python3 tools/call.py status
python3 tools/call.py GetReadiness '{"want":"editor"}'

In-game Openplanet compile is the ground truth if LSP reports nested-enum / dependency noise.


Security

Localhost-only control socket with no authentication. Anyone on the machine can mutate maps, drive menus, and call DEV tools while the plugin is loaded. Keep Socket Host = 127.0.0.1. Full threat model and hardening: SECURITY.md.


Related

  • Editor++ — free placement, inventory cache, deletion, camera helpers this plugin calls into
  • MLHook — menu router push, Manialink inject, custom events
  • Sibling tooling in the Openplanet ecosystem (RemoteBuild, openplanet-lsp) optional for the reload loop

License

Dual-licensed under the Unlicense or CC0 1.0 Universal, at your option. See LICENSE.

No warranty. Trackmania and Nadeo assets remain subject to their own terms; this repo is the control bridge only.


Credits

Author: XertroV (info.toml). Maintained under clankercode.

About

Openplanet localhost JSON bridge for Trackmania editor/menu control (AI agents + scripts)

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages