🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

dfhack-mcp

Package Overview
Dependencies
Maintainers
1
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

dfhack-mcp - npm Package Compare versions

Comparing version
1.0.0
to
1.0.1
+130
dist/dfhack-queries/mcp_gameSave.lua
-- mcp_gameSave: A4 actuator — checkpoint the fort with a quicksave. Backs one tool:
-- game_save (gated actuator; subcommands "plan" preview / "apply" trigger)
--
-- EXECUTE, NEVER DECIDE: the caller asks to save; this script triggers a save and
-- reports facts — no "you should save now" logic. The §A0 dry-run/confirm loop lives
-- in TS (src/actuator.ts); this script answers plan (preview + a CONSTANT signature)
-- and apply (trigger + readback).
--
-- Departures from the other actuators, surfaced as facts (all verified live on
-- 53.15 against a Dreamfort container):
-- * The save is ASYNCHRONOUS. `quicksave` requests DF's autosave; DF writes the
-- save over the NEXT FEW FRAMES. apply() can confirm the quicksave command was
-- DISPATCHED (command_result), NOT that the file has landed — that's async.
-- * It routes through DF's AUTOSAVE: the write lands in a rotating "autosave N"
-- folder governed by the player's DF autosave settings — it does NOT overwrite
-- the loaded region save. So we report the game DATE being frozen (reliable),
-- never an authoritative destination folder (cur_savegame.save_dir lags a save
-- behind and is config-dependent — reporting it would mislead).
-- * IRREVERSIBLE: once written, a save can't be un-written from here; roll back by
-- loading the appropriate save/autosave in DF.
-- We DELEGATE to the stock, maintained `quicksave` script (via run_command_silent, so
-- its print stays out of our JSON stdout) instead of poking the version-fragile
-- save_progress.* fields ourselves — a field rename is then DFHack's problem, not ours.
-- (Stock quicksave defers the actual autosave_request set to a next-frame overlay
-- render, so reading that flag synchronously here would always see false — we don't.)
--
-- Invoked by name via DFHack RunCommand with a subcommand as arg 1; prints ONE JSON.
local json = require('json')
local function emit(t) print(json.encode(t)) end
-- quicksave itself only runs in fortress mode with a loaded map; mirror the codebase
-- no-fort guard so game_save honors the same contract as every other tool.
if df.global.gamemode ~= df.game_mode.DWARF then
emit({ error = 'no fort loaded' })
return
end
local a = { ... }
local sub = a[1]
-- ---- shared facts: what a save would freeze --------------------------------
local months = {'Granite','Slate','Felsite','Hematite','Malachite','Galena',
'Limestone','Sandstone','Timber','Moonstone','Opal','Obsidian'}
local seasons = {'Spring','Summer','Autumn','Winter'}
local function game_date()
local tick = df.global.cur_year_tick
local midx = math.floor(tick / 33600)
local day = math.floor((tick % 33600) / 1200) + 1
return {
year = df.global.cur_year,
year_tick = tick,
month = months[midx + 1],
season = seasons[math.floor(midx / 3) + 1],
day = day,
}
end
-- fort_name + game_date identify WHAT is being frozen — the reliable facts. We do
-- NOT report a destination folder: DF's autosave picks a rotating "autosave N" dir
-- per the player's settings, and cur_savegame.save_dir lags a save behind, so any
-- folder we named would mislead. fort_name is pcall'd (the JSON encoder can't emit
-- null, so an unavailable field is simply omitted).
local function save_facts()
local ok_name, fname = pcall(function()
return dfhack.translation.translateName(df.global.world.world_data.active_site[0].name, true)
end)
return {
fort_name = (ok_name and fname ~= '') and fname or nil,
method = 'quicksave',
game_date = game_date(),
}
end
-- ============================ plan ============================
if sub == 'plan' then
local preview = save_facts()
-- Facts the agent needs to understand what confirming does.
preview.reversible = false
preview.effect = 'triggers DFHack quicksave; DF writes a save asynchronously (over the next few frames) '
.. 'via its autosave — the destination follows your DF autosave settings, typically a rotating "autosave" '
.. 'folder rather than an overwrite of the loaded save'
emit({
preview = preview,
-- CONSTANT signature: a save always freezes the CURRENT state, whatever it is, so
-- no sub-target exists whose drift should void the confirm token (contrast the
-- work-order / blueprint signatures, which sign their specific target). Single-use
-- is the only guard that applies here.
signature = 'game_save',
})
return
end
-- ============================ apply ============================
if sub == 'apply' then
local facts = save_facts()
-- Delegate to the stock quicksave script. run_command_silent returns (output,
-- command_result); it keeps quicksave's 'The game should autosave now.' print out
-- of our stdout. quicksave requests DF's autosave; DF performs the write on later
-- frames (verified live: the save lands in a rotating "autosave N" folder).
local out, rc = dfhack.run_command_silent('quicksave')
if rc ~= 0 then
emit({ error = 'quicksave failed (command_result ' .. tostring(rc) .. '): ' .. tostring(out) })
return
end
emit({
changes = {
save_requested = true,
method = 'quicksave',
game_date = facts.game_date,
},
undo = {
reversible = false,
note = 'no undo — once written, the save persists. To roll back, load the appropriate save/autosave from before this call in DF',
},
-- Readback confirms the quicksave command was DISPATCHED (command_result 0 =
-- CR_OK). It does NOT and CANNOT confirm the file finished writing — DF commits
-- the save asynchronously over the next few frames.
readback = {
dispatched = rc == 0,
command_result = rc,
write = 'asynchronous — DF commits the save over the next few frames via its autosave; this call cannot confirm the file has landed',
game_date = facts.game_date,
},
})
return
end
emit({ error = 'unknown subcommand: ' .. tostring(sub) })
--@ module = true
-- mcp_unitVisibility: the fog-of-war gate for UNIT enumeration.
--
-- Companion to mcp_readTerrain (which gates TERRAIN reads). Every terrain tool
-- honors designation.hidden; unit-listing tools must too, or the fort's fog of
-- war leaks through the back door -- an undiscovered cavern's wraiths are
-- reported as "on the map" even though no dwarf has ever seen them. That is an
-- X-ray: the agent learns the existence and count of hostiles the player
-- cannot see, breaking the facts-only doctrine as much as leaking real terrain
-- would.
--
-- is_hidden(u) is the SINGLE source of truth for "has the fort discovered the
-- tile this unit stands on". Any current or future sensor that enumerates
-- units (threats, fort_status, and any wildlife/animal-economy tool to come)
-- must reqscript this module and filter through it, rather than re-deriving
-- the designation.hidden check inline -- that duplication is exactly how the
-- original leak happened (isDanger/isCitizen predicate copy-pasted into two
-- files with no visibility gate at all).
--
-- A caged/chained beast is gated the same as a loose one: if its tile has
-- never been uncovered, the fort has not actually seen it (e.g. a forgotten
-- beast trapped in an undiscovered cavern), so it stays hidden. Off-map/
-- unloaded blocks are treated as unseen (fail closed, never leak).
-- Returns true when the unit's current tile is undiscovered (designation.hidden)
-- or off-map/unloaded. Mirrors mcp_readTerrain.read_window's per-tile check.
function is_hidden(u)
local p = u.pos
local blk = dfhack.maps.getTileBlock(p.x, p.y, p.z)
if not blk then return true end -- off-map / unloaded => treat as unseen
return blk.designation[p.x % 16][p.y % 16].hidden
end
-- When loaded via reqscript, stop here: the caller just wanted the functions.
if dfhack_flags and dfhack_flags.module then
return
end
+6
-1

@@ -13,2 +13,6 @@ -- mcp_fortStatus: one-call situational overview of the loaded fort — name, date,

-- Fog-of-war gate: mirrors mcp_threats — a hostile the fort hasn't discovered
-- must never contribute to the count/alert here either.
local visibility = reqscript('mcp_unitVisibility')
local months = {'Granite','Slate','Felsite','Hematite','Malachite','Galena',

@@ -47,3 +51,4 @@ 'Limestone','Sandstone','Timber','Moonstone','Opal','Obsidian'}

if dfhack.units.isActive(u) and not dfhack.units.isDead(u)
and dfhack.units.isDanger(u) and not dfhack.units.isCitizen(u) then
and dfhack.units.isDanger(u) and not dfhack.units.isCitizen(u)
and not visibility.is_hidden(u) then
hostiles = hostiles + 1

@@ -50,0 +55,0 @@ end

@@ -7,2 +7,5 @@ -- mcp_threats: enumerate dangerous units on the map, grouped by kind.

-- a live attack). Groups identical creatures so "12 goblins" reads as one line.
-- FOG OF WAR: a unit on an undiscovered tile is filtered via mcp_unitVisibility
-- before it ever reaches a group/count/alert — the player must have actually
-- found it (see issue: "threats & fort_status expose undiscovered hostiles").
-- Invoked by name via DFHack RunCommand; prints ONE JSON object.

@@ -18,2 +21,6 @@

-- Fog-of-war gate: a unit standing on an undiscovered tile must never surface
-- here, loose OR caged/chained -- see mcp_unitVisibility for the rationale.
local visibility = reqscript('mcp_unitVisibility')
-- Group dangerous units by a stable key so identical creatures collapse to one

@@ -101,3 +108,4 @@ -- line. Contained (caged/chained) threats are counted apart from active ones.

if dfhack.units.isActive(u) and not dfhack.units.isDead(u)
and dfhack.units.isDanger(u) and not dfhack.units.isCitizen(u) then
and dfhack.units.isDanger(u) and not dfhack.units.isCitizen(u)
and not visibility.is_hidden(u) then
local contained = u.flags1.caged or u.flags1.chained

@@ -104,0 +112,0 @@ local name = dfhack.units.getReadableName(u)

+1
-1
{
"name": "dfhack-mcp",
"version": "1.0.0",
"version": "1.0.1",
"description": "MCP server exposing a live Dwarf Fortress fort to an AI agent as curated, semantic tools",

@@ -5,0 +5,0 @@ "type": "module",

@@ -27,2 +27,5 @@ # dfhack-mcp

Every tool has a reference page — parameters, return shape, real example
output, caveats — in [docs/tools](docs/tools/README.md).
## Quick start

@@ -179,2 +182,6 @@

**Saving the game**
- **`game_save()`** — checkpoint the fort with a quicksave before a large or risky change, so a bad batch can be rolled back by loading the save. Takes no arguments; the dry-run previews the fort and game date being frozen. Two facts to know: the write is **asynchronous** (DF commits it over the next few frames — the readback confirms the quicksave _dispatched_, not that the file landed) and it routes through DF's **autosave**, so it lands in a rotating "autosave" folder per your DF settings rather than overwriting the loaded save. Irreversible: to roll back, load the appropriate save/autosave in DF. Fortress mode only.
## Configuration

@@ -181,0 +188,0 @@

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display