Skip to content

Repository files navigation

Civ7 FireTuner Terminal

A proper terminal for Civilization 7's debug port, because the built-in one just wasn't cutting it.

Why I Built This

Civ 7 ships with a debug console called FireTuner - lets you run JavaScript commands against the game engine. Cool, right? Except the actual terminal UI is... barebones. No command history, no multiline support, can't even copy text properly. If you're doing any serious poking around, you end up fighting the tool more than the game.

So I built my own.

Terminal interface

The Protocol

Firaxis didn't document this anywhere (that I could find), so I had to reverse engineer it. Fired up Wireshark and watched the traffic:

Wireshark capture of FireTuner protocol

Turns out it's pretty simple - binary messages over TCP on port 4318:

Sending commands:

[4 bytes: length, little-endian]
[4 bytes: type=3, little-endian]
[CMD:65535:{your javascript here}\0]

Receiving responses:

[4 bytes: length, little-endian]
[4 bytes: type=3, little-endian]
[{result}\0]

The CMD:65535: prefix is some kind of command identifier - 65535 might be a player ID or session thing. The message type is always 3 for command/response pairs. Everything's null-terminated.

Features

What you get that FireTuner doesn't give you:

  • Command history - Up/Down arrows, like a real terminal
  • Multiline editing - Write actual code, not one-liners
  • Smart continuation - Detects unclosed brackets/quotes and auto-continues
  • Copy/paste that works - Select text, Ctrl+C to copy
  • Multiple tabs - Different sessions for different experiments
  • Session logging - Everything gets saved to disk
  • Auto-reconnect - Game crashed? Terminal reconnects when you restart
  • Pretty JSON - Responses get formatted so you can actually read them

Installation

Windows

git clone https://github.com/ghost-ng/FiretunerTerminal.git
cd FiretunerTerminal

python -m venv venv
.\venv\Scripts\Activate.ps1

pip install -r requirements.txt

Linux/Mac

git clone https://github.com/ghost-ng/FiretunerTerminal.git
cd FiretunerTerminal

python3 -m venv venv
source venv/bin/activate

pip install -r requirements.txt

Enable FireTuner in Civ 7

Edit your AppOptions.txt file and set EnableTuner to 1:

Windows: %LOCALAPPDATA%\Firaxis Games\Sid Meier's Civilization VII\AppOptions.txt

Find this line and change it:

EnableTuner 1

Restart the game after making this change.

Usage

With FireTuner enabled and Civ 7 running:

# Windows (with venv activated)
python -m civ7_terminal

# Linux/Mac (with venv activated)
python3 -m civ7_terminal

Just type JavaScript and hit Enter:

GameplayMap.getGridWidth()

For multiline stuff, the terminal auto-continues when you have unclosed brackets:

// Find all continents with sample tiles
const continents = new Map();
const w = GameplayMap.getGridWidth();
const h = GameplayMap.getGridHeight();
for (let x = 0; x < w; x += 5) {
  for (let y = 0; y < h; y += 5) {
    const c = GameplayMap.getContinentType(x, y);
    if (c !== -1 && !continents.has(c)) {
      const regionId = GameplayMap.getLandmassRegionId(x, y);
      continents.set(c, {x, y, regionId});
    }
  }
}

// Build result string
let r = "=== CONTINENTS ===\n";
continents.forEach((v, k) => {
  r += `C${k}: tile(${v.x},${v.y}) region=${v.regionId}\n`;
});

r += "\n=== PLAYERS ===\n";
Players.getAliveMajorIds().forEach(id => {
  const p = Players.get(id);
  const cap = p.Cities?.getCapital()?.location;
  const homeReg = cap ? GameplayMap.getLandmassRegionId(cap.x, cap.y) : "?";
  r += `P${id} (reg ${homeReg}): `;
  continents.forEach((v, k) => {
    r += `C${k}=${p.isDistantLands(v) ? "D" : "H"} `;
  });
  r += "\n";
});

r

The last line r returns the built string. Or force a newline with Ctrl+Enter.

Keybindings

Key What it does
Enter Run command (auto-continues if syntax incomplete)
Ctrl+Enter Force newline
Up/Down Command history
Ctrl+C Copy / Cancel
Ctrl+L Clear screen
Ctrl+T New tab
Ctrl+W Close tab
Ctrl+D Exit
Tab Autocomplete API methods/properties

Autocomplete

Tab completion for the Civ7 JavaScript API, extracted directly from the game's source maps. Covers 18 globals, 40 sub-objects, 507 methods, and 232 properties — see the full Type Reference for every method signature and property.

Setup

# Extract types from your game install (auto-detects Steam path)
python -m civ7_terminal.extract_types

# Or specify the path manually
python -m civ7_terminal.extract_types --game-dir "/path/to/Sid Meier's Civilization VII"

This creates a completions.json file. Re-run after game updates to pick up any API changes.

Usage

  • Type a global name and press Tab to complete: GameplayMGameplayMap
  • After a dot, Tab completes methods and properties: GameplayMap.getGrGameplayMap.getGridWidth
  • Press Tab repeatedly to cycle through matches
  • Works with sub-objects too: player.Cities.getplayer.Cities.getCities
  • Typing any key resets the completion cycle

Colors

  • Cyan - Your commands
  • Green - Responses from the game
  • Red - Errors
  • Yellow - Info messages

JavaScript API Library

The Civ 7 debug console exposes a rich JavaScript API. The methods and objects are visible in the game's JS files, but there's no single reference that pulls it all together — so we built one. API_LIBRARY.md is a community-driven quick reference covering GameplayMap, Players, Game, cities, units, diplomacy, and more. Saves you from digging through game files every time you need a method signature.

If you're an AI agent or a human poking around, start there. If you discover something new, open a PR to add it.

Want to discover APIs yourself? The test-harness/ folder has a guide on crawling the game's API (test-harness/api-crawling.md) and verified walkthroughs like querying the pause menu and exiting to the main menu programmatically.

MCP Server (AI Agent Access)

Want to let Claude, Cursor, or other AI agents send commands to Civ 7? There's an MCP server built in.

Running It

# stdio transport (default) - for Claude Desktop, Cursor, Claude Code
python -m civ7_terminal.mcp_server

# Streamable HTTP - for remote access or multiple clients
python -m civ7_terminal.mcp_server --transport streamable-http --http-port 8080

It runs as a separate process from the terminal UI - both can be open at the same time.

Claude Code / Cursor Setup

Drop a .mcp.json in any project root where you want agents to have access to Civ 7. Two example configs are provided:

  • examples/mcp-venv.json - Use this if you installed dependencies in a virtual environment. Points directly to the venv's Python so packages are found without activating.
  • examples/mcp-global.json - Use this if you installed dependencies globally (system Python). Uses plain python as the command.

Copy whichever fits your setup to .mcp.json in your project root, then update the paths to match where you cloned FiretunerTerminal. On Linux/Mac, change venv/Scripts/python.exe to venv/bin/python.

You can also add it globally via CLI so it's available in all projects:

# With venv
claude mcp add --transport stdio --scope user civ7 -- /path/to/FiretunerTerminal/venv/Scripts/python.exe -m civ7_terminal.mcp_server

# Without venv (if mcp + civ7_terminal are globally installed)
claude mcp add --transport stdio --scope user civ7 -- python -m civ7_terminal.mcp_server

Claude Desktop Setup

Add to your claude_desktop_config.json (same idea - use the venv Python path if you're using a venv):

{
  "mcpServers": {
    "civ7": {
      "command": "/path/to/FiretunerTerminal/venv/Scripts/python.exe",
      "args": ["-m", "civ7_terminal.mcp_server"],
      "env": {
        "PYTHONPATH": "/path/to/FiretunerTerminal"
      }
    }
  }
}

What Agents Get

  • execute_js(code) - Send any JavaScript to the game, get the response back
  • get_game_state(sections?) - Structured JSON snapshot of the live game: overview (turn/age), players (civ, gold, research, counts), cities, units, map dimensions. No JS required.
  • describe_screen() - Semantic summary of what is on screen: game context (turn/date), screen composition, the visible headings/labels a human would read, pressable buttons, dialog text.
  • get_screen() / press_button(caption) - UI automation: see the current screen stack, pressable buttons, and open dialogs, then press buttons by caption using the engine-input pattern (synthetic DOM clicks are ignored by the game). Enough to drive the whole create-game flow.
  • render_map(save_path?, tile_px?) - Draw the map from exact per-tile engine data: terrain/biome colors, territory outlines, cities, and units with a player legend. Ground truth for map-mod testing, vs. interpreting screenshots.
  • reveal_map(scope?) - Reveal the whole map via Visibility.revealAllPlots (the same mechanism as Firaxis's Map tuner panel). Scope: human (default), all, or a player id. Note the gameplay side effects: natural-wonder cinematics fire and all civs are met.
  • list_civs_and_units() - Full roster review: every alive civ with localized leader/civ names, gold, city names, and all units (type, position, damage) plus per-type counts.
  • get_continents() - Every continent on the map: type, localized name, tile count, cities on it and which players are present there.
  • get_players() - Major-player roster: civ, leader, team, gold, government, capital, city/unit counts, and diplomacy (met / at war with).
  • get_citystates() - Independent powers & city-states: localized name, village plot, unit count, suzerain-bonus state, and hostile/friendly relationship toward each major.
  • get_age() - Current age (type, localized name, chronology index), turn/date, and age progression points.
  • get_map_resources(include_locations?) - Whole-map resource scan: counts per resource type with localized names and class, optionally with every tile location.
  • get_player_resources(player_id?) - A player's resources (or all majors'): type, name, class, source plot, assignment info, imports.
  • get_continent_size() - Continent size metrics: tiles, % of land/map, bounding boxes, plus map land/water totals.
  • get_tile(x, y) / get_units_at(x, y) / get_city(x, y) - Plot-level deep inspection: everything about one hex, full detail for its units (moves, damage, promotions, army), and full city detail (populations, growth, happiness, net yields, production, buildings).
  • get_victory_progress() / get_milestones(player_id?) - Victory progress per team and age-progression milestones + legacy-path scores per player — the natural assertion targets for automated runs.
  • get_diplomacy() - Full major-to-major matrix: met, relationship levels, active wars with names.
  • get_tech_civics(player_id) - Both research trees: current node, turns left, completed nodes.
  • get_yields(player_id?) - Net per-turn yields per type plus gold balance.
  • get_city_production(player_id?) - Every city's current production and queue, resolved to type names.
  • get_wonders() / get_religion() / get_trade_routes(player_id?) - Built + natural wonders, religion state (pantheons, beliefs, holy cities), and active trade routes with endpoints and payloads.
  • get_notifications(player_id?) - Pending notifications and end-turn blockers ("why can't the turn end?").
  • search_api(object_expr, pattern) - Live API introspection: find members of any game object by regex.
  • autoplay(action, turns?, observe_as?) / end_turn() - AI fast-forward (the verified FireTuner Autoplay recipe) and single turn advancement. Both mutate game state.
  • look_at(x, y) - Point the camera at a plot before a screenshot.
  • execute_js_file(path) / preflight_mod(module_path) / reload_ui() - The mod dev loop: run a test-suite file from disk, validate a module's import graph via cache-busted dynamic import, and hot-reload the UI without a game restart.
  • list_saves() - List local save files (Steam Cloud saves have no local directory; the tool says so).
  • read_game_logs(name?, tail_lines?, grep?) - Tail or grep the game's own logs (Scripting.log, Database.log, Modding.log...) for failure diagnosis. No args lists available logs.
  • screenshot(save_path?, max_width?) - Capture the game window as a PNG image. This is an OS-level capture (the debug port is text-only) using PrintWindow, so it works even when other windows overlap the game. Pair it with camera calls via execute_js to frame a shot of the map. Windows only for the occlusion-proof path; elsewhere it falls back to a screen grab.
  • help() - Quick-reference summary of every tool and Civ7 API category, for agents orienting themselves.
  • civ7://status - Check connection status to the game, including which transport is active
  • civ7://api-library - The full API_LIBRARY.md JavaScript API reference, served as an MCP resource

The MCP server auto-reconnects to Civ 7, so agents can start before the game is running. When the game suspends the FireTuner port (it does this during multiplayer/hotseat sessions), commands automatically fall back to the Cohtml CDP debugger on port 9444 — same JS context, same results — and switch back once the tuner returns. execute_js prefixes a one-line [transport] notice to its next result whenever the protocol switches. Claude Code and Cursor will start the server automatically when you open a project with the .mcp.json config.

Demo

Live UI Iteration - No Game Restarts

This is where the MCP server really earns its keep: an agent can make live changes to a running game. The debug bridge exposes UI.reloadUI(), which reloads the game's UI documents and re-fetches JS/CSS - so an agent can edit mod files, redeploy, and hot-reload the interface without you ever leaving the game.

Here's a real session. A modded map type's icon wasn't showing up on the Game Setup screen, so the agent probed the running game through the MCP bridge, mapped out exactly what UI.reloadUI() can and can't refresh, then inspected the live DOM to find out why the icon div was never being created:

Agent probing reload hooks and inspecting the live game UI

Then it fixed the selector in the mod's JS, redeployed the files, and reloaded the UI through the bridge - fix verified in-game, zero restarts:

Agent deploying a fix and hot-reloading the UI through the MCP bridge

The edit-redeploy-reload loop turns UI mod iteration from minutes per change (full game restart) into seconds. Database and module-cache changes still need a restart since those are built at boot, but anything UI-side - scripts, CSS, icons, layout - can be iterated on live.

Example: Agent Querying a Live Game

Here's what it looks like when an AI agent uses the MCP server to query a real Civ 7 game. Each call is a single execute_js tool invocation:

Get the map dimensions:

> execute_js('GameplayMap.getGridWidth() + "x" + GameplayMap.getGridHeight()')
84x54

List all players with their cities:

> execute_js(`
let result = [];
Players.getAliveMajorIds().forEach(id => {
  const p = Players.get(id);
  const cities = p.Cities.getCities().map(c => ({
    name: c.name, x: c.location.x, y: c.location.y,
    population: c.population, isCapital: c.isCapital
  }));
  result.push({ id, civ: p.civilizationFullName, cities });
});
JSON.stringify(result, null, 2)
`)
[
  {
    "id": 0, "civ": "LOC_CIVILIZATION_EGYPT_FULLNAME",
    "cities": [{ "name": "LOC_CITY_NAME_EGYPT1", "x": 41, "y": 39, "population": 1, "isCapital": true }]
  },
  {
    "id": 1, "civ": "LOC_CIVILIZATION_MAURYA_FULLNAME",
    "cities": [{ "name": "LOC_CITY_NAME_MAURYA1", "x": 60, "y": 14, "population": 1, "isCapital": true }]
  },
  { "id": 2, "civ": "LOC_CIVILIZATION_MISSISSIPPIAN_FULLNAME", "cities": [] },
  {
    "id": 3, "civ": "LOC_CIVILIZATION_PERSIA_FULLNAME",
    "cities": [{ "name": "LOC_CITY_NAME_PERSIA1", "x": 34, "y": 36, "population": 1, "isCapital": true }]
  },
  { "id": 4, "civ": "LOC_CIVILIZATION_GREECE_FULLNAME", "cities": [] },
  {
    "id": 5, "civ": "LOC_CIVILIZATION_ROME_FULLNAME",
    "cities": [{ "name": "LOC_CITY_NAME_ROME1", "x": 61, "y": 27, "population": 1, "isCapital": true }]
  },
  {
    "id": 6, "civ": "LOC_CIVILIZATION_ASSYRIA_FULLNAME",
    "cities": [{ "name": "LOC_CITY_NAME_ASSYRIA1", "x": 27, "y": 42, "population": 1, "isCapital": true }]
  },
  { "id": 7, "civ": "LOC_CIVILIZATION_TONGA_FULLNAME", "cities": [] }
]

Scan the map for continents:

> execute_js(`
const continents = new Map();
const w = GameplayMap.getGridWidth();
const h = GameplayMap.getGridHeight();
for (let x = 0; x < w; x += 5) {
  for (let y = 0; y < h; y += 5) {
    const c = GameplayMap.getContinentType(x, y);
    if (c !== -1 && !continents.has(c))
      continents.set(c, {x, y});
  }
}
let r = [];
continents.forEach((v, k) => r.push({continent: k, sampleX: v.x, sampleY: v.y}));
JSON.stringify({count: continents.size, continents: r}, null, 2)
`)
{
  "count": 4,
  "continents": [
    { "continent": 36,  "sampleX": 5,  "sampleY": 10 },
    { "continent": 134, "sampleX": 5,  "sampleY": 45 },
    { "continent": 283, "sampleX": 35, "sampleY": 10 },
    { "continent": 273, "sampleX": 55, "sampleY": 20 }
  ]
}

Even simple math works:

> execute_js('1+1')
2

CLI Options

--host, -H     Debug server host (default: 127.0.0.1)
--port, -p     Debug server port (default: 4318)
--session-dir  Where to save session logs (default: ./sessions)

Requirements

  • Python 3.10+
  • Civ 7 running with debug enabled
  • That's it

License

Do whatever you want with it.

About

No description, website, or topics provided.

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages