Skip to content

feat(worldbuilder): Adds an MCP server for AI-driven map, script and skirmish AI editing - #125

Merged
pWn3d1337 merged 31 commits into
mainfrom
worldbuilder_mcp
Oct 17, 2026
Merged

pWn3d1337 merged 31 commits into
mainfrom
worldbuilder_mcp

Conversation

@pWn3d1337

Copy link
Copy Markdown
Collaborator

Summary

Adds a Model Context Protocol server that lets an AI assistant (Claude Code, or any MCP client) drive a running WorldBuilder. It covers map editing (terrain, objects, waypoints, roads, water) as well as scripts, teams and the skirmish AI around them. It also adds an engine feature that plays headless AI-vs-AI skirmishes and logs them, so that AI changes can be tested.

MCP client  <-- stdio -->  scripts/worldbuilder-mcp/server.py  <-- TCP 127.0.0.1:47800 -->  WorldBuilderZH.exe -mcp

The bridge only starts with -mcp. Without it WorldBuilder behaves as before.

What's included

WorldBuilder bridge (GeneralsMD/Code/Tools/WorldBuilder/src/mcp/)

  • A localhost JSON command server that runs handlers on the UI thread. Every edit goes through an Undoable, so each MCP edit is one undo step.
  • Map: info, resize, mirror/rotate/symmetrize, replace, usage, start position generation, screenshots of the 3D view.
  • Terrain: brushes, set heights, procedural generation presets, slope limiting, passability, heightmap and mask image import/export, texture painting, auto texturing and blending.
  • Objects / waypoints / areas / water / roads: placement, scattering, properties, waypoint links, polygon triggers, terrain-aware roads.route.
  • Skirmish setup: ai_skirmish_setup / ai_skirmish_check create and check the perimeters, Center<N> / Flank<N> / Backdoor<N> paths and, on maps with EnableShips, shipyard waypoints, NavalPerimeter<N> and the Naval<N> / NavalFlank<N> paths.
  • Scripts and teams: list, get, find, add, update, move, delete, groups, batches (all or nothing), copy, clone a player, rename with references, verify, missing references, dead-logic analysis, compare, build order and trace views, .scb import/export, and build list editing.
  • Faction data: faction.describe reads templates, prerequisites, sciences, upgrades, weapons, armor and locomotors, so the server can describe what a faction can build and fight.

Python server (scripts/worldbuilder-mcp/)

  • server.py: the MCP tools on top of the bridge (run with uv, registered in .mcp.json).
  • faction.py: generates a faction description (tech tree, unit roles, naval roles, phases) with a reviewable .overrides.yaml, plus faction_check against a script player.
  • framework.py: the scripts, flags, teams and hooks every skirmish AI shares.
  • scaffold.py: derives a new faction AI from a related faction's player.
  • aidata.py: reads AIData.ini and edits SkirmishBuildList and SideInfo surgically, keeping comments.
  • match.py: runs AI matches and turns their logs into a report.
  • smoke_test.py exercises the bridge end to end. README has the full tool reference.

Engine

  • AI matches (GameLogic/AIMatch.h/.cpp): generalszh.exe -aiMatch <map> -aiPlayers Faction:difficulty[:team],... -aiMatchLog <file> [-aiMatchMinutes] [-aiMatchInterval] [-aiMatchCash] [-aiMatchScripts <scb>] [-aiMatchAIData <ini>] [-seed] [-headless]. Plays an observed skirmish between AI players and writes a JSON-lines log (snapshots, teams, sciences, upgrades, scripts that ran, script messages, defeats). With -headless it runs without the game client, much faster than real time; the same seed gives the same match.
  • Read accessors on ThingTemplate, ProductionPrerequisite, Science, Upgrade, Armor, Locomotor, Weapon and ScoreKeeper, for the tools. No behaviour change.

Fixes

  • fix(ai): MAX_AI_UPGRADES was 20; it now equals MAX_PURCHASE_SCIENCES (new Common/SciencePurchaseSlots.h), so a skill set can list every purchasable science.
  • fix(thing): prerequisites set in an ObjectExtend now replace the inherited ones instead of being added to them. This changes game behaviour for mod data that relied on the old merging.
  • fix(debug): -ignoreAsserts stays in effect during shutdown, and WorldBuilder honours it from GameData parsing on.
  • fix(filesystem): skips the "Go install Generals!" assert when the Generals archives were found in the game folder (mods, repacks).
  • WorldBuilder: heights respect the map's HeightMapScale, and the brush tools no longer truncate heights to a byte.
  • fix(worldbuilder): MCP flood fill replace and camera pitch.

Docs

  • CLAUDE.md is now tracked (removed from .gitignore) and has a WorldBuilder MCP section.

Usage

  1. Start WorldBuilder from the game/mod folder: WorldBuilderZH.exe -mcp -ignoreAsserts.
  2. Open the repo in Claude Code; the worldbuilder server from .mcp.json starts through uv.
  3. See scripts/worldbuilder-mcp/README.md for the tools.

Testing

  • Built z_generals and z_worldbuilder with the w32-modding-zh preset.
  • scripts/worldbuilder-mcp/smoke_test.py against a running WorldBuilder.
  • AI matches with the Operation Firestorm skirmish scripts, windowed and headless (same seed gives an identical log).

🤖 Generated with Claude Code

pWn3d1337 and others added 30 commits September 30, 2026 18:26
WorldBuilder started with -mcp (or -mcpport:N / WB_MCP_PORT) listens on
127.0.0.1 for line-delimited JSON commands. A worker thread services the
socket and hands every command to the UI thread, where it runs through the
normal undo system. Commands cover map new/open/save/settings, undo/redo,
camera and 3D screenshots, terrain heights, brushes, textures and
passability, objects, players and teams, waypoints, trigger and water
polygons, and roads.

scripts/worldbuilder-mcp contains the Python MCP server (uv project) that
exposes these commands as MCP tools, a smoke test, and a README. The repo
.mcp.json registers it for Claude Code.

Also adds -ignoreAsserts to WorldBuilder so debug-build assertions are
logged instead of opening blocking dialogs, which would otherwise stall
both startup with mod data and bridge commands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
terrain.flood_fill with replace_all, or with a texture that no longer
fits in the map, silently did nothing: WorldHeightMapEdit::floodFill
opened a confirmation message box, which blocks a bridge command. It now
takes an askToConfirm flag; the interactive tool keeps prompting, the
bridge skips the prompt and reports clear errors instead.

The camera pitch was documented backwards (larger is steeper), and with
pitch != 1 the requested x/y was no longer at the screen center, because
WorldBuilder tilts the view ray away from the camera target. The bridge
now compensates so x/y is the point in the middle of the screen, reports
that point back, and rejects pitch <= 0 before changing the camera.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New bridge commands exchange terrain data with image files (north up):
- terrain.export_heightmap / import_heightmap: one pixel per vertex,
  raw heights, 16-bit import, set or add mode with a height range,
  stretch/exact/center fit, optional smoothing.
- terrain.export_mask / import_mask: one pixel per cell for
  passability, a texture or water coverage; import paints a texture or
  sets passability where the mask passes a threshold.

Images are read with stb_image, compiled only into WorldBuilder. The
MCP tools resolve relative paths and can return exported images so the
model can inspect the terrain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
map.transform mirrors or rotates the whole map (mirror_x/y, rotate_90/
180/270, diagonal mirrors). map.symmetrize copies one half, triangle or
quarter onto the rest for symmetric multiplayer maps (mirror_x/y,
rotate_180, mirror_diag/antidiag, rotate_quarters).

Both cover heights, passability, textures, objects, waypoints and areas
in one undo step. New WorldHeightMapEdit::copyTransformedFrom re-derives
tiles for their new position and re-orients blend layers, including the
forced triangle flips of 3-way blends. Cliff UV mapping is reset on
copied cells.

Symmetrize keeps objects near the axis once, keeps road pairs intact,
recreates waypoint links between copies (undoable), and renumbers
players in names and owners per copy (Player_1_Start -> Player_2_Start,
P1_, Perimeter1, teamplayer0001), with explicit rename pairs and an
owner map as overrides.

The feature set follows the Genesis map tools by The CWC Team.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New bridge commands, modeled on the Genesis map generator processors:
- terrain.generate: layered seeded Perlin noise (fractal, ridged or
  signed layers) with area limits, feathering and protected spots.
- terrain.limit_slope: caps height steps between neighbours; cliff
  flags follow the new slopes.
- terrain.auto_texture: classifies cells as ground, cliff (slope or
  impassable) or water (height or water polygons), paints base textures
  with noise overlays, checks texture space up front, then blends.
- terrain.blend_all / remove_blends: bulk blending helpers.

The noise is ported from Genesis PerlinNoise.cs (Apache-2.0, The CWC
Team) with our own seeding; layout and scenery presets come from Genesis
and are loaded by the Python server. See scripts/worldbuilder-mcp/NOTICE.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds generate, limit_slope, auto_texture and the blend helpers to the
smoke test, undoing exactly the steps that changed the map, and documents
the procedural terrain tools and the single-client bridge in the README.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ai.skirmish_setup creates, for every Player_<N>_Start waypoint, the data
the skirmish AI looks up by name: InnerPerimeter<N>/OuterPerimeter<N>
areas and three approach paths into the base labeled Center<N>,
Flank<N> and Backdoor<N> (the number is the defending player, see
doTeamFollowSkirmishApproachPath and buildAIBaseDefenseStructure), plus
a CombatZone area. Waypoints stay inside the map and are moved off
cliffs and water. Existing data, including unnumbered labels the AI
ignores, is replaced, all in one undo step.

ai.skirmish_check reports per start which areas and paths exist and
lists what is missing.

The waypoint links undoable moves to a shared header. The idea follows
the SkirmishAIGenerator of Genesis (The CWC Team).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Scatters weighted templates over the playable map or a circle/rect by
count or density, with random rotation and a deterministic seed. It
keeps a minimum spacing, stays clear of roads, of existing objects'
footprints, of Player_<N>_Start positions, cliffs and water, and can be
limited by slope, texture and named areas. An optional noise mask groups
objects into groves. All objects are added in one undo step.

The idea follows the TreeGenerator and RockGenerator of Genesis (The CWC
Team).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
roads.route finds a road between two points (and optional via points)
with A* over the heightmap vertices. Cliffs, water, structure footprints
and named areas are blocked, slopes add cost and steps above max_grade
are forbidden. Stops on blocked ground are reached from the nearest free
vertex.

The grid path is straightened and nearly collinear points are dropped;
a shortcut must be neither clearly costlier nor clearly steeper than the
path it replaces, so the road does not cut across features the
pathfinder went around. Sharp bends get tight corners, ends snap to
existing road points, and the ground under the road can be flattened to
a smoothed profile. Road and terrain are one undo step.

roads.add now shares its segment builder (mcpBuildRoadChain). The idea
follows the RoadGenerator and RoadFlattener of Genesis (The CWC Team).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
map.usage lists the textures (with cell counts), object templates and
road types a map uses and flags templates the game does not know, which
is the starting point for converting a map made for another mod.

map.replace swaps textures, object templates and road or bridge types
everywhere from {from: to} mappings, in one undo step, with a dry run.
Textures keep their blends, objects keep position, rotation, owner and
properties, roads keep their flags. All targets are validated first and
chained mappings are rejected because their result would depend on the
order. The Python tool also reads Genesis-style "from to" list files.

The modify-objects undoable moves to the shared header and is reused by
objects.modify, map.transform and map.replace. The idea follows the
Replace command of the Genesis map tools (The CWC Team).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Places Player_<N>_Start waypoints for 2 to 8 players, evenly spaced
around the map center on a rectangle inset from the playable edge.
Each base area is flattened with a feathered edge and lifted above
water. Existing start waypoints are replaced, and the whole operation
is one undo step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Grows or crops the map, either to a new size with an anchor or by a
number of cells per side. Terrain, textures, passability, objects,
waypoints, roads, areas, water and build lists keep their place on the
terrain. New cells continue the old edge or get a fixed height and
texture. Objects and waypoints that fall off the map can be removed,
and areas that cover the whole map are refit. One undo step.

Adds WorldHeightMapEdit::resizeBySides for this. Unlike resize() it
keeps manual passability, updates the map boundaries and never shows
a dialog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…are found locally

Self-contained folders (mods, repacks) carry the original Generals archives
in a subfolder that the local scan already loads. The assert for a missing
registry install path fired there on every start of the game and of
WorldBuilder, before -ignoreAsserts takes effect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lets the MCP bridge do what the script editor and the Teams dialog do,
without their windows. Edits work on a copy of the sides list and commit
it as one undo step, like the script editor's OK. They are refused while
the script editor window is open, since its OK would overwrite them.

Scripts: list, get, add, update, delete, move, copy, groups, the catalog
of actions and conditions, the values a parameter accepts, search, rename
with references, bulk value edits, batches as one undo step, clone a
player's set, compare two sets, JSON dump and load, .scb import and export
with optional waypoints, areas and units.

Teams: typed properties checked against what the game reads, list, get,
set, copy, bulk changes, summary, verification.

Checks and views: the script editor's reference checks, missing
references grouped by cause with suggestions, dead logic, build order,
trace of what a team or script sets in motion, scripts and teams per
difficulty, object type stats, player editing and build lists.

The Python server adds script templates and a Markdown report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Exposes data that tools need to describe the game data and that had no
public reader: the units and sciences of a production prerequisite, the
build time of things and upgrades, the armor sets of a thing, the name,
speed and surfaces of a locomotor, the shot delay of a weapon, the name
of an armor template and the prerequisites of a science.

Read only; no data layout changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lets an AI plan and write the scripts and teams of a faction from what
the game data says the faction can do.

Bridge:
- faction.describe: tech tree and unit stats of a faction from the
  loaded templates (what builds what, prerequisites, cost, health,
  armor, weapons, movement, upgrades, sciences, special powers).
- app.info: where WorldBuilder runs, so that files can be kept with the
  game data it loaded.

MCP server:
- faction_describe: the raw description.
- faction_generate / faction_get: a planning description per faction in
  worldbuilder_factions next to the executable. The generated facts get
  tiers, build paths, combat numbers and guessed roles; an overrides
  file holds the hand tuning and is written only once.
- faction_check: checks a player's teams and build scripts against the
  description.
- ai_framework_generate / ai_framework_get: finds the skeleton the AI
  players share by comparing them (identical scripts, scripts differing
  in parameters, shared flags and timers, team roster and hooks, object
  lists, names expected of a map).
- faction_map / ai_scaffold: derives a new player's AI from an existing
  one, keeping the shared scripts and translating the rest to the
  counterparts in the target faction.

Adds PyYAML for the overrides files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nces

A SideInfo skill set in AIData.ini kept at most 20 sciences; entries
after that were dropped without notice in release builds, so the AI
never bought them. The limit is now the number of sciences a player can
purchase, taken from the slot counts of the science purchase screens.

Those counts move from ControlBar.h to a header of their own so that
game logic can use them without including the user interface.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Build scripts only say what the skirmish AI builds; where it goes comes
from the SkirmishBuildList of the side in AIData.ini, and a structure
without an entry there is never built. The MCP could not see that file.

Bridge:
- game.read_file: text of a game data file under Data, read through
  the engine's file system (folders and archives).
- buildlist.set: replaces a player's build list in one undo step.

MCP server:
- aidata_get: global AI settings and, per side, gatherer counts, base
  defense structure, science skill sets and the base layout.
- buildlist_from_aidata: loads a side's layout into a map player's
  build list to view and edit.
- buildlist_export: writes a build list as a SkirmishBuildList block,
  optionally into an AIData.ini with a backup.
- faction_generate stores the side's AIData with the description;
  faction_check reports build scripts for structures without a place
  in the layout, layouts without a command center, entries the faction
  cannot build, and foreign base defenses and skill set sciences.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The option lives in the global data, which is destroyed before the
last asserts of a shutdown are raised (the memory leak report among
them). Those then showed a dialog although asserts were to be ignored,
which also blocks a game that a tool has started. The setting is now
remembered once it has been seen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…line

Lets tools find out whether AI scripts work by having the game play
them: -aiMatch <map> -aiPlayers Faction:difficulty[:team],... starts a
skirmish with the local player as observer and no frame rate limit,
writes what happens to -aiMatchLog as JSON lines and quits when one
side is left or -aiMatchMinutes of game time have passed. -seed makes a
match repeatable.

The log holds the players, a snapshot every -aiMatchInterval seconds
(money, power, rank, score, objects by type, teams), events for teams
started, sciences and upgrades gained, the first run of every script
and everything the script engine and the AI report, each with the
player it belongs to, and at the end what was built and lost by type
and how often every script ran.

Such a game runs as an instance of its own, so that it can be started
while WorldBuilder or another game is open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ai_match_run starts the game's -aiMatch mode and ai_match_report
  reads its log: per player the build timeline, what the faction never
  built, teams started, sciences and upgrades bought, scripts and teams
  that never ran, and what its scripts and AI complained about.
- aidata_set_side changes a side's SideInfo in AIData.ini (gatherers,
  base defense, science skill sets) line by line, so that comments and
  untouched lines stay as they are. It shows a diff and writes only
  when given the file to change, keeping a backup.
- ai_scaffold proposes the SideInfo of the new side, translated from
  the source side.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An AI match played the installed AI only, so a change could not be
tried without overwriting Data\Scripts\SkirmishScripts.scb or
AIData.ini.

-aiMatchScripts <file> makes the skirmish players take their scripts
and teams from that file. -aiMatchAIData <file> is read on top of the
game's AIData.ini: its settings, side infos and build lists replace
those of the same name. The log's setup record names both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ai_match_run takes scripts="open" to play with the scripts and teams
open in WorldBuilder (exported next to the match log), scripts=<path>
for any script file, and ai_data=<path> for a changed AIData.ini. With
the same seed, a match before and after a change shows what the change
did, without installing anything. The report names the files used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An AI match needed a window and took the screen while it ran. With
-headless only the game logic is updated, the way replays are
simulated: the new game message goes to the logic's command list
directly and no window, renderer, audio or input is set up. A match
plays out exactly as it does in a window and takes about a third of
the time.

The loop stops with an error in the log if the game does not get into
the match or leaves it by itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ai_match_run starts the game with -headless, so that a match no longer
opens a window; headless=false shows it in one, to watch the AI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ted ones

An extended object that stated prerequisites got them in addition to
those of the object it extends, as the list was only cleared for map
overrides. A shipyard extended from another general's shipyard then
needed that general's supply center as well as its own, which no
player can have; it was only buildable through the TEST_MODE
alternative in the data.

The first Prerequisites block of an extended object now clears the
inherited list, as weapon and armor sets already do. An extended
object without such a block keeps the prerequisites it inherits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A map is meant for ships when its map.ini sets EnableShips; scripts
test that with SKIRMISH_SHIPS_ENABLED and build shipyards with
SKIRMISH_BUILD_SHIPYARD at the map's Shipyard<player>_<n> waypoints.

- map.info shows ships_enabled and map.set_info writes EnableShips to
  the map.ini next to the map, keeping the rest of the file.
- ai.skirmish_check reports per player the shipyard waypoints, the
  naval perimeter and the Naval and NavalFlank paths, and lists what a
  map with ships lacks.
- faction_check knows SKIRMISH_BUILD_SHIPYARD, rejects shipyards built
  with the wrong action and points out shipyard scripts and teams of
  ships that do not ask SKIRMISH_SHIPS_ENABLED.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iptions

Ships were treated as vehicles: they landed in land slots, had land
roles and nothing said what can fight them.

- Ships are recognised by KindOf SHIP or SUBMARINE, by moving on water
  only or by being built in a shipyard, and marked naval, submarine or
  amphibious. The bridge reports the surfaces of all locomotors of a
  unit, so that amphibious ones are found.
- They get roles and slots of their own (shipyard, naval_battle,
  naval_anti_air, naval_siege, submarine, naval_transport,
  naval_support) and are kept out of the land slots.
- Damage per second is also given against ship and submarine armor,
  for every unit.
- faction_get has a naval view: shipyard, ships, naval slots,
  amphibious types, what hurts ships most, what a map needs for ships
  and notes on the data.

Weapons that disable, convert or heal no longer count as damage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md was ignored as user-specific; it now holds the guidance for
working in this fork and is tracked.

The new section describes how to run the WorldBuilder MCP bridge, the
rules that hold for every edit, the tools for map editing and for
script and team editing, the order of the tools for writing and
testing AI, and where to change the MCP itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

⚠️ Title/Commit Validation Failed

Invalid commit messages:

type: Description
type(scope): Description

Allowed types: bugfix, build, chore, ci, docs, fix, feat, perf, refactor, revert, style, test, tweak, unify

See CONTRIBUTING.md for details.

@pWn3d1337
pWn3d1337 merged commit 0b0259f into main Oct 17, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant