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 auth → SECURITY.md |
| Releasing | Agent checklist → RELEASE.md · history → CHANGELOG.md |
| Migration | From tm-mcptm → docs/migration-from-mcptm.md |
Safety: the control socket is localhost-only by design and has no auth. Do not bind it to a public interface. Details: SECURITY.md.
- Readiness & waiting —
GetReadiness,WaitUntil, andtools/call.py--until-ready/--wait-modeso agents stop guessing “is the editor ready?” - Provenance tags —
SetAgentTag→ place →RemoveByTagcleans agent debris without nuking the whole map (ClearMapContentremains available when you mean it) - Placement verification —
AssertPlacement(deltas, near-point, tags) - Structured errors — failed tools can include
code,retryable,requiredMode,hint(plus the classicerrorstring) - In-plugin guides —
ListGuides/GetGuide(menu nav, vistas, skins, cleanup, manialink runner, …) - Screenshots —
TakeScreenshotnative viewport capture with in-plugin file detection,hideOverlay,forceRes;call.pyadds Linux-side path detection
- 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 gridPlaceBlockfor 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
- Full main-menu stack: route push (
SetMenuPage), UI layer introspection, OnAction clicks (ClickMenuButton/TriggerControlOnAction) - Never use
TriggerPageAction— that path crashes; this plugin uses the sameCControlBase::OnActiondispatch as a real click - One-shot map create:
CreateMapViaMenu(QuickStart off) or fasterEditNewMap - Leave editor/race:
BackToMainMenu
RunManialinkScriptinjects 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", …)
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).
- Trackmania (Nadeo) running under your normal install (native Windows or Proton/Wine).
- Openplanet for Trackmania.
- 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 returnmissing_dependency. - MLHook — dependency id
MLHook
Tested with ≥ 0.5.4 (site id 252). Required for menuRouter_Pushand Manialink inject. - MLFeedRaceData — dependency id
MLFeedRaceData
Required forGetRaceData/GetPlayersrace-mode tools.
- Editor++ — optional, id
- This plugin loaded as a folder plugin (dev) or
.oppackage (release build).
Optional host tooling:
python3fortools/call.pyand testsopenplanet-lsp/tm-remote-buildif you use./build.sh devreload (optional; can skip)
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 devDefault stage path: ~/OpenplanetNext/Plugins/tm-control-mcp
Override with OPENPLANET_DIR / PLUGINS_DIR.
Release .op package:
./build.sh release # → tm-control-mcp-<version>.opEnable the plugin in Openplanet’s UI. Confirm the socket:
python3 tools/call.py status
python3 tools/call.py GetModeThis plugin exposes Server settings (and any future categories) through Openplanet’s
normal settings UI and via MCP tools (ListPluginSettings / SetPluginSetting).
UI path
- Open the Openplanet overlay in-game.
- Open Settings (or Scripts → plugin settings).
- Select TM Control MCP (dev builds may show TM Control MCP (Dev)).
- 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)
- Socket Host — default
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}'| 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).
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":"…"}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 (
--prettyfor humans) - Checks for a real
Trackmania.exeprocess before connecting (--skip-process-checkfor raw socket debug only) --strictvalidates tool input against the live schema when available- Screenshot path detection under common Proton prefixes (
TM_USER_GAME_FOLDERoverride)
# 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
- Prefer
GetReadiness/WaitUntil/--until-readybefore mutate. - Tag placements; clean with
RemoveByTaginstead of blind clear. - Prefer E++ free placement and named macroblocks over one-by-one grid place.
- After menu nav: wait for
pageVisible/ mode change. - Stadium maps: empty
decoration=""can trap the vista prompt — use a real deco or omit. - Do not depend on old disabled library plugins (
mcp-tm/tm-mcptm).
Ground truth: every MakeTool("…") registration in src/McpTools.as.
Prefer {"route":"tools"} at runtime if this list drifts.
| 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 dynamic → MoodIsDynamicTime. 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, … |
| 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. |
| 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. |
| 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. |
| 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. |
| 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. |
| 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 stack is landed. Clicks use CControlBase::OnAction — not
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). |
| Tool | Summary |
|---|---|
RunManialinkScript |
MLHook inject (menu/in-map/in-editor/current); optional collectMs/resultEvent. |
ListGuides / GetGuide |
In-plugin documentation topics. |
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. |
| 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. |
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.
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.
RemoveRecentItems / RemoveItemsByIndex call E++ DeleteItems (works when PluginMapType.Items is empty). Direct AnchoredObjects buffer removal is opt-in (forceBufferFallback=true) and reports undoSupported=false.
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.
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/rollwithout 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,zon its own releases the target first and says so insteps. - Cams 1/2/3 cannot be moved. They are rigidly attached to the car.
set_modeis only consumed on a live playground tick. When the tick does not read the request the camera simply stays put, so the response carriesswitched: true|falseand awarning; judge the result on that and on theafterreadback, never onsuccess. 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, withuiSequence: 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.forceCameraTypeis a 4-bit field:15is the all-bits-set "not forced" sentinel that ManiaScript writes as-1. Seeing 15 does not mean something is forcing the camera;spectator.forcedsays that.- Needs an active playground — solo/local Race, editor test runs, spectating.
NO_PLAYGROUNDin menus. In the map editor useSetEditorCamera/ControlCamerainstead. MediaTracker clips override this camera.
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).
- Routes are hierarchical (
/create/mapeditorsettings, not bare/mapeditorsettings). SetMenuPageonly while main-menu module is active.- Side-effect / playground-launch routes blocked unless
allowPlaygroundLaunch:true. - Enter editor:
EditNewMap(fast) orCreateMapViaMenu(full UI; QuickStart off). - Leave:
BackToMainMenu, poll untilmode=="Menu".
| 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.
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.
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
# 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.
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.
- 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
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.
Author: XertroV (info.toml). Maintained under clankercode.
