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.4.0
to
1.4.1
+45
-24
dist/dfhack-queries/mcp_artifacts.lua

@@ -132,34 +132,50 @@ local args = {...}

local function build_artifacts(cursor, limit)
local fort_site_id = df.global.plotinfo.site_id
local function in_scope(ar, scope)
if scope == 'world' then return true end
return sget(ar, 'site') == fort_site_id
end
local function build_artifacts(cursor, limit, scope)
local all = df.global.world.artifacts.all
local total = #all
local world_total = #all
local indices = {}
for i = 0, world_total - 1 do
if in_scope(all[i], scope) then indices[#indices + 1] = i end
end
local total = #indices
local list = {}
local i = cursor
while i < total and #list < limit do
local ok, rec = pcall(artifact_record, all[i])
if ok then
list[#list + 1] = rec
else
list[#list + 1] = { id = (all[i] and all[i].id) or nil, error = 'unreadable artifact record' }
local n = cursor
while n < total and #list < limit do
local ar = all[indices[n + 1]]
local ok, rec = pcall(artifact_record, ar)
if not ok then
rec = { id = (ar and ar.id) or nil, error = 'unreadable artifact record' }
end
i = i + 1
if scope == 'world' then rec.site_local = sget(ar, 'site') == fort_site_id end
list[#list + 1] = rec
n = n + 1
end
local next_cursor
if i < total then next_cursor = tostring(i) end
return list, total, next_cursor
if n < total then next_cursor = tostring(n) end
return list, total, world_total, next_cursor
end
local chunk_by_id = {}
local art_images_loaded = false
do
local ok = pcall(function()
local chunks = df.global.world.art_image_chunks.all
for i = 0, #chunks - 1 do chunk_by_id[chunks[i].id] = chunks[i] end
end)
art_images_loaded = ok and next(chunk_by_id) ~= nil
end
local function resolve_subject(art_id, art_subid)
local chunk = chunk_by_id[art_id]
if not chunk then return nil end
local ok, subj = pcall(function()
local chunks = df.global.world.art_image_chunks.all
for i = 0, #chunks - 1 do
if chunks[i].id == art_id then
local imgs = chunks[i].images
if art_subid >= 0 and art_subid < #imgs then
local nm = tname(imgs[art_subid].name, true)
if nm then return nm end
end
return nil
end
end
local imgs = chunk.images
if art_subid >= 0 and art_subid < #imgs then return tname(imgs[art_subid].name, true) end
return nil

@@ -239,2 +255,3 @@ end)

distinct_subjects = distinct,
art_images_loaded = art_images_loaded,
subjects_resolvable = any_resolved,

@@ -257,7 +274,11 @@ quality = quality,

local artifacts, total, next_cursor = build_artifacts(cursor, limit)
local scope = (args[3] == 'world') and 'world' or 'site'
local artifacts, total, world_total, next_cursor = build_artifacts(cursor, limit, scope)
emit({
scope = scope,
artifacts = artifacts,
artifact_count = total,
artifact_count_world = world_total,
returned = #artifacts,

@@ -264,0 +285,0 @@ cursor = cursor,

@@ -10,11 +10,9 @@ local json = require('json')

local terrain = reqscript('mcp_readTerrain')
local visibility = reqscript('mcp_unitVisibility')
local function footing(p)
local blk = dfhack.maps.getTileBlock(p.x, p.y, p.z)
if not blk then return { discovered = false } end
local des = blk.designation[p.x % 16][p.y % 16]
if des.hidden then return { discovered = false } end
local ch = terrain.sym(blk.tiletype[p.x % 16][p.y % 16], false)
return { discovered = true, symbol = ch, terrain = terrain.TERRAIN_LEGEND[ch],
open_to_sky = des.outside }
return { symbol = ch, terrain = terrain.TERRAIN_LEGEND[ch], open_to_sky = des.outside }
end

@@ -90,3 +88,4 @@

and dfhack.units.isDanger(u) and not dfhack.units.isCitizen(u)
and not (u.flags1.caged or u.flags1.chained) then
and not (u.flags1.caged or u.flags1.chained)
and not visibility.is_hidden(u) then
local p = u.pos

@@ -107,7 +106,8 @@ local token = nil

for _, br in ipairs(bridges) do
local d = cheb(p.x, p.y, br.x, br.y)
local d = math.max(cheb(p.x, p.y, br.x, br.y), math.abs(br.z - p.z))
if not best or d < best then best = d; bi = br end
end
if bi then
th.nearest_bridge = { x = bi.x, y = bi.y, z = bi.z, dist = best,
th.nearest_bridge = { x = bi.x, y = bi.y, z = bi.z,
dist = cheb(p.x, p.y, bi.x, bi.y),
dz = bi.z - p.z, dir = bearing(p.x, p.y, bi.x, bi.y) }

@@ -114,0 +114,0 @@ end

@@ -14,2 +14,3 @@ local json = require('json')

local WATER_LAYERS_CAP = 200
local ICE_LAYERS_CAP = 200
local FLOOD_RISK_CAP = 50

@@ -62,4 +63,13 @@ local WELLS_CAP = 20

local ICE_WALKABLE = {}
for tt in ipairs(df.tiletype) do
local a = df.tiletype.attrs[tt]
if a.material == df.tiletype_material.FROZEN_LIQUID then
ICE_WALKABLE[tt] = df.tiletype_shape.attrs[a.shape].walkable and true or false
end
end
local aquifer_by_z = {}
local water_by_z = {}
local ice_by_z = {}
local magma_by_z = {}

@@ -95,2 +105,10 @@

if not terrain.is_hidden(x, y, z, blk) then
local walkable_ice = ICE_WALKABLE[blk.tiletype[lx][ly]]
if walkable_ice ~= nil then
local ib = ice_by_z[z]
if not ib then ib = { tiles = 0, walkable = 0, solid = 0 }; ice_by_z[z] = ib end
ib.tiles = ib.tiles + 1
if walkable_ice then ib.walkable = ib.walkable + 1
else ib.solid = ib.solid + 1 end
end
if des.water_table then

@@ -215,2 +233,21 @@ local occ = blk.occupancy[lx][ly]

local ice_zs = {}
for z in pairs(ice_by_z) do ice_zs[#ice_zs + 1] = z end
table.sort(ice_zs, function(a, b) return a > b end)
local ice_layers = {}
for _, z in ipairs(ice_zs) do
local ib = ice_by_z[z]
ice_layers[#ice_layers + 1] =
{ z = z, tiles = ib.tiles, walkable_tiles = ib.walkable, solid_tiles = ib.solid }
end
local ice_layers_total = #ice_layers
local ice_layers_truncated = false
if #ice_layers > ICE_LAYERS_CAP then
local capped = {}
for i = 1, ICE_LAYERS_CAP do capped[i] = ice_layers[i] end
ice_layers = capped
ice_layers_truncated = true
end
local magma_sea = nil

@@ -272,2 +309,5 @@ do

local footing_glyphs = {}
for _, t in ipairs(flood_risk) do footing_glyphs[t.footing] = true end
emit({

@@ -280,2 +320,5 @@ aquifer_layers = aquifer_layers,

water_layers_truncated = water_layers_truncated,
ice_layers = ice_layers,
ice_layers_total = ice_layers_total,
ice_layers_truncated = ice_layers_truncated,
magma_sea = magma_sea,

@@ -288,4 +331,4 @@ flood_risk_tiles = flood_risk,

wells_truncated = wells_truncated,
legend = terrain.TERRAIN_LEGEND,
legend = next(footing_glyphs) and terrain.legend_for(footing_glyphs) or nil,
scan = { complete = scan_complete, tiles_scanned = scanned_tiles, last_z_scanned = last_z_scanned },
})

@@ -53,2 +53,60 @@ local json = require('json')

local DEATH_WINDOW_YEARS = 1
local DEATH_SCAN_CAP = 5000
local SITE_ID = df.global.plotinfo.site_id
local GROUP_ID = df.global.plotinfo.group_id
local function is_ours(hf)
if not hf then return false end
for _, link in ipairs(hf.entity_links) do
if link.entity_id == GROUP_ID
and tostring(df.histfig_entity_link_type[link:getType()]) == 'MEMBER' then
return true
end
end
return false
end
local since_year = df.global.cur_year - DEATH_WINDOW_YEARS
local citizen_deaths, slain_by_citizen, outsider_deaths_at_site = 0, 0, 0
local death_causes = {}
pcall(function()
local ed = df.global.world.history.events_death
local scanned = 0
for i = #ed - 1, 0, -1 do
scanned = scanned + 1
if scanned > DEATH_SCAN_CAP then break end
local e = ed[i]
if e.year < since_year then break end
if e.site == SITE_ID then
local victim = df.historical_figure.find(e.victim_hf)
if is_ours(victim) then
citizen_deaths = citizen_deaths + 1
local cause = tostring(df.death_type[e.death_cause])
death_causes[cause] = (death_causes[cause] or 0) + 1
local slayer = (e.slayer_hf ~= -1) and df.historical_figure.find(e.slayer_hf) or nil
if is_ours(slayer) then slain_by_citizen = slain_by_citizen + 1 end
else
outsider_deaths_at_site = outsider_deaths_at_site + 1
end
end
end
end)
local cause_rows = {}
for cause, n in pairs(death_causes) do cause_rows[#cause_rows + 1] = { cause = cause, count = n } end
table.sort(cause_rows, function(a, b)
if a.count ~= b.count then return a.count > b.count end
return a.cause < b.cause
end)
local recent_deaths = {
since_year = since_year,
citizens = citizen_deaths,
citizens_slain_by_citizen = slain_by_citizen,
outsiders_at_site = outsider_deaths_at_site,
citizen_causes = cause_rows,
}
local UNHAPPY_FRACTION_ALERT = 0.10

@@ -66,2 +124,14 @@ local UNHAPPY_MIN_ALERT = 3

end
if citizen_deaths > 0 then
local parts = {}
for _, r in ipairs(cause_rows) do
parts[#parts + 1] = r.count .. ' ' .. string.lower(r.cause):gsub('_', ' ')
end
alerts[#alerts+1] = citizen_deaths .. ' citizen death' .. (citizen_deaths == 1 and '' or 's') ..
' since year ' .. since_year .. ' (' .. table.concat(parts, ', ') .. ')'
end
if slain_by_citizen > 0 then
alerts[#alerts+1] = slain_by_citizen .. ' of those citizen' ..
(slain_by_citizen == 1 and ' was' or 's were') .. ' slain by another citizen'
end

@@ -75,3 +145,4 @@ emit({

happiness = hap,
recent_deaths = recent_deaths,
alerts = alerts,
})

@@ -288,3 +288,3 @@ -- mcp_geology: see docs/tools/geology.md for the data model and field paths.

local out = {
surface_z = surface_z,
surface_z_max = surface_z,
layers = layers,

@@ -291,0 +291,0 @@ aquifer = aquifer,

@@ -21,9 +21,56 @@ local json = require('json')

local DESCRIPTIVE_PART_FLAGS = { edged_damage = true, diagnosed = true }
local function part_is_active(pt)
if pt.bleeding > 0 or pt.pain > 0 or pt.swelling > 0 or pt.strain > 0 then return true end
for name, set in pairs(pt.flags1) do
name = tostring(name)
if set == true and not DESCRIPTIVE_PART_FLAGS[name] and name:sub(1, 5) ~= 'scar_' then
return true
end
end
return pt.flags2.needs_setting == true
end
local function wound_is_active(w)
if w.flags.severed_part or w.flags.infection or w.flags.stuck_weapon or w.flags.popped_out then
return true
end
if w.pain > 0 or w.nausea > 0 or w.dizziness > 0 or w.paralysis > 0
or w.numbness > 0 or w.fever > 0 then
return true
end
for _, pt in ipairs(w.parts) do
if part_is_active(pt) then return true end
end
return false
end
local function wound_state(u)
local ws = u.body and u.body.wounds
if not ws or #ws == 0 then return 'none' end
for _, w in ipairs(ws) do
local ok, active = pcall(wound_is_active, w)
if not ok or active then return 'active' end
end
return 'resolved'
end
local function is_asleep(u)
local job = u.job and u.job.current_job
return job ~= nil and job.job_type == df.job_type.Sleep
end
local citizens = dfhack.units.getCitizens(true)
local wounded, patients, bedridden, unconscious = 0, 0, 0, 0
local old_wounds_only, asleep = 0, 0
local care = {}
for _, u in ipairs(citizens) do
if u.body and u.body.wounds and #u.body.wounds > 0 then wounded = wounded + 1 end
if u.counters and u.counters.unconscious and u.counters.unconscious > 0 then
local state = wound_state(u)
if state == 'active' then wounded = wounded + 1
elseif state == 'resolved' then old_wounds_only = old_wounds_only + 1 end
local sleeping = is_asleep(u)
if sleeping then asleep = asleep + 1 end
if u.counters and u.counters.unconscious and u.counters.unconscious > 0 and not sleeping then
unconscious = unconscious + 1

@@ -61,7 +108,9 @@ end

wounded = wounded,
old_wounds_only = old_wounds_only,
patients = patients,
bedridden = bedridden,
unconscious = unconscious,
asleep = asleep,
care_needs = care_needs,
alerts = alerts,
})

@@ -202,14 +202,20 @@ local json = require('json')

local bo = df.global.world.buildings.other
local function restraint_free(r)
if r.assigned ~= nil or r.chained ~= nil then return false end
return true
local function is_constructed(b)
local ok, done = pcall(function() return b:getBuildStage() >= b:getMaxBuildStage() end)
return not ok or done
end
local restraints_built, restraints_free = 0, 0
for _, r in ipairs(bo.CHAIN or {}) do
restraints_built = restraints_built + 1
if restraint_free(r) then restraints_free = restraints_free + 1 end
local function is_assigned(b)
local ok, assigned = pcall(function() return b:isAssigned() end)
return not ok or assigned
end
for _, r in ipairs(bo.CAGE or {}) do
restraints_built = restraints_built + 1
if restraint_free(r) then restraints_free = restraints_free + 1 end
local restraints_built, restraints_free, restraints_unbuilt = 0, 0, 0
for _, vec in ipairs({ bo.CHAIN or {}, bo.CAGE or {} }) do
for _, r in ipairs(vec) do
if is_constructed(r) then
restraints_built = restraints_built + 1
if not is_assigned(r) then restraints_free = restraints_free + 1 end
else
restraints_unbuilt = restraints_unbuilt + 1
end
end
end

@@ -226,2 +232,3 @@

restraints_free = restraints_free,
restraints_unbuilt = restraints_unbuilt,
}

@@ -228,0 +235,0 @@

@@ -141,3 +141,3 @@ local json = require('json')

if core and surface_z == nil then
alerts[#alerts + 1] = 'fort core column is not open to sky (surface_z unknown)'
alerts[#alerts + 1] = 'fort core column is not open to sky (surface_z_at_core unknown)'
end

@@ -148,3 +148,3 @@

fort_core = core,
surface_z = surface_z,
surface_z_at_core = surface_z,
activity = {

@@ -151,0 +151,0 @@ z_levels = activity_z,

@@ -9,2 +9,4 @@ local json = require('json')

local visibility = reqscript('mcp_unitVisibility')
local WORN_MODES = {

@@ -190,3 +192,4 @@ [df.inv_item_role_type.Worn] = true,

and dfhack.units.isDanger(u) and not dfhack.units.isCitizen(u)
and not (u.flags1.caged or u.flags1.chained) then
and not (u.flags1.caged or u.flags1.chained)
and not visibility.is_hidden(u) then
hostiles = hostiles + 1

@@ -193,0 +196,0 @@ if dfhack.units.isGreatDanger(u) then great_danger = great_danger + 1 end

@@ -21,2 +21,8 @@ --@ module = true

function legend_for(glyphs)
local out = {}
for ch in pairs(glyphs) do out[ch] = TERRAIN_LEGEND[ch] or 'unknown glyph' end
return out
end
local SHAPE = df.tiletype_shape

@@ -97,3 +103,3 @@

fortifications = fortifications,
legend = TERRAIN_LEGEND, distinct = distinct, grid = rows,
legend = legend_for(distinct), distinct = distinct, grid = rows,
}

@@ -100,0 +106,0 @@ end

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

@@ -5,0 +5,0 @@ "mcpName": "io.github.alexanderolvera/dfhack-mcp",

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

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