From 0ab42627e87b397f4d76d03f6a1be66fa54bfbee Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sat, 19 Jul 2025 19:19:18 +0200 Subject: [PATCH 01/93] Add chronicle script to record fortress events --- chronicle.lua | 128 +++++++++++++++++++++++++++++++++++++++++++++ docs/chronicle.rst | 29 ++++++++++ 2 files changed, 157 insertions(+) create mode 100644 chronicle.lua create mode 100644 docs/chronicle.rst diff --git a/chronicle.lua b/chronicle.lua new file mode 100644 index 000000000..d750dbbbf --- /dev/null +++ b/chronicle.lua @@ -0,0 +1,128 @@ +-- Chronicles fortress events (deaths, artifacts, invasions) +--@module = true +--@enable = true + +local eventful = require('plugins.eventful') +local utils = require('utils') + +local GLOBAL_KEY = 'chronicle' + +local function get_default_state() + return { + entries = {}, + last_artifact_id = -1, + known_invasions = {}, + } +end + +state = state or get_default_state() + +local function persist_state() + dfhack.persistent.saveSiteData(GLOBAL_KEY, state) +end + +local function format_date(year, ticks) + local julian_day = math.floor(ticks / 1200) + 1 + local month = math.floor(julian_day / 28) + 1 + local day = julian_day % 28 + return string.format('%03d-%02d-%02d', year, month, day) +end + +local function add_entry(text) + table.insert(state.entries, text) + persist_state() +end + +local function on_unit_death(unit_id) + local unit = df.unit.find(unit_id) + if not unit then return end + local name = dfhack.units.getReadableName(unit) + local date = format_date(df.global.cur_year, df.global.cur_year_tick) + add_entry(string.format('Death of %s on %s', name, date)) +end + +eventful.onUnitDeath[GLOBAL_KEY] = on_unit_death + +local function check_artifacts() + local last_id = state.last_artifact_id + for _, rec in ipairs(df.global.world.artifacts.all) do + if rec.id > last_id then + local name = dfhack.translation.translateName(rec.name) + local date = format_date(rec.year, rec.year_ticks or 0) + add_entry(string.format('Artifact "%s" created on %s', name, date)) + last_id = rec.id + end + end + state.last_artifact_id = last_id +end + +local function check_invasions() + for _, inv in ipairs(df.global.plotinfo.invasions.list) do + if inv.flags.active and not state.known_invasions[inv.id] then + state.known_invasions[inv.id] = true + local date = format_date(df.global.cur_year, df.global.cur_year_tick) + add_entry(string.format('Invasion started on %s', date)) + end + end +end + +local function event_loop() + if not state.enabled then return end + check_artifacts() + check_invasions() + dfhack.timeout(1200, 'ticks', event_loop) +end + +local function do_enable() + state.enabled = true + event_loop() +end + +local function do_disable() + state.enabled = false +end + +local function load_state() + state = get_default_state() + utils.assign(state, dfhack.persistent.getSiteData(GLOBAL_KEY, state)) +end + +-- State change hook + +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_MAP_UNLOADED then + eventful.onUnitDeath[GLOBAL_KEY] = nil + state.enabled = false + return + end + if sc ~= SC_MAP_LOADED or not dfhack.world.isFortressMode() then + return + end + + load_state() + if state.enabled then + do_enable() + end +end + +if dfhack_flags.module then return end + +local args = {...} +local cmd = args[1] or 'print' + +if cmd == 'enable' then + do_enable() +elseif cmd == 'disable' then + do_disable() +elseif cmd == 'clear' then + state.entries = {} + persist_state() +elseif cmd == 'print' then + for _, entry in ipairs(state.entries) do + print(entry) + end +else + print(dfhack.script_help()) +end + +persist_state() diff --git a/docs/chronicle.rst b/docs/chronicle.rst new file mode 100644 index 000000000..7239a22fe --- /dev/null +++ b/docs/chronicle.rst @@ -0,0 +1,29 @@ +chronicle +========= + +.. dfhack-tool:: + :summary: Record fortress events like deaths, artifacts, and invasions. + :tags: fort gameplay + +This tool automatically records notable events in a chronicle that is stored +with your save. The chronicle contains entries for unit deaths, newly created +artifacts, and the start of invasions. + +Usage +----- + +:: + + chronicle enable + chronicle disable + chronicle print + chronicle clear + +``chronicle enable`` + Start recording events in the current fortress. +``chronicle disable`` + Stop recording events. +``chronicle print`` + Print all recorded events. +``chronicle clear`` + Delete the chronicle. From 2e675db911686c7deb2696f76696308bfdad4b71 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sat, 19 Jul 2025 19:28:42 +0200 Subject: [PATCH 02/93] fix chronicle initialization and loading --- chronicle.lua | 141 +++++++++++++++++++++++++++++++++++++++++++++ docs/chronicle.rst | 29 ++++++++++ 2 files changed, 170 insertions(+) create mode 100644 chronicle.lua create mode 100644 docs/chronicle.rst diff --git a/chronicle.lua b/chronicle.lua new file mode 100644 index 000000000..ee3d8980e --- /dev/null +++ b/chronicle.lua @@ -0,0 +1,141 @@ +-- Chronicles fortress events (deaths, artifacts, invasions) +--@module = true +--@enable = true + +local eventful = require('plugins.eventful') +local utils = require('utils') + +local GLOBAL_KEY = 'chronicle' + +local function get_default_state() + return { + entries = {}, + last_artifact_id = -1, + known_invasions = {}, + } +end + +state = state or get_default_state() + +local function persist_state() + dfhack.persistent.saveSiteData(GLOBAL_KEY, state) +end + +local function format_date(year, ticks) + local julian_day = math.floor(ticks / 1200) + 1 + local month = math.floor(julian_day / 28) + 1 + local day = julian_day % 28 + return string.format('%03d-%02d-%02d', year, month, day) +end + +local function add_entry(text) + table.insert(state.entries, text) + persist_state() +end + +local function on_unit_death(unit_id) + local unit = df.unit.find(unit_id) + if not unit then return end + local name = dfhack.units.getReadableName(unit) + local date = format_date(df.global.cur_year, df.global.cur_year_tick) + add_entry(string.format('Death of %s on %s', name, date)) +end + +eventful.onUnitDeath[GLOBAL_KEY] = on_unit_death + +local function check_artifacts() + local last_id = state.last_artifact_id + for _, rec in ipairs(df.global.world.artifacts.all) do + if rec.id > last_id then + local name = dfhack.translation.translateName(rec.name) + local date = format_date(rec.year, rec.year_ticks or 0) + add_entry(string.format('Artifact "%s" created on %s', name, date)) + last_id = rec.id + end + end + state.last_artifact_id = last_id +end + +local function check_invasions() + for _, inv in ipairs(df.global.plotinfo.invasions.list) do + if inv.flags.active and not state.known_invasions[inv.id] then + state.known_invasions[inv.id] = true + local date = format_date(df.global.cur_year, df.global.cur_year_tick) + add_entry(string.format('Invasion started on %s', date)) + end + end +end + +local function event_loop() + if not state.enabled then return end + check_artifacts() + check_invasions() + dfhack.timeout(1200, 'ticks', event_loop) +end + +local function do_enable() + state.enabled = true + event_loop() +end + +local function do_disable() + state.enabled = false +end + +local function load_state() + state = get_default_state() + utils.assign(state, dfhack.persistent.getSiteData(GLOBAL_KEY, state)) +end + +-- State change hook + +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_MAP_UNLOADED then + eventful.onUnitDeath[GLOBAL_KEY] = nil + state.enabled = false + return + end + if sc ~= SC_MAP_LOADED or not dfhack.world.isFortressMode() then + return + end + + load_state() + if state.enabled then + do_enable() + end +end + +if dfhack.isMapLoaded() and dfhack.world.isFortressMode() then + load_state() + if state.enabled then + do_enable() + end +end + +if dfhack_flags.module then return end + +if not dfhack.world.isFortressMode() or not dfhack.isMapLoaded() then + qerror('chronicle requires a loaded fortress map') +end + +load_state() + +local args = {...} +local cmd = args[1] or 'print' + +if cmd == 'enable' then + do_enable() +elseif cmd == 'disable' then + do_disable() +elseif cmd == 'clear' then + state.entries = {} + persist_state() +elseif cmd == 'print' then + for _, entry in ipairs(state.entries) do + print(entry) + end +else + print(dfhack.script_help()) +end + +persist_state() diff --git a/docs/chronicle.rst b/docs/chronicle.rst new file mode 100644 index 000000000..7239a22fe --- /dev/null +++ b/docs/chronicle.rst @@ -0,0 +1,29 @@ +chronicle +========= + +.. dfhack-tool:: + :summary: Record fortress events like deaths, artifacts, and invasions. + :tags: fort gameplay + +This tool automatically records notable events in a chronicle that is stored +with your save. The chronicle contains entries for unit deaths, newly created +artifacts, and the start of invasions. + +Usage +----- + +:: + + chronicle enable + chronicle disable + chronicle print + chronicle clear + +``chronicle enable`` + Start recording events in the current fortress. +``chronicle disable`` + Stop recording events. +``chronicle print`` + Print all recorded events. +``chronicle clear`` + Delete the chronicle. From 8da32eb333cb1feb85c02a534daab68f4806ab77 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sat, 19 Jul 2025 19:58:26 +0200 Subject: [PATCH 03/93] Fix chronicle artifact log and hook handling --- chronicle.lua | 144 +++++++++++++++++++++++++++++++++++++++++++++ docs/chronicle.rst | 29 +++++++++ 2 files changed, 173 insertions(+) create mode 100644 chronicle.lua create mode 100644 docs/chronicle.rst diff --git a/chronicle.lua b/chronicle.lua new file mode 100644 index 000000000..db0a468ce --- /dev/null +++ b/chronicle.lua @@ -0,0 +1,144 @@ +-- Chronicles fortress events (deaths, artifacts, invasions) +--@module = true +--@enable = true + +local eventful = require('plugins.eventful') +local utils = require('utils') + +local GLOBAL_KEY = 'chronicle' + +local function get_default_state() + return { + entries = {}, + last_artifact_id = -1, + known_invasions = {}, + } +end + +state = state or get_default_state() + +local function persist_state() + dfhack.persistent.saveSiteData(GLOBAL_KEY, state) +end + +local function format_date(year, ticks) + local julian_day = math.floor(ticks / 1200) + 1 + local month = math.floor(julian_day / 28) + 1 + local day = julian_day % 28 + return string.format('%03d-%02d-%02d', year, month, day) +end + +local function add_entry(text) + table.insert(state.entries, text) + persist_state() +end + +local function on_unit_death(unit_id) + local unit = df.unit.find(unit_id) + if not unit then return end + local name = dfhack.units.getReadableName(unit) + local date = format_date(df.global.cur_year, df.global.cur_year_tick) + add_entry(string.format('Death of %s on %s', name, date)) +end + +local function check_artifacts() + local last_id = state.last_artifact_id + for _, rec in ipairs(df.global.world.artifacts.all) do + if rec.id > last_id then + local name = dfhack.translation.translateName(rec.name) + -- artifact_record stores the creation tick in `year_tick` + local date = format_date(rec.year, rec.year_tick or 0) + add_entry(string.format('Artifact "%s" created on %s', name, date)) + last_id = rec.id + end + end + state.last_artifact_id = last_id +end + +local function check_invasions() + for _, inv in ipairs(df.global.plotinfo.invasions.list) do + if inv.flags.active and not state.known_invasions[inv.id] then + state.known_invasions[inv.id] = true + local date = format_date(df.global.cur_year, df.global.cur_year_tick) + add_entry(string.format('Invasion started on %s', date)) + end + end +end + +local function event_loop() + if not state.enabled then return end + check_artifacts() + check_invasions() + dfhack.timeout(1200, 'ticks', event_loop) +end + +local function do_enable() + state.enabled = true + eventful.onUnitDeath[GLOBAL_KEY] = on_unit_death + persist_state() + event_loop() +end + +local function do_disable() + state.enabled = false + eventful.onUnitDeath[GLOBAL_KEY] = nil + persist_state() +end + +local function load_state() + state = get_default_state() + utils.assign(state, dfhack.persistent.getSiteData(GLOBAL_KEY, state)) +end + +-- State change hook + +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_MAP_UNLOADED then + eventful.onUnitDeath[GLOBAL_KEY] = nil + state.enabled = false + return + end + if sc ~= SC_MAP_LOADED or not dfhack.world.isFortressMode() then + return + end + + load_state() + if state.enabled then + do_enable() + end +end + +if dfhack.isMapLoaded() and dfhack.world.isFortressMode() then + load_state() + if state.enabled then + do_enable() + end +end + +if dfhack_flags.module then return end + +if not dfhack.world.isFortressMode() or not dfhack.isMapLoaded() then + qerror('chronicle requires a loaded fortress map') +end + +load_state() + +local args = {...} +local cmd = args[1] or 'print' + +if cmd == 'enable' then + do_enable() +elseif cmd == 'disable' then + do_disable() +elseif cmd == 'clear' then + state.entries = {} + persist_state() +elseif cmd == 'print' then + for _, entry in ipairs(state.entries) do + print(entry) + end +else + print(dfhack.script_help()) +end + +persist_state() diff --git a/docs/chronicle.rst b/docs/chronicle.rst new file mode 100644 index 000000000..7239a22fe --- /dev/null +++ b/docs/chronicle.rst @@ -0,0 +1,29 @@ +chronicle +========= + +.. dfhack-tool:: + :summary: Record fortress events like deaths, artifacts, and invasions. + :tags: fort gameplay + +This tool automatically records notable events in a chronicle that is stored +with your save. The chronicle contains entries for unit deaths, newly created +artifacts, and the start of invasions. + +Usage +----- + +:: + + chronicle enable + chronicle disable + chronicle print + chronicle clear + +``chronicle enable`` + Start recording events in the current fortress. +``chronicle disable`` + Stop recording events. +``chronicle print`` + Print all recorded events. +``chronicle clear`` + Delete the chronicle. From 4ee1a2123adfc938407349524179e7bf11de0dbe Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sat, 19 Jul 2025 20:14:24 +0200 Subject: [PATCH 04/93] fix chronicle artifact timestamp --- chronicle.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chronicle.lua b/chronicle.lua index 40548b68d..0a02840cb 100644 --- a/chronicle.lua +++ b/chronicle.lua @@ -45,8 +45,8 @@ local function check_artifacts() for _, rec in ipairs(df.global.world.artifacts.all) do if rec.id > last_id then local name = dfhack.translation.translateName(rec.name) - -- artifact_record stores the creation tick in `year_tick` - local date = format_date(rec.year, rec.year_tick or 0) + -- artifact_record stores the creation tick in `season_tick` + local date = format_date(rec.year, rec.season_tick or 0) add_entry(string.format('Artifact "%s" created on %s', name, date)) last_id = rec.id end From 5a4b43a85e68b2572462d7fd028abca7ad19de60 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sat, 19 Jul 2025 20:25:03 +0200 Subject: [PATCH 05/93] disable heavy scanning in chronicle --- chronicle.lua | 6 +++--- docs/chronicle.rst | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/chronicle.lua b/chronicle.lua index 40548b68d..c40b4a0b4 100644 --- a/chronicle.lua +++ b/chronicle.lua @@ -1,4 +1,4 @@ --- Chronicles fortress events (deaths, artifacts, invasions) +-- Chronicles fortress events (currently only unit deaths) --@module = true --@enable = true @@ -64,10 +64,10 @@ local function check_invasions() end end +-- main loop; artifact and invasion tracking disabled to avoid scanning large +-- data structures, which was causing hangs on some forts local function event_loop() if not state.enabled then return end - check_artifacts() - check_invasions() dfhack.timeout(1200, 'ticks', event_loop) end diff --git a/docs/chronicle.rst b/docs/chronicle.rst index 7239a22fe..7943ee30a 100644 --- a/docs/chronicle.rst +++ b/docs/chronicle.rst @@ -2,12 +2,12 @@ chronicle ========= .. dfhack-tool:: - :summary: Record fortress events like deaths, artifacts, and invasions. + :summary: Record fortress events like deaths. Artifact and invasion tracking disabled. :tags: fort gameplay This tool automatically records notable events in a chronicle that is stored -with your save. The chronicle contains entries for unit deaths, newly created -artifacts, and the start of invasions. +with your save. Currently only unit deaths are recorded since artifact and +invasion tracking has been disabled due to performance issues. Usage ----- From bcbcc3d1e756d1009cbaab6e830e9f67cdaebd20 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sat, 19 Jul 2025 20:34:50 +0200 Subject: [PATCH 06/93] Track artifacts and invasions --- chronicle.lua | 33 ++++++++++++++++++++++++++++++++- docs/chronicle.rst | 5 ++--- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/chronicle.lua b/chronicle.lua index 446b3fd6c..8f3efccca 100644 --- a/chronicle.lua +++ b/chronicle.lua @@ -1,4 +1,4 @@ --- Chronicles fortress events (currently only unit deaths) +-- Chronicles fortress events: unit deaths, artifact creation, and invasions --@module = true --@enable = true @@ -40,6 +40,29 @@ local function on_unit_death(unit_id) local date = format_date(df.global.cur_year, df.global.cur_year_tick) add_entry(string.format('Death of %s on %s', name, date)) end + +local function on_item_created(item_id) + local item = df.item.find(item_id) + if not item or not item.flags.artifact then return end + + local gref = dfhack.items.getGeneralRef(item, df.general_ref_type.IS_ARTIFACT) + local rec = gref and df.artifact_record.find(gref.artifact_id) or nil + if not rec then return end + + local name = dfhack.translation.translateName(rec.name) + local date = format_date(rec.year, rec.season_tick or 0) + if rec.id > state.last_artifact_id then + state.last_artifact_id = rec.id + end + add_entry(string.format('Artifact "%s" created on %s', name, date)) +end + +local function on_invasion(invasion_id) + if state.known_invasions[invasion_id] then return end + state.known_invasions[invasion_id] = true + local date = format_date(df.global.cur_year, df.global.cur_year_tick) + add_entry(string.format('Invasion started on %s', date)) +end local function check_artifacts() local last_id = state.last_artifact_id for _, rec in ipairs(df.global.world.artifacts.all) do @@ -73,7 +96,11 @@ end local function do_enable() state.enabled = true + eventful.enableEvent(eventful.eventType.ITEM_CREATED, 1) + eventful.enableEvent(eventful.eventType.INVASION, 1) eventful.onUnitDeath[GLOBAL_KEY] = on_unit_death + eventful.onItemCreated[GLOBAL_KEY] = on_item_created + eventful.onInvasion[GLOBAL_KEY] = on_invasion persist_state() event_loop() @@ -82,6 +109,8 @@ end local function do_disable() state.enabled = false eventful.onUnitDeath[GLOBAL_KEY] = nil + eventful.onItemCreated[GLOBAL_KEY] = nil + eventful.onInvasion[GLOBAL_KEY] = nil persist_state() end @@ -95,6 +124,8 @@ end dfhack.onStateChange[GLOBAL_KEY] = function(sc) if sc == SC_MAP_UNLOADED then eventful.onUnitDeath[GLOBAL_KEY] = nil + eventful.onItemCreated[GLOBAL_KEY] = nil + eventful.onInvasion[GLOBAL_KEY] = nil state.enabled = false return end diff --git a/docs/chronicle.rst b/docs/chronicle.rst index 7943ee30a..e573d967b 100644 --- a/docs/chronicle.rst +++ b/docs/chronicle.rst @@ -2,12 +2,11 @@ chronicle ========= .. dfhack-tool:: - :summary: Record fortress events like deaths. Artifact and invasion tracking disabled. + :summary: Record fortress events like deaths, artifacts, and invasions. :tags: fort gameplay This tool automatically records notable events in a chronicle that is stored -with your save. Currently only unit deaths are recorded since artifact and -invasion tracking has been disabled due to performance issues. +with your save. Unit deaths, artifact creation, and invasions are recorded. Usage ----- From c623822944a29856ec5b24e548027ef1e7a0bf30 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sat, 19 Jul 2025 20:39:20 +0200 Subject: [PATCH 07/93] Remove legacy scanning in chronicle --- chronicle.lua | 34 ++-------------------------------- 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/chronicle.lua b/chronicle.lua index 8f3efccca..c247180ed 100644 --- a/chronicle.lua +++ b/chronicle.lua @@ -63,36 +63,8 @@ local function on_invasion(invasion_id) local date = format_date(df.global.cur_year, df.global.cur_year_tick) add_entry(string.format('Invasion started on %s', date)) end -local function check_artifacts() - local last_id = state.last_artifact_id - for _, rec in ipairs(df.global.world.artifacts.all) do - if rec.id > last_id then - local name = dfhack.translation.translateName(rec.name) - -- artifact_record stores the creation tick in `season_tick` - local date = format_date(rec.year, rec.season_tick or 0) - add_entry(string.format('Artifact "%s" created on %s', name, date)) - last_id = rec.id - end - end - state.last_artifact_id = last_id -end - -local function check_invasions() - for _, inv in ipairs(df.global.plotinfo.invasions.list) do - if inv.flags.active and not state.known_invasions[inv.id] then - state.known_invasions[inv.id] = true - local date = format_date(df.global.cur_year, df.global.cur_year_tick) - add_entry(string.format('Invasion started on %s', date)) - end - end -end - --- main loop; artifact and invasion tracking disabled to avoid scanning large --- data structures, which was causing hangs on some forts -local function event_loop() - if not state.enabled then return end - dfhack.timeout(1200, 'ticks', event_loop) -end +-- legacy scanning functions for artifacts and invasions have been removed in +-- favor of event-based tracking. the main loop is no longer needed. local function do_enable() state.enabled = true @@ -102,8 +74,6 @@ local function do_enable() eventful.onItemCreated[GLOBAL_KEY] = on_item_created eventful.onInvasion[GLOBAL_KEY] = on_invasion persist_state() - - event_loop() end local function do_disable() From dba3c1aa18270292ce31e2348bd05dbe6bf96be5 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sat, 19 Jul 2025 21:11:47 +0200 Subject: [PATCH 08/93] Refine holy-war mod with deity spheres --- docs/holy-war.rst | 24 +++++++ holy-war.lua | 160 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 docs/holy-war.rst create mode 100644 holy-war.lua diff --git a/docs/holy-war.rst b/docs/holy-war.rst new file mode 100644 index 000000000..b25dda91d --- /dev/null +++ b/docs/holy-war.rst @@ -0,0 +1,24 @@ +holy-war +======== + +.. dfhack-tool:: + :summary: Start wars when religions clash. + :tags: fort gameplay diplomacy + +This tool compares the spheres of influence represented by the gods of +nearby civilizations with those worshipped by your civilization and +represented in fortress temples. If no spheres overlap, or if the +historical record shows a ``religious_persecution_grudge`` between the +two peoples, the civilization is set to war. + +Usage +----- + +:: + + holy-war [--dry-run] + +When run without options, wars are declared immediately on all +qualifying civilizations and an announcement is displayed. With +``--dry-run``, the tool only reports which civilizations would be +affected without actually changing diplomacy. diff --git a/holy-war.lua b/holy-war.lua new file mode 100644 index 000000000..83836f862 --- /dev/null +++ b/holy-war.lua @@ -0,0 +1,160 @@ +-- Trigger wars based on conflicts between deity spheres +--@ module = true + +local utils = require('utils') + +local help = [====[ +holy-war +======== + +Provoke wars with civilizations that do not share deity spheres with +your civilization or the temples in your fortress. Religious +persecution grudges in the historical record also trigger war. + +Usage: + holy-war [--dry-run] + +If ``--dry-run`` is specified, no diplomatic changes are made and a list of +potential targets is printed instead. +]====] + +local valid_args = utils.invert({ 'dry-run', 'help' }) + +local RELIGIOUS_PERSECUTION_GRUDGE = + df.vague_relationship_type.RELIGIOUS_PERSECUTION_GRUDGE or + df.vague_relationship_type.religious_persecution_grudge + +local function merge(dst, src) + for k in pairs(src) do dst[k] = true end +end + +local function get_deity_spheres(hfid) + local spheres = {} + local hf = df.historical_figure.find(hfid) + if hf and hf.info and hf.info.metaphysical then + for _, sph in ipairs(hf.info.metaphysical.spheres) do + spheres[sph] = true + end + end + return spheres +end + +local function get_civ_spheres(civ) + local spheres = {} + for _, deity_id in ipairs(civ.relations.deities) do + merge(spheres, get_deity_spheres(deity_id)) + end + return spheres +end + +local function get_fort_spheres() + local spheres = {} + for _, bld in ipairs(df.global.world.buildings.all) do + if bld:getType() == df.building_type.Temple then + local dtype = bld.deity_type + if dtype == df.religious_practice_type.WORSHIP_HFID then + merge(spheres, get_deity_spheres(bld.deity_data.HFID)) + elseif dtype == df.religious_practice_type.RELIGION_ENID then + local rciv = df.historical_entity.find(bld.deity_data.Religion) + if rciv then merge(spheres, get_civ_spheres(rciv)) end + end + end + end + return spheres +end + +local function union(a, b) + local u = {} + merge(u, a) + merge(u, b) + return u +end + +local function share(p1, p2) + for k in pairs(p1) do + if p2[k] then return true end + end + return false +end + +local function get_civ_hists(civ) + local hfs = {} + for _, id in ipairs(civ.histfig_ids) do hfs[id] = true end + return hfs +end + +local function has_religious_grudge(p_hfs, t_hfs) + if not RELIGIOUS_PERSECUTION_GRUDGE then return false end + for _, set in ipairs(df.global.world.history.relationship_events) do + for i = 0, set.next_element-1 do + if set.relationship[i] == RELIGIOUS_PERSECUTION_GRUDGE then + local src = set.source_hf[i] + local tgt = set.target_hf[i] + if (p_hfs[src] and t_hfs[tgt]) or (p_hfs[tgt] and t_hfs[src]) then + return true + end + end + end + end + return false +end + +local function change_relation(target, relation) + local pciv = df.historical_entity.find(df.global.plotinfo.civ_id) + for _, state in ipairs(pciv.relations.diplomacy.state) do + if state.group_id == target.id then + state.relation = relation + end + end + for _, state in ipairs(target.relations.diplomacy.state) do + if state.group_id == pciv.id then + state.relation = relation + end + end +end + +local function main(...) + local args = utils.processArgs({...}, valid_args) + + if args.help then + print(help) + return + end + + local dry_run = args['dry-run'] + local pciv = df.historical_entity.find(df.global.plotinfo.civ_id) + local player_spheres = union(get_civ_spheres(pciv), get_fort_spheres()) + local player_hfs = get_civ_hists(pciv) + + for _, civ in ipairs(df.global.world.entities.all) do + if civ.type == 0 and civ.id ~= pciv.id then + local status + for _, state in ipairs(pciv.relations.diplomacy.state) do + if state.group_id == civ.id then + status = state.relation + break + end + end + if status == 0 or status == nil then -- peace or unknown + local civ_spheres = get_civ_spheres(civ) + local civ_hfs = get_civ_hists(civ) + if not share(player_spheres, civ_spheres) or + has_religious_grudge(player_hfs, civ_hfs) then + local name = dfhack.translation.translateName(civ.name, true) + if dry_run then + print(('Would declare war on %s over divine conflict.'):format(name)) + else + change_relation(civ, 1) -- war + dfhack.gui.showAnnouncement( + ('Religious persecution sparks war with %s!'):format(name), + COLOR_RED, true) + end + end + end + end + end +end + +if not dfhack_flags.module then + main(...) +end From 66bc94e8138823c9a82cfbbbb2da0e89ff23602b Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sat, 19 Jul 2025 21:17:46 +0200 Subject: [PATCH 09/93] Enhance chronicle output and item tracking --- chronicle.lua | 32 ++++++++++++++++++++------------ docs/chronicle.rst | 7 ++++--- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/chronicle.lua b/chronicle.lua index c247180ed..eb91cc8e6 100644 --- a/chronicle.lua +++ b/chronicle.lua @@ -1,4 +1,4 @@ --- Chronicles fortress events: unit deaths, artifact creation, and invasions +-- Chronicles fortress events: unit deaths, item creation, and invasions --@module = true --@enable = true @@ -43,18 +43,22 @@ end local function on_item_created(item_id) local item = df.item.find(item_id) - if not item or not item.flags.artifact then return end + if not item then return end - local gref = dfhack.items.getGeneralRef(item, df.general_ref_type.IS_ARTIFACT) - local rec = gref and df.artifact_record.find(gref.artifact_id) or nil - if not rec then return end + local date = format_date(df.global.cur_year, df.global.cur_year_tick) - local name = dfhack.translation.translateName(rec.name) - local date = format_date(rec.year, rec.season_tick or 0) - if rec.id > state.last_artifact_id then - state.last_artifact_id = rec.id + if item.flags.artifact then + local gref = dfhack.items.getGeneralRef(item, df.general_ref_type.IS_ARTIFACT) + local rec = gref and df.artifact_record.find(gref.artifact_id) or nil + local name = rec and dfhack.translation.translateName(rec.name) or 'unknown artifact' + if rec and rec.id > state.last_artifact_id then + state.last_artifact_id = rec.id + end + add_entry(string.format('Artifact "%s" created on %s', name, date)) + else + local desc = dfhack.items.getDescription(item, 0, true) + add_entry(string.format('Item "%s" created on %s', desc, date)) end - add_entry(string.format('Artifact "%s" created on %s', name, date)) end local function on_invasion(invasion_id) @@ -134,8 +138,12 @@ elseif cmd == 'clear' then state.entries = {} persist_state() elseif cmd == 'print' then - for _, entry in ipairs(state.entries) do - print(entry) + if #state.entries == 0 then + print('Chronicle is empty.') + else + for _, entry in ipairs(state.entries) do + print(entry) + end end else print(dfhack.script_help()) diff --git a/docs/chronicle.rst b/docs/chronicle.rst index e573d967b..0c4c5b7c1 100644 --- a/docs/chronicle.rst +++ b/docs/chronicle.rst @@ -2,11 +2,12 @@ chronicle ========= .. dfhack-tool:: - :summary: Record fortress events like deaths, artifacts, and invasions. + :summary: Record fortress events like deaths, item creation, and invasions. :tags: fort gameplay This tool automatically records notable events in a chronicle that is stored -with your save. Unit deaths, artifact creation, and invasions are recorded. +with your save. Unit deaths, all item creation events, and invasions are +recorded. Usage ----- @@ -23,6 +24,6 @@ Usage ``chronicle disable`` Stop recording events. ``chronicle print`` - Print all recorded events. + Print all recorded events. Prints a notice if the chronicle is empty. ``chronicle clear`` Delete the chronicle. From 55777ed10d6012b176d3d3a1a7dbf0afdcb7608f Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sat, 19 Jul 2025 21:32:46 +0200 Subject: [PATCH 10/93] Ensure holy-war sets war on both sides --- docs/holy-war.rst | 4 +++- holy-war.lua | 13 ++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/holy-war.rst b/docs/holy-war.rst index b25dda91d..b5b6e3735 100644 --- a/docs/holy-war.rst +++ b/docs/holy-war.rst @@ -9,7 +9,9 @@ This tool compares the spheres of influence represented by the gods of nearby civilizations with those worshipped by your civilization and represented in fortress temples. If no spheres overlap, or if the historical record shows a ``religious_persecution_grudge`` between the -two peoples, the civilization is set to war. +two peoples, the civilization is set to war. Both your stance toward +the other civilization and their stance toward you are set to war, +ensuring a mutual declaration. Usage ----- diff --git a/holy-war.lua b/holy-war.lua index 83836f862..02ec8a31b 100644 --- a/holy-war.lua +++ b/holy-war.lua @@ -128,14 +128,21 @@ local function main(...) for _, civ in ipairs(df.global.world.entities.all) do if civ.type == 0 and civ.id ~= pciv.id then - local status + local p_status for _, state in ipairs(pciv.relations.diplomacy.state) do if state.group_id == civ.id then - status = state.relation + p_status = state.relation break end end - if status == 0 or status == nil then -- peace or unknown + local c_status + for _, state in ipairs(civ.relations.diplomacy.state) do + if state.group_id == pciv.id then + c_status = state.relation + break + end + end + if p_status ~= 1 or c_status ~= 1 then -- not already mutually at war local civ_spheres = get_civ_spheres(civ) local civ_hfs = get_civ_hists(civ) if not share(player_spheres, civ_spheres) or From f79f046f5c5104bbd0e9f38c5afec4a7545cb7fd Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sat, 19 Jul 2025 21:35:26 +0200 Subject: [PATCH 11/93] Refine chronicle output --- chronicle.lua | 61 +++++++++++++++++++++++++++++++++++++--------- docs/chronicle.rst | 6 +++-- 2 files changed, 54 insertions(+), 13 deletions(-) diff --git a/chronicle.lua b/chronicle.lua index eb91cc8e6..40f0dd533 100644 --- a/chronicle.lua +++ b/chronicle.lua @@ -21,15 +21,52 @@ local function persist_state() dfhack.persistent.saveSiteData(GLOBAL_KEY, state) end +local months = { + 'Granite', 'Slate', 'Felsite', + 'Hematite', 'Malachite', 'Galena', + 'Limestone', 'Sandstone', 'Timber', + 'Moonstone', 'Opal', 'Obsidian', +} + +local seasons = { + 'Early Spring', 'Mid Spring', 'Late Spring', + 'Early Summer', 'Mid Summer', 'Late Summer', + 'Early Autumn', 'Mid Autumn', 'Late Autumn', + 'Early Winter', 'Mid Winter', 'Late Winter', +} + +local function ordinal(n) + local rem100 = n % 100 + local rem10 = n % 10 + local suffix = 'th' + if rem100 < 11 or rem100 > 13 then + if rem10 == 1 then suffix = 'st' + elseif rem10 == 2 then suffix = 'nd' + elseif rem10 == 3 then suffix = 'rd' + end + end + return ('%d%s'):format(n, suffix) +end + local function format_date(year, ticks) - local julian_day = math.floor(ticks / 1200) + 1 - local month = math.floor(julian_day / 28) + 1 - local day = julian_day % 28 - return string.format('%03d-%02d-%02d', year, month, day) + local day_of_year = math.floor(ticks / 1200) + 1 + local month = math.floor((day_of_year - 1) / 28) + 1 + local day = ((day_of_year - 1) % 28) + 1 + local month_name = months[month] or ('Month' .. tostring(month)) + local season = seasons[month] or 'Unknown Season' + return string.format('%s %s, %s of Year %d', ordinal(day), month_name, season, year) +end + +local function sanitize(text) + -- convert game strings to utf8 and remove non-printable characters + local str = dfhack.df2utf(text or '') + -- strip control characters that may have leaked through + str = str:gsub('[%z\1-\31]', '') + return str end local function add_entry(text) - table.insert(state.entries, text) + table.insert(state.entries, sanitize(text)) persist_state() end @@ -38,7 +75,7 @@ local function on_unit_death(unit_id) if not unit then return end local name = dfhack.units.getReadableName(unit) local date = format_date(df.global.cur_year, df.global.cur_year_tick) - add_entry(string.format('Death of %s on %s', name, date)) + add_entry(string.format('%s: Death of %s', date, name)) end local function on_item_created(item_id) @@ -54,10 +91,10 @@ local function on_item_created(item_id) if rec and rec.id > state.last_artifact_id then state.last_artifact_id = rec.id end - add_entry(string.format('Artifact "%s" created on %s', name, date)) + add_entry(string.format('%s: Artifact "%s" created', date, name)) else local desc = dfhack.items.getDescription(item, 0, true) - add_entry(string.format('Item "%s" created on %s', desc, date)) + add_entry(string.format('%s: Item "%s" created', date, desc)) end end @@ -65,7 +102,7 @@ local function on_invasion(invasion_id) if state.known_invasions[invasion_id] then return end state.known_invasions[invasion_id] = true local date = format_date(df.global.cur_year, df.global.cur_year_tick) - add_entry(string.format('Invasion started on %s', date)) + add_entry(string.format('%s: Invasion started', date)) end -- legacy scanning functions for artifacts and invasions have been removed in -- favor of event-based tracking. the main loop is no longer needed. @@ -138,11 +175,13 @@ elseif cmd == 'clear' then state.entries = {} persist_state() elseif cmd == 'print' then + local count = tonumber(args[2]) or 25 if #state.entries == 0 then print('Chronicle is empty.') else - for _, entry in ipairs(state.entries) do - print(entry) + local start_idx = math.max(1, #state.entries - count + 1) + for i = start_idx, #state.entries do + print(state.entries[i]) end end else diff --git a/docs/chronicle.rst b/docs/chronicle.rst index 0c4c5b7c1..0c5b039ef 100644 --- a/docs/chronicle.rst +++ b/docs/chronicle.rst @@ -16,7 +16,7 @@ Usage chronicle enable chronicle disable - chronicle print + chronicle print [count] chronicle clear ``chronicle enable`` @@ -24,6 +24,8 @@ Usage ``chronicle disable`` Stop recording events. ``chronicle print`` - Print all recorded events. Prints a notice if the chronicle is empty. + Print the most recent recorded events. Takes an optional ``count`` + argument (default ``25``) that specifies how many events to show. Prints + a notice if the chronicle is empty. ``chronicle clear`` Delete the chronicle. From 5839e89fc7014d99d9d40cd082115e13a7e75bb5 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sat, 19 Jul 2025 21:44:54 +0200 Subject: [PATCH 12/93] holy-war: display war reason --- docs/holy-war.rst | 5 ++++- holy-war.lua | 30 ++++++++++++++++++++++++++---- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/docs/holy-war.rst b/docs/holy-war.rst index b5b6e3735..c24f45266 100644 --- a/docs/holy-war.rst +++ b/docs/holy-war.rst @@ -23,4 +23,7 @@ Usage When run without options, wars are declared immediately on all qualifying civilizations and an announcement is displayed. With ``--dry-run``, the tool only reports which civilizations would be -affected without actually changing diplomacy. +affected without actually changing diplomacy. Each message also notes +whether the conflict arises from disjoint spheres of influence or a +religious persecution grudge and lists the conflicting spheres when +appropriate. diff --git a/holy-war.lua b/holy-war.lua index 02ec8a31b..2326682bf 100644 --- a/holy-war.lua +++ b/holy-war.lua @@ -28,6 +28,15 @@ local function merge(dst, src) for k in pairs(src) do dst[k] = true end end +local function spheres_to_str(spheres) + local names = {} + for sph in pairs(spheres) do + table.insert(names, df.sphere_type[sph]) + end + table.sort(names) + return table.concat(names, ', ') +end + local function get_deity_spheres(hfid) local spheres = {} local hf = df.historical_figure.find(hfid) @@ -145,15 +154,28 @@ local function main(...) if p_status ~= 1 or c_status ~= 1 then -- not already mutually at war local civ_spheres = get_civ_spheres(civ) local civ_hfs = get_civ_hists(civ) - if not share(player_spheres, civ_spheres) or - has_religious_grudge(player_hfs, civ_hfs) then + local divine_conflict = not share(player_spheres, civ_spheres) + local persecution = has_religious_grudge(player_hfs, civ_hfs) + if divine_conflict or persecution then local name = dfhack.translation.translateName(civ.name, true) + local reason_parts = {} + if divine_conflict then + table.insert(reason_parts, + ('conflicting spheres (%s vs %s)'):format( + spheres_to_str(player_spheres), + spheres_to_str(civ_spheres))) + end + if persecution then + table.insert(reason_parts, 'religious persecution') + end + local reason = table.concat(reason_parts, ' and ') if dry_run then - print(('Would declare war on %s over divine conflict.'):format(name)) + print(('Would declare war on %s due to %s.'):format(name, reason)) else change_relation(civ, 1) -- war dfhack.gui.showAnnouncement( - ('Religious persecution sparks war with %s!'):format(name), + ('%s sparks war with %s!'):format( + reason:gsub('^.', string.upper), name), COLOR_RED, true) end end From 9a12910301bc264fa3757771797979ae1bf15978 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sat, 19 Jul 2025 21:53:14 +0200 Subject: [PATCH 13/93] Summarize non-artifact item creation --- chronicle.lua | 65 +++++++++++++++++++++++++++++++++++++++++++--- docs/chronicle.rst | 7 +++-- 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/chronicle.lua b/chronicle.lua index 40f0dd533..54d02e0b4 100644 --- a/chronicle.lua +++ b/chronicle.lua @@ -12,6 +12,7 @@ local function get_default_state() entries = {}, last_artifact_id = -1, known_invasions = {}, + item_counts = {}, -- item creation summary per year } end @@ -70,6 +71,40 @@ local function add_entry(text) persist_state() end +local CATEGORY_MAP = { + -- food and food-related + DRINK='food', DRINK2='food', FOOD='food', MEAT='food', FISH='food', + FISH_RAW='food', PLANT='food', PLANT_GROWTH='food', SEEDS='food', + EGG='food', CHEESE='food', POWDER_MISC='food', LIQUID_MISC='food', + GLOB='food', + -- weapons and defense + WEAPON='weapons', TRAPCOMP='weapons', + AMMO='ammo', SIEGEAMMO='ammo', + ARMOR='armor', PANTS='armor', HELM='armor', GLOVES='armor', + SHOES='armor', SHIELD='armor', QUIVER='armor', + -- materials + WOOD='wood', BOULDER='stone', ROCK='stone', ROUGH='gems', SMALLGEM='gems', + BAR='bars_blocks', BLOCKS='bars_blocks', + -- misc + COIN='coins', + -- finished goods and furniture + FIGURINE='finished_goods', AMULET='finished_goods', SCEPTER='finished_goods', + CROWN='finished_goods', RING='finished_goods', EARRING='finished_goods', + BRACELET='finished_goods', CRAFTS='finished_goods', TOY='finished_goods', + TOOL='finished_goods', GOBLET='finished_goods', FLASK='finished_goods', + BOX='furniture', BARREL='furniture', BED='furniture', CHAIR='furniture', + TABLE='furniture', DOOR='furniture', WINDOW='furniture', BIN='furniture', +} + +local IGNORE_TYPES = { + CORPSE=true, CORPSEPIECE=true, REMAINS=true, +} + +local function get_category(item) + local t = df.item_type[item:getType()] + return CATEGORY_MAP[t] or 'other' +end + local function on_unit_death(unit_id) local unit = df.unit.find(unit_id) if not unit then return end @@ -92,10 +127,17 @@ local function on_item_created(item_id) state.last_artifact_id = rec.id end add_entry(string.format('%s: Artifact "%s" created', date, name)) - else - local desc = dfhack.items.getDescription(item, 0, true) - add_entry(string.format('%s: Item "%s" created', date, desc)) + return end + + local type_name = df.item_type[item:getType()] + if IGNORE_TYPES[type_name] then return end + + local year = df.global.cur_year + local category = get_category(item) + state.item_counts[year] = state.item_counts[year] or {} + state.item_counts[year][category] = (state.item_counts[year][category] or 0) + 1 + persist_state() end local function on_invasion(invasion_id) @@ -184,6 +226,23 @@ elseif cmd == 'print' then print(state.entries[i]) end end +elseif cmd == 'summary' then + local years = {} + for year in pairs(state.item_counts) do table.insert(years, year) end + table.sort(years) + if #years == 0 then + print('No item creation records.') + return + end + for _,year in ipairs(years) do + local counts = state.item_counts[year] + local parts = {} + for cat,count in pairs(counts) do + table.insert(parts, string.format('%d %s', count, cat)) + end + table.sort(parts) + print(string.format('Year %d: %s', year, table.concat(parts, ', '))) + end else print(dfhack.script_help()) end diff --git a/docs/chronicle.rst b/docs/chronicle.rst index 0c5b039ef..d59b6e42b 100644 --- a/docs/chronicle.rst +++ b/docs/chronicle.rst @@ -6,8 +6,8 @@ chronicle :tags: fort gameplay This tool automatically records notable events in a chronicle that is stored -with your save. Unit deaths, all item creation events, and invasions are -recorded. +with your save. Unit deaths, artifact creation events, invasions, and yearly +totals of crafted items are recorded. Usage ----- @@ -17,6 +17,7 @@ Usage chronicle enable chronicle disable chronicle print [count] + chronicle summary chronicle clear ``chronicle enable`` @@ -27,5 +28,7 @@ Usage Print the most recent recorded events. Takes an optional ``count`` argument (default ``25``) that specifies how many events to show. Prints a notice if the chronicle is empty. +``chronicle summary`` + Show yearly totals of created items by category (non-artifact items only). ``chronicle clear`` Delete the chronicle. From d7b8668d9a37113bf7f5a165b0a886b99c1e79f8 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sat, 19 Jul 2025 21:53:53 +0200 Subject: [PATCH 14/93] Improve holy-war output --- docs/holy-war.rst | 3 +++ holy-war.lua | 19 +++++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/docs/holy-war.rst b/docs/holy-war.rst index c24f45266..3c9ceecf2 100644 --- a/docs/holy-war.rst +++ b/docs/holy-war.rst @@ -13,6 +13,9 @@ two peoples, the civilization is set to war. Both your stance toward the other civilization and their stance toward you are set to war, ensuring a mutual declaration. +Civilizations without proper names are ignored, and the reported sphere +lists contain only the spheres unique to each civilization. + Usage ----- diff --git a/holy-war.lua b/holy-war.lua index 2326682bf..71227dafe 100644 --- a/holy-war.lua +++ b/holy-war.lua @@ -37,6 +37,14 @@ local function spheres_to_str(spheres) return table.concat(names, ', ') end +local function diff(a, b) + local d = {} + for k in pairs(a) do + if not b[k] then d[k] = true end + end + return d +end + local function get_deity_spheres(hfid) local spheres = {} local hf = df.historical_figure.find(hfid) @@ -154,16 +162,19 @@ local function main(...) if p_status ~= 1 or c_status ~= 1 then -- not already mutually at war local civ_spheres = get_civ_spheres(civ) local civ_hfs = get_civ_hists(civ) - local divine_conflict = not share(player_spheres, civ_spheres) + local divine_conflict = next(player_spheres) and next(civ_spheres) + and not share(player_spheres, civ_spheres) local persecution = has_religious_grudge(player_hfs, civ_hfs) - if divine_conflict or persecution then + if (divine_conflict or persecution) and civ.name.has_name then local name = dfhack.translation.translateName(civ.name, true) local reason_parts = {} if divine_conflict then + local p_diff = diff(player_spheres, civ_spheres) + local c_diff = diff(civ_spheres, player_spheres) table.insert(reason_parts, ('conflicting spheres (%s vs %s)'):format( - spheres_to_str(player_spheres), - spheres_to_str(civ_spheres))) + spheres_to_str(p_diff), + spheres_to_str(c_diff))) end if persecution then table.insert(reason_parts, 'religious persecution') From 1c8ab864bf87e0b6babf0607f465e718b4b10d9c Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sat, 19 Jul 2025 22:10:22 +0200 Subject: [PATCH 15/93] Need-aquire --- need-acquire.lua | 148 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 need-acquire.lua diff --git a/need-acquire.lua b/need-acquire.lua new file mode 100644 index 000000000..f911c5bd9 --- /dev/null +++ b/need-acquire.lua @@ -0,0 +1,148 @@ +-- need-acquire.lua +-- ver 0.9 +-- Assign existing Trinkets to Citizen for them to collect +-- to fulfill their Acquire Object Need. +-- + +local utils=require('utils') + +validArgs = utils.invert({ + 'help', + 't' +}) +local args = utils.processArgs({...}, validArgs) +local aquire_need_id = 20 +local aquire_threshold = -1000 +local helpme = [===[ +Assign existing trinkets to citizen for them to collect, +to fulfill their "Acquire Object" Need. +This does not simple change the need. +You need the have the Trinkets in your fortress and the dwarfs will +go and collect their new Items. +Setup: + -An stockpile full of trinkets +arguments: + -t + Expects an integer value. + The negativ need threshhold to trigger for each citizen + Default is 3000 + -help + Display this text +]===] + + +--###### +--Helper +--###### + +function getAllCititzen() +local citizen = {} +local my_civ = df.global.world.world_data.active_site[0].entity_links[0].entity_id +for n, unit in ipairs(df.global.world.units.all) do + if unit.civ_id == my_civ and dfhack.units.isCitizen(unit) then + if unit.profession ~= df.profession.BABY and unit.profession ~= df.profession.CHILD then + table.insert(citizen, unit) + end + end +end +return citizen +end +local citizen = getAllCititzen() + +function findNeed(unit,need_id) + local needs = unit.status.current_soul.personality.needs + local need_index = -1 + for k = #needs-1,0,-1 do + if needs[k].id == need_id then + need_index = k + break + end + end + if (need_index ~= -1 ) then + return needs[need_index] + end + return nil +end + +--###### +--Main +--###### + + +function getFreeTrinkets() + -- Where are the items that we can we give away? + local item_count = 0 + local aquire_item_list = {} + local trinket_list = {} + for _, i in ipairs(df.global.world.items.other.EARRING) do + table.insert(trinket_list,i) + end + for _, i in ipairs(df.global.world.items.other.RING) do + table.insert(trinket_list,i) + end + for _, i in ipairs(df.global.world.items.other.AMULET) do + table.insert(trinket_list,i) + end + for _, i in ipairs(df.global.world.items.other.BRACELET) do + table.insert(trinket_list,i) + end + for _, i in ipairs(trinket_list) do + if ( not i.flags.trader and + not i.flags.in_job and + not i.flags.construction and + not i.flags.removed and + not i.flags.forbid and + not i.flags.dump and + not i.flags.owned) then + item_count = item_count+1 + table.insert(aquire_item_list,i) + --end + end + end + return aquire_item_list +end + + +function giveItems() + local aquire_item_list = getFreeTrinkets() + --WHo needs to acquire new Item real bad? + local aquire_count = 0 + local missing_item_count = 0 + local fullfilled_count = 0 + for i, unit in ipairs(citizen) do + -- Find local need + local need = findNeed(unit,aquire_need_id) + if (need ~= nil ) then + if ( need.focus_level < aquire_threshold ) then + aquire_count = aquire_count+1 + if ( aquire_item_list[aquire_count] ~= nill) then + fullfilled_count = fullfilled_count+1 + dfhack.items.setOwner(aquire_item_list[aquire_count],unit) + need.focus_level = 200 + need.need_level = 1 + else + missing_item_count = missing_item_count+1 + end + end + end + end + dfhack.print("need-acquire | Need: ".. aquire_count ) + dfhack.print(" Done: ".. fullfilled_count ) + dfhack.println(" TODO: ".. missing_item_count ) + if (missing_item_count > 0) then + dfhack.print("need-acquire | ") + dfhack.printerr("Need " .. missing_item_count .. " more Trinkets to fulfill needs!") + return + end +end + +if (args.help) then + print(helpme) + return +end + +if (args.t) then + aquire_threshold = 0-tonumber(args.t) +end + +giveItems() \ No newline at end of file From b541254dc1e4b80b4debae1e1848c97f341b4636 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sat, 19 Jul 2025 22:16:44 +0200 Subject: [PATCH 16/93] chronicle: capture artifact announcements --- chronicle.lua | 34 ++++++++++++++++++++++++++++++---- docs/chronicle.rst | 4 +++- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/chronicle.lua b/chronicle.lua index 54d02e0b4..98e29f92f 100644 --- a/chronicle.lua +++ b/chronicle.lua @@ -59,8 +59,8 @@ local function format_date(year, ticks) end local function sanitize(text) - -- convert game strings to utf8 and remove non-printable characters - local str = dfhack.df2utf(text or '') + -- convert game strings to console encoding and remove non-printable characters + local str = dfhack.df2console(text or '') -- strip control characters that may have leaked through str = str:gsub('[%z\1-\31]', '') return str @@ -122,11 +122,10 @@ local function on_item_created(item_id) if item.flags.artifact then local gref = dfhack.items.getGeneralRef(item, df.general_ref_type.IS_ARTIFACT) local rec = gref and df.artifact_record.find(gref.artifact_id) or nil - local name = rec and dfhack.translation.translateName(rec.name) or 'unknown artifact' if rec and rec.id > state.last_artifact_id then state.last_artifact_id = rec.id end - add_entry(string.format('%s: Artifact "%s" created', date, name)) + -- artifact announcements are captured via REPORT events return end @@ -146,6 +145,29 @@ local function on_invasion(invasion_id) local date = format_date(df.global.cur_year, df.global.cur_year_tick) add_entry(string.format('%s: Invasion started', date)) end + +-- capture artifact announcements verbatim from reports +local pending_artifact_report +local function on_report(report_id) + local rep = df.report.find(report_id) + if not rep or not rep.flags.announcement then return end + local text = dfhack.df2console(rep.text) + if pending_artifact_report then + if text:find(' offers it to ') then + local date = format_date(df.global.cur_year, df.global.cur_year_tick) + add_entry(string.format('%s: %s %s', date, pending_artifact_report, text)) + pending_artifact_report = nil + return + else + local date = format_date(df.global.cur_year, df.global.cur_year_tick) + add_entry(string.format('%s: %s', date, pending_artifact_report)) + pending_artifact_report = nil + end + end + if text:find(' has created ') then + pending_artifact_report = text + end +end -- legacy scanning functions for artifacts and invasions have been removed in -- favor of event-based tracking. the main loop is no longer needed. @@ -153,9 +175,11 @@ local function do_enable() state.enabled = true eventful.enableEvent(eventful.eventType.ITEM_CREATED, 1) eventful.enableEvent(eventful.eventType.INVASION, 1) + eventful.enableEvent(eventful.eventType.REPORT, 1) eventful.onUnitDeath[GLOBAL_KEY] = on_unit_death eventful.onItemCreated[GLOBAL_KEY] = on_item_created eventful.onInvasion[GLOBAL_KEY] = on_invasion + eventful.onReport[GLOBAL_KEY] = on_report persist_state() end @@ -164,6 +188,7 @@ local function do_disable() eventful.onUnitDeath[GLOBAL_KEY] = nil eventful.onItemCreated[GLOBAL_KEY] = nil eventful.onInvasion[GLOBAL_KEY] = nil + eventful.onReport[GLOBAL_KEY] = nil persist_state() end @@ -179,6 +204,7 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) eventful.onUnitDeath[GLOBAL_KEY] = nil eventful.onItemCreated[GLOBAL_KEY] = nil eventful.onInvasion[GLOBAL_KEY] = nil + eventful.onReport[GLOBAL_KEY] = nil state.enabled = false return end diff --git a/docs/chronicle.rst b/docs/chronicle.rst index d59b6e42b..aa333a411 100644 --- a/docs/chronicle.rst +++ b/docs/chronicle.rst @@ -7,7 +7,9 @@ chronicle This tool automatically records notable events in a chronicle that is stored with your save. Unit deaths, artifact creation events, invasions, and yearly -totals of crafted items are recorded. +totals of crafted items are recorded. Artifact entries now include the full +announcement text from the game, complete with item descriptions and special +characters rendered just as they appear in the in-game logs. Usage ----- From 27f92b881ced5c256307c6088f3059d90c105bc4 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sat, 19 Jul 2025 22:25:23 +0200 Subject: [PATCH 17/93] Improve need-acquire.lua with documentation and cleanup --- need-acquire.lua | 200 ++++++++++++++++++----------------------------- 1 file changed, 76 insertions(+), 124 deletions(-) diff --git a/need-acquire.lua b/need-acquire.lua index f911c5bd9..e7d00fb5a 100644 --- a/need-acquire.lua +++ b/need-acquire.lua @@ -1,148 +1,100 @@ --- need-acquire.lua --- ver 0.9 --- Assign existing Trinkets to Citizen for them to collect --- to fulfill their Acquire Object Need. --- +-- Assign trinkets to citizens so they can satisfy the "Acquire Object" need. +-- Derived from an old Bay12 forums script and updated for modern DFHack. -local utils=require('utils') +local HELP = [=[ +need-acquire +============ +Assign trinkets to citizens who have a strong "Acquire Object" need. -validArgs = utils.invert({ - 'help', - 't' -}) -local args = utils.processArgs({...}, validArgs) -local aquire_need_id = 20 -local aquire_threshold = -1000 -local helpme = [===[ -Assign existing trinkets to citizen for them to collect, -to fulfill their "Acquire Object" Need. -This does not simple change the need. -You need the have the Trinkets in your fortress and the dwarfs will -go and collect their new Items. -Setup: - -An stockpile full of trinkets -arguments: - -t - Expects an integer value. - The negativ need threshhold to trigger for each citizen - Default is 3000 - -help - Display this text -]===] +Usage: + need-acquire [-t ] +Options: + -t Focus level below which the need is considered unmet + (default: -3000). + -help Show this help text. +]=] ---###### ---Helper ---###### +local utils = require('utils') -function getAllCititzen() -local citizen = {} -local my_civ = df.global.world.world_data.active_site[0].entity_links[0].entity_id -for n, unit in ipairs(df.global.world.units.all) do - if unit.civ_id == my_civ and dfhack.units.isCitizen(unit) then - if unit.profession ~= df.profession.BABY and unit.profession ~= df.profession.CHILD then - table.insert(citizen, unit) - end - end +local valid_args = utils.invert{'help', 't'} +local args = utils.processArgs({...}, valid_args) + +local ACQUIRE_NEED_ID = df.need_type.AcquireObject +local acquire_threshold = -3000 + +if args.help then + print(HELP) + return end -return citizen + +if args.t then + acquire_threshold = -tonumber(args.t) end -local citizen = getAllCititzen() -function findNeed(unit,need_id) - local needs = unit.status.current_soul.personality.needs - local need_index = -1 - for k = #needs-1,0,-1 do - if needs[k].id == need_id then - need_index = k - break +local function get_citizens() + local result = {} + for _, unit in ipairs(dfhack.units.getCitizens(true)) do + if unit.profession ~= df.profession.BABY and + unit.profession ~= df.profession.CHILD then + table.insert(result, unit) end - end - if (need_index ~= -1 ) then - return needs[need_index] end - return nil + return result end ---###### ---Main ---###### - - -function getFreeTrinkets() - -- Where are the items that we can we give away? - local item_count = 0 - local aquire_item_list = {} - local trinket_list = {} - for _, i in ipairs(df.global.world.items.other.EARRING) do - table.insert(trinket_list,i) - end - for _, i in ipairs(df.global.world.items.other.RING) do - table.insert(trinket_list,i) - end - for _, i in ipairs(df.global.world.items.other.AMULET) do - table.insert(trinket_list,i) - end - for _, i in ipairs(df.global.world.items.other.BRACELET) do - table.insert(trinket_list,i) - end - for _, i in ipairs(trinket_list) do - if ( not i.flags.trader and - not i.flags.in_job and - not i.flags.construction and - not i.flags.removed and - not i.flags.forbid and - not i.flags.dump and - not i.flags.owned) then - item_count = item_count+1 - table.insert(aquire_item_list,i) - --end +local function find_need(unit, need_id) + if not unit.status.current_soul then return nil end + local needs = unit.status.current_soul.personality.needs + for idx = #needs - 1, 0, -1 do + if needs[idx].id == need_id then + return needs[idx] end - end - return aquire_item_list + end end +local function get_free_trinkets() + local trinkets = {} + local function add(list) for _, i in ipairs(list) do table.insert(trinkets, i) end end + add(df.global.world.items.other.EARRING) + add(df.global.world.items.other.RING) + add(df.global.world.items.other.AMULET) + add(df.global.world.items.other.BRACELET) + local free = {} + for _, item in ipairs(trinkets) do + if not (item.flags.trader or item.flags.in_job or item.flags.construction or + item.flags.removed or item.flags.forbid or item.flags.dump or + item.flags.owned) then + table.insert(free, item) + end + end + return free +end -function giveItems() - local aquire_item_list = getFreeTrinkets() - --WHo needs to acquire new Item real bad? - local aquire_count = 0 - local missing_item_count = 0 - local fullfilled_count = 0 - for i, unit in ipairs(citizen) do - -- Find local need - local need = findNeed(unit,aquire_need_id) - if (need ~= nil ) then - if ( need.focus_level < aquire_threshold ) then - aquire_count = aquire_count+1 - if ( aquire_item_list[aquire_count] ~= nill) then - fullfilled_count = fullfilled_count+1 - dfhack.items.setOwner(aquire_item_list[aquire_count],unit) +local function give_items() + local trinkets = get_free_trinkets() + local needs, fulfilled = 0, 0 + local idx = 1 + for _, unit in ipairs(get_citizens()) do + local need = find_need(unit, ACQUIRE_NEED_ID) + if need and need.focus_level < acquire_threshold then + needs = needs + 1 + local item = trinkets[idx] + if item then + dfhack.items.setOwner(item, unit) need.focus_level = 200 need.need_level = 1 - else - missing_item_count = missing_item_count+1 + fulfilled = fulfilled + 1 + idx = idx + 1 end end - end end - dfhack.print("need-acquire | Need: ".. aquire_count ) - dfhack.print(" Done: ".. fullfilled_count ) - dfhack.println(" TODO: ".. missing_item_count ) - if (missing_item_count > 0) then - dfhack.print("need-acquire | ") - dfhack.printerr("Need " .. missing_item_count .. " more Trinkets to fulfill needs!") - return - end -end - -if (args.help) then - print(helpme) - return + local missing = needs - fulfilled + dfhack.println(('need-acquire | Need: %d Done: %d TODO: %d'):format(needs, fulfilled, missing)) + if missing > 0 then + dfhack.printerr('Need ' .. missing .. ' more trinkets to fulfill needs!') + end end -if (args.t) then - aquire_threshold = 0-tonumber(args.t) -end +give_items() -giveItems() \ No newline at end of file From 1271e4211842dde907f3d61cb85073cebc986472 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sat, 19 Jul 2025 23:27:11 +0200 Subject: [PATCH 18/93] chronicle: add export command --- chronicle.lua | 140 +++++++++++++++++++++++++++++++++++++++++++-- docs/chronicle.rst | 19 ++++-- 2 files changed, 150 insertions(+), 9 deletions(-) diff --git a/chronicle.lua b/chronicle.lua index 98e29f92f..592affdd2 100644 --- a/chronicle.lua +++ b/chronicle.lua @@ -13,6 +13,7 @@ local function get_default_state() last_artifact_id = -1, known_invasions = {}, item_counts = {}, -- item creation summary per year + log_masterworks = true, -- capture "masterpiece" announcements } end @@ -58,11 +59,41 @@ local function format_date(year, ticks) return string.format('%s %s, %s of Year %d', ordinal(day), month_name, season, year) end +local function transliterate(str) + -- replace unicode punctuation with ASCII equivalents + str = str:gsub('[\226\128\152\226\128\153]', "'") -- single quotes + str = str:gsub('[\226\128\156\226\128\157]', '"') -- double quotes + str = str:gsub('\226\128\147', '-') -- en dash + str = str:gsub('\226\128\148', '-') -- em dash + str = str:gsub('\226\128\166', '...') -- ellipsis + + local accent_map = { + ['á']='a', ['à']='a', ['ä']='a', ['â']='a', ['ã']='a', ['å']='a', + ['Á']='A', ['À']='A', ['Ä']='A', ['Â']='A', ['Ã']='A', ['Å']='A', + ['é']='e', ['è']='e', ['ë']='e', ['ê']='e', + ['É']='E', ['È']='E', ['Ë']='E', ['Ê']='E', + ['í']='i', ['ì']='i', ['ï']='i', ['î']='i', + ['Í']='I', ['Ì']='I', ['Ï']='I', ['Î']='I', + ['ó']='o', ['ò']='o', ['ö']='o', ['ô']='o', ['õ']='o', + ['Ó']='O', ['Ò']='O', ['Ö']='O', ['Ô']='O', ['Õ']='O', + ['ú']='u', ['ù']='u', ['ü']='u', ['û']='u', + ['Ú']='U', ['Ù']='U', ['Ü']='U', ['Û']='U', + ['ç']='c', ['Ç']='C', ['ñ']='n', ['Ñ']='N', ['ß']='ss', + ['Æ']='AE', ['æ']='ae', ['Ø']='O', ['ø']='o', + ['Þ']='Th', ['þ']='th', ['Ð']='Dh', ['ð']='dh', + } + for k,v in pairs(accent_map) do + str = str:gsub(k, v) + end + return str +end + local function sanitize(text) - -- convert game strings to console encoding and remove non-printable characters - local str = dfhack.df2console(text or '') + -- convert game strings to UTF-8 and remove non-printable characters + local str = dfhack.df2utf(text or '') -- strip control characters that may have leaked through str = str:gsub('[%z\1-\31]', '') + str = transliterate(str) return str end @@ -71,6 +102,73 @@ local function add_entry(text) persist_state() end +local function export_chronicle(path) + path = path or (dfhack.getSavePath() .. '/chronicle.txt') + local ok, f = pcall(io.open, path, 'w') + if not ok or not f then + qerror('Cannot open file for writing: ' .. path) + end + for _,entry in ipairs(state.entries) do + f:write(entry, '\n') + end + f:close() + print('Chronicle written to: ' .. path) +end + +local DEATH_TYPES = reqscript('gui/unit-info-viewer').DEATH_TYPES + +local function trim(str) + return str:gsub('^%s+', ''):gsub('%s+$', '') +end + +local function get_race_name(race_id) + return df.creature_raw.find(race_id).name[0] +end + +local function death_string(cause) + if cause == -1 then return 'died' end + return trim(DEATH_TYPES[cause] or 'died') +end + +local function describe_unit(unit) + local name = dfhack.units.getReadableName(unit) + if unit.name.nickname ~= '' and not name:find(unit.name.nickname, 1, true) then + name = name:gsub(unit.name.first_name, unit.name.first_name .. ' "' .. unit.name.nickname .. '"') + end + local titles = {} + local prof = dfhack.units.getProfessionName(unit) + if prof and prof ~= '' then table.insert(titles, prof) end + for _, np in ipairs(dfhack.units.getNoblePositions(unit) or {}) do + if np.position and np.position.name and np.position.name[0] ~= '' then + table.insert(titles, np.position.name[0]) + end + end + if #titles > 0 then + name = name .. ' (' .. table.concat(titles, ', ') .. ')' + end + return name +end + +local function format_death_text(unit) + local str = unit.name.has_name and '' or 'The ' + str = str .. describe_unit(unit) + str = str .. ' ' .. death_string(unit.counters.death_cause) + local incident = df.incident.find(unit.counters.death_id) + if incident then + str = str .. (' in year %d'):format(incident.event_year) + if incident.criminal then + local killer = df.unit.find(incident.criminal) + if killer then + str = str .. (', killed by the %s'):format(get_race_name(killer.race)) + if killer.name.has_name then + str = str .. (' %s'):format(dfhack.translation.translateName(dfhack.units.getVisibleName(killer))) + end + end + end + end + return str +end + local CATEGORY_MAP = { -- food and food-related DRINK='food', DRINK2='food', FOOD='food', MEAT='food', FISH='food', @@ -108,9 +206,8 @@ end local function on_unit_death(unit_id) local unit = df.unit.find(unit_id) if not unit then return end - local name = dfhack.units.getReadableName(unit) local date = format_date(df.global.cur_year, df.global.cur_year_tick) - add_entry(string.format('%s: Death of %s', date, name)) + add_entry(string.format('%s: %s', date, format_death_text(unit))) end local function on_item_created(item_id) @@ -151,7 +248,7 @@ local pending_artifact_report local function on_report(report_id) local rep = df.report.find(report_id) if not rep or not rep.flags.announcement then return end - local text = dfhack.df2console(rep.text) + local text = sanitize(rep.text) if pending_artifact_report then if text:find(' offers it to ') then local date = format_date(df.global.cur_year, df.global.cur_year_tick) @@ -166,6 +263,25 @@ local function on_report(report_id) end if text:find(' has created ') then pending_artifact_report = text + return + end + + if state.log_masterworks and text:lower():find('has created a master') then + local date = format_date(df.global.cur_year, df.global.cur_year_tick) + add_entry(string.format('%s: %s', date, text)) + return + end + + -- other notable announcements + local date = format_date(df.global.cur_year, df.global.cur_year_tick) + if text:find('The enemy have come') then + add_entry(string.format('%s: %s', date, text)) + elseif text:find(' has bestowed the name ') then + add_entry(string.format('%s: %s', date, text)) + elseif text:find(' has been found dead') then + add_entry(string.format('%s: %s', date, text)) + elseif text:find('Mission Report') then + add_entry(string.format('%s: %s', date, text)) end end -- legacy scanning functions for artifacts and invasions have been removed in @@ -242,6 +358,20 @@ elseif cmd == 'disable' then elseif cmd == 'clear' then state.entries = {} persist_state() +elseif cmd == 'masterworks' then + local sub = args[2] + if sub == 'enable' then + state.log_masterworks = true + elseif sub == 'disable' then + state.log_masterworks = false + else + print(string.format('Masterwork logging is currently %s.', + state.log_masterworks and 'enabled' or 'disabled')) + return + end + persist_state() +elseif cmd == 'export' then + export_chronicle(args[2]) elseif cmd == 'print' then local count = tonumber(args[2]) or 25 if #state.entries == 0 then diff --git a/docs/chronicle.rst b/docs/chronicle.rst index aa333a411..6104acbf6 100644 --- a/docs/chronicle.rst +++ b/docs/chronicle.rst @@ -6,10 +6,13 @@ chronicle :tags: fort gameplay This tool automatically records notable events in a chronicle that is stored -with your save. Unit deaths, artifact creation events, invasions, and yearly -totals of crafted items are recorded. Artifact entries now include the full -announcement text from the game, complete with item descriptions and special -characters rendered just as they appear in the in-game logs. +with your save. Unit deaths now include the cause of death as well as any +titles, nicknames, or noble positions held by the fallen. Artifact creation +events, invasions, mission reports, and yearly totals of crafted items are also +recorded. Announcements for masterwork creations can be toggled on or off +and are enabled by default. Artifact entries include the full announcement text +from the game, and output text is sanitized so that any special characters are +replaced with simple Latin equivalents. Usage ----- @@ -21,6 +24,8 @@ Usage chronicle print [count] chronicle summary chronicle clear + chronicle masterworks + chronicle export [filename] ``chronicle enable`` Start recording events in the current fortress. @@ -34,3 +39,9 @@ Usage Show yearly totals of created items by category (non-artifact items only). ``chronicle clear`` Delete the chronicle. +``chronicle masterworks`` + Enable or disable logging of masterwork creation announcements. When run + with no argument, displays the current setting. +``chronicle export`` + Write all recorded events to a text file. If ``filename`` is omitted, the + output is saved as ``chronicle.txt`` in your save folder. From a827df40616eff020fcd4120cbbe19d8abfd6acc Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sat, 19 Jul 2025 23:38:06 +0200 Subject: [PATCH 19/93] Sanitize chronicle output --- chronicle.lua | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/chronicle.lua b/chronicle.lua index 592affdd2..ca47aa718 100644 --- a/chronicle.lua +++ b/chronicle.lua @@ -94,6 +94,14 @@ local function sanitize(text) -- strip control characters that may have leaked through str = str:gsub('[%z\1-\31]', '') str = transliterate(str) + -- strip quality wrappers from item names + -- e.g. -item-, +item+, *item*, ≡item≡, ☼item☼, «item» + str = str:gsub('%-([^%-]+)%-', '%1') + str = str:gsub('%+([^%+]+)%+', '%1') + str = str:gsub('%*([^%*]+)%*', '%1') + str = str:gsub('≡([^≡]+)≡', '%1') + str = str:gsub('☼([^☼]+)☼', '%1') + str = str:gsub('«([^»]+)»', '%1') return str end From aa46986a50c018e1207d1bf3d2ef45b42e56ef2c Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sat, 19 Jul 2025 23:53:56 +0200 Subject: [PATCH 20/93] sanitize chronicle entries --- chronicle.lua | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/chronicle.lua b/chronicle.lua index 592affdd2..b6c8bb47f 100644 --- a/chronicle.lua +++ b/chronicle.lua @@ -94,6 +94,19 @@ local function sanitize(text) -- strip control characters that may have leaked through str = str:gsub('[%z\1-\31]', '') str = transliterate(str) + -- strip quality wrappers from item names + -- e.g. -item-, +item+, *item*, ≡item≡, ☼item☼, «item» + str = str:gsub('%-([^%-]+)%-', '%1') + str = str:gsub('%+([^%+]+)%+', '%1') + str = str:gsub('%*([^%*]+)%*', '%1') + str = str:gsub('≡([^≡]+)≡', '%1') + str = str:gsub('☼([^☼]+)☼', '%1') + str = str:gsub('«([^»]+)»', '%1') + -- remove any stray wrapper characters that might remain + str = str:gsub('[☼≡«»]', '') + -- strip any remaining characters outside of latin letters, digits, and + -- basic punctuation + str = str:gsub("[^A-Za-z0-9%s%.:,;!'\"%?()%+%-]", '') return str end From e187aedf1cd9ddef036ceec38fbf8d92338503f2 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sun, 20 Jul 2025 00:31:41 +0200 Subject: [PATCH 21/93] Optimization of chronicle.lua Help function added --- chronicle.lua | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/chronicle.lua b/chronicle.lua index 1f1dd5a1e..279b36515 100644 --- a/chronicle.lua +++ b/chronicle.lua @@ -1,10 +1,29 @@ -- Chronicles fortress events: unit deaths, item creation, and invasions --@module = true ---@enable = true local eventful = require('plugins.eventful') local utils = require('utils') +local help = [====[ +chronicle +======== + +Chronicles fortress events: unit deaths, item creation, and invasions + +Usage: + chronicle enable + chronicle disable + + chronicle [print] - prints 25 last recorded events + chronicle print [number] - prints last [number] recorded events + chronicle export - saves current chronicle to a txt file + chronicle clear - erases current chronicle (DANGER) + + chronicle summary - shows how much items were produced per category in each year + + chronicle masterworks [enable|disable] - enables or disables logging of masterful crafted items events +]====] + local GLOBAL_KEY = 'chronicle' local function get_default_state() @@ -257,7 +276,7 @@ local function on_invasion(invasion_id) add_entry(string.format('%s: Invasion started', date)) end --- capture artifact announcements verbatim from reports +-- capture artifact announcements from reports local pending_artifact_report local function on_report(report_id) local rep = df.report.find(report_id) @@ -298,8 +317,6 @@ local function on_report(report_id) add_entry(string.format('%s: %s', date, text)) end end --- legacy scanning functions for artifacts and invasions have been removed in --- favor of event-based tracking. the main loop is no longer needed. local function do_enable() state.enabled = true @@ -328,7 +345,6 @@ local function load_state() end -- State change hook - dfhack.onStateChange[GLOBAL_KEY] = function(sc) if sc == SC_MAP_UNLOADED then eventful.onUnitDeath[GLOBAL_KEY] = nil @@ -414,7 +430,7 @@ elseif cmd == 'summary' then print(string.format('Year %d: %s', year, table.concat(parts, ', '))) end else - print(dfhack.script_help()) + print(help) end persist_state() From c6cd65a590857ef4722df9b105d5942ff4c9a4a5 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sun, 20 Jul 2025 00:48:26 +0200 Subject: [PATCH 22/93] Refactor scripts and update docs --- chronicle.lua | 115 ++++++++++++++++++++++-------------------- docs/chronicle.rst | 8 +++ docs/holy-war.rst | 16 +++++- docs/need-acquire.rst | 34 +++++++++++++ need-acquire.lua | 29 ++++++----- 5 files changed, 133 insertions(+), 69 deletions(-) create mode 100644 docs/need-acquire.rst diff --git a/chronicle.lua b/chronicle.lua index 279b36515..6398a5d4b 100644 --- a/chronicle.lua +++ b/chronicle.lua @@ -371,66 +371,69 @@ if dfhack.isMapLoaded() and dfhack.world.isFortressMode() then end end -if dfhack_flags.module then return end - -if not dfhack.world.isFortressMode() or not dfhack.isMapLoaded() then - qerror('chronicle requires a loaded fortress map') -end +local function main(args) + if not dfhack.world.isFortressMode() or not dfhack.isMapLoaded() then + qerror('chronicle requires a loaded fortress map') + end -load_state() -local args = {...} -local cmd = args[1] or 'print' + load_state() + local cmd = args[1] or 'print' -if cmd == 'enable' then - do_enable() -elseif cmd == 'disable' then - do_disable() -elseif cmd == 'clear' then - state.entries = {} - persist_state() -elseif cmd == 'masterworks' then - local sub = args[2] - if sub == 'enable' then - state.log_masterworks = true - elseif sub == 'disable' then - state.log_masterworks = false - else - print(string.format('Masterwork logging is currently %s.', - state.log_masterworks and 'enabled' or 'disabled')) - return - end - persist_state() -elseif cmd == 'export' then - export_chronicle(args[2]) -elseif cmd == 'print' then - local count = tonumber(args[2]) or 25 - if #state.entries == 0 then - print('Chronicle is empty.') - else - local start_idx = math.max(1, #state.entries - count + 1) - for i = start_idx, #state.entries do - print(state.entries[i]) + if cmd == 'enable' then + do_enable() + elseif cmd == 'disable' then + do_disable() + elseif cmd == 'clear' then + state.entries = {} + persist_state() + elseif cmd == 'masterworks' then + local sub = args[2] + if sub == 'enable' then + state.log_masterworks = true + elseif sub == 'disable' then + state.log_masterworks = false + else + print(string.format('Masterwork logging is currently %s.', + state.log_masterworks and 'enabled' or 'disabled')) + return end - end -elseif cmd == 'summary' then - local years = {} - for year in pairs(state.item_counts) do table.insert(years, year) end - table.sort(years) - if #years == 0 then - print('No item creation records.') - return - end - for _,year in ipairs(years) do - local counts = state.item_counts[year] - local parts = {} - for cat,count in pairs(counts) do - table.insert(parts, string.format('%d %s', count, cat)) + persist_state() + elseif cmd == 'export' then + export_chronicle(args[2]) + elseif cmd == 'print' then + local count = tonumber(args[2]) or 25 + if #state.entries == 0 then + print('Chronicle is empty.') + else + local start_idx = math.max(1, #state.entries - count + 1) + for i = start_idx, #state.entries do + print(state.entries[i]) + end + end + elseif cmd == 'summary' then + local years = {} + for year in pairs(state.item_counts) do table.insert(years, year) end + table.sort(years) + if #years == 0 then + print('No item creation records.') + return end - table.sort(parts) - print(string.format('Year %d: %s', year, table.concat(parts, ', '))) + for _,year in ipairs(years) do + local counts = state.item_counts[year] + local parts = {} + for cat,count in pairs(counts) do + table.insert(parts, string.format('%d %s', count, cat)) + end + table.sort(parts) + print(string.format('Year %d: %s', year, table.concat(parts, ', '))) + end + else + print(help) end -else - print(help) + + persist_state() end -persist_state() +if not dfhack_flags.module then + main({...}) +end diff --git a/docs/chronicle.rst b/docs/chronicle.rst index 6104acbf6..71b6d7839 100644 --- a/docs/chronicle.rst +++ b/docs/chronicle.rst @@ -45,3 +45,11 @@ Usage ``chronicle export`` Write all recorded events to a text file. If ``filename`` is omitted, the output is saved as ``chronicle.txt`` in your save folder. + +Examples +-------- + +``chronicle print 10`` + Show the 10 most recent chronicle entries. +``chronicle summary`` + Display yearly summaries of items created in the fort. diff --git a/docs/holy-war.rst b/docs/holy-war.rst index 3c9ceecf2..5c8fa3c70 100644 --- a/docs/holy-war.rst +++ b/docs/holy-war.rst @@ -21,7 +21,7 @@ Usage :: - holy-war [--dry-run] + holy-war [--dry-run] When run without options, wars are declared immediately on all qualifying civilizations and an announcement is displayed. With @@ -30,3 +30,17 @@ affected without actually changing diplomacy. Each message also notes whether the conflict arises from disjoint spheres of influence or a religious persecution grudge and lists the conflicting spheres when appropriate. + +Examples +-------- + +``holy-war`` + Immediately declare war on all qualifying civilizations. +``holy-war --dry-run`` + Show which civilizations would be targeted without changing diplomacy. + +Options +------- + +``--dry-run`` + List potential targets without declaring war. diff --git a/docs/need-acquire.rst b/docs/need-acquire.rst new file mode 100644 index 000000000..eeaaae0ef --- /dev/null +++ b/docs/need-acquire.rst @@ -0,0 +1,34 @@ +need-acquire +============ + +.. dfhack-tool:: + :summary: Give trinkets to citizens to satisfy the Acquire Object need. + :tags: fort gameplay happiness + +Assigns free jewelry items to dwarves who have a strong ``Acquire Object`` need. +The script searches for unowned earrings, rings, amulets, and bracelets and +assigns them to dwarves whose focus level for the need falls below a configurable +threshold. + +Usage +----- + +``need-acquire [-t ]`` + Give trinkets to all dwarves whose focus level is below ``-``. + The default threshold is ``-3000``. + +Examples +-------- + +``need-acquire`` + Use the default focus threshold of ``-3000``. +``need-acquire -t 2000`` + Fulfill the need for dwarves whose focus drops below ``-2000``. + +Options +------- + +``-t`` ```` + Focus level below which the need is considered unmet. +``-help`` + Show the help text. diff --git a/need-acquire.lua b/need-acquire.lua index e7d00fb5a..ce81818f6 100644 --- a/need-acquire.lua +++ b/need-acquire.lua @@ -1,7 +1,8 @@ -- Assign trinkets to citizens so they can satisfy the "Acquire Object" need. -- Derived from an old Bay12 forums script and updated for modern DFHack. +--@module = true -local HELP = [=[ +local help = [=[ need-acquire ============ Assign trinkets to citizens who have a strong "Acquire Object" need. @@ -18,20 +19,10 @@ Options: local utils = require('utils') local valid_args = utils.invert{'help', 't'} -local args = utils.processArgs({...}, valid_args) local ACQUIRE_NEED_ID = df.need_type.AcquireObject local acquire_threshold = -3000 -if args.help then - print(HELP) - return -end - -if args.t then - acquire_threshold = -tonumber(args.t) -end - local function get_citizens() local result = {} for _, unit in ipairs(dfhack.units.getCitizens(true)) do @@ -96,5 +87,19 @@ local function give_items() end end -give_items() +local function main(args) + args = utils.processArgs(args, valid_args) + if args.help then + print(help) + return + end + if args.t then + acquire_threshold = -tonumber(args.t) + end + give_items() +end + +if not dfhack_flags.module then + main({...}) +end From bcd1d94439df73a72d1fa19f42947bc302ccc973 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sun, 20 Jul 2025 01:34:23 +0200 Subject: [PATCH 23/93] Chronicle.lua new events and text changes from notification style Added new events connected to invasion of forgotten beast and other fun stuff. Added weather events. Added "You have found" events. Added text replacements everywhere with "you" Ticks to check moved from 1 to 10 --- chronicle.lua | 58 +++++++++++++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/chronicle.lua b/chronicle.lua index 6398a5d4b..9caf8c4fd 100644 --- a/chronicle.lua +++ b/chronicle.lua @@ -168,17 +168,6 @@ local function describe_unit(unit) if unit.name.nickname ~= '' and not name:find(unit.name.nickname, 1, true) then name = name:gsub(unit.name.first_name, unit.name.first_name .. ' "' .. unit.name.nickname .. '"') end - local titles = {} - local prof = dfhack.units.getProfessionName(unit) - if prof and prof ~= '' then table.insert(titles, prof) end - for _, np in ipairs(dfhack.units.getNoblePositions(unit) or {}) do - if np.position and np.position.name and np.position.name[0] ~= '' then - table.insert(titles, np.position.name[0]) - end - end - if #titles > 0 then - name = name .. ' (' .. table.concat(titles, ', ') .. ')' - end return name end @@ -188,7 +177,6 @@ local function format_death_text(unit) str = str .. ' ' .. death_string(unit.counters.death_cause) local incident = df.incident.find(unit.counters.death_id) if incident then - str = str .. (' in year %d'):format(incident.event_year) if incident.criminal then local killer = df.unit.find(incident.criminal) if killer then @@ -283,7 +271,7 @@ local function on_report(report_id) if not rep or not rep.flags.announcement then return end local text = sanitize(rep.text) if pending_artifact_report then - if text:find(' offers it to ') then + if text:find(' offers it to ') or text:find(' claims it ') then local date = format_date(df.global.cur_year, df.global.cur_year_tick) add_entry(string.format('%s: %s %s', date, pending_artifact_report, text)) pending_artifact_report = nil @@ -305,24 +293,44 @@ local function on_report(report_id) return end - -- other notable announcements local date = format_date(df.global.cur_year, df.global.cur_year_tick) - if text:find('The enemy have come') then - add_entry(string.format('%s: %s', date, text)) - elseif text:find(' has bestowed the name ') then - add_entry(string.format('%s: %s', date, text)) - elseif text:find(' has been found dead') then - add_entry(string.format('%s: %s', date, text)) - elseif text:find('Mission Report') then - add_entry(string.format('%s: %s', date, text)) + local msg = transform_notification(text) + + if msg:find('The enemy have come') then + add_entry(string.format('%s: %s', date, msg)) + elseif msg:find(' has bestowed the name ') then + add_entry(string.format('%s: %s', date, msg)) + elseif msg:find(' has been found dead') then + add_entry(string.format('%s: %s', date, msg)) + elseif msg:find('Dwarves have ') then + add_entry(string.format('%s: %s', date, msg)) + elseif msg:find(' has come') then + add_entry(string.format('%s: %s', date, msg)) + elseif msg:find(' upon you') then + add_entry(string.format('%s: %s', date, msg)) + elseif msg:find('It is ') then + add_entry(string.format('%s: %s', date, msg)) + end +end + +local function transform_notification(text) + -- "You have " >> "Dwarves have " + if text:sub(1, 9) == "You have " then + text = "Dwarves have " .. text:sub(10) end + + -- "Now you will know why you fear the night." >> "Gods have mercy!" + text = text:gsub("Now you will know why you fear the night%.", "Gods have mercy!") + + return text end local function do_enable() state.enabled = true - eventful.enableEvent(eventful.eventType.ITEM_CREATED, 1) - eventful.enableEvent(eventful.eventType.INVASION, 1) - eventful.enableEvent(eventful.eventType.REPORT, 1) + eventful.enableEvent(eventful.eventType.ITEM_CREATED, 10) + eventful.enableEvent(eventful.eventType.INVASION, 10) + eventful.enableEvent(eventful.eventType.REPORT, 10) + eventful.enableEvent(eventful.eventType.UNIT_DEATH, 10) eventful.onUnitDeath[GLOBAL_KEY] = on_unit_death eventful.onItemCreated[GLOBAL_KEY] = on_item_created eventful.onInvasion[GLOBAL_KEY] = on_invasion From a811f4e0deaff68fa5a38196095bf265b84de894 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sun, 20 Jul 2025 14:48:38 +0200 Subject: [PATCH 24/93] Ignore wildlife deaths in chronicle --- chronicle.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/chronicle.lua b/chronicle.lua index 9caf8c4fd..e1730da85 100644 --- a/chronicle.lua +++ b/chronicle.lua @@ -227,6 +227,7 @@ end local function on_unit_death(unit_id) local unit = df.unit.find(unit_id) if not unit then return end + if dfhack.units.isWildlife(unit) then return end local date = format_date(df.global.cur_year, df.global.cur_year_tick) add_entry(string.format('%s: %s', date, format_death_text(unit))) end From 5f3bea411faea5daa596c265c9c0af4a70ed78f1 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sun, 20 Jul 2025 15:09:41 +0200 Subject: [PATCH 25/93] Add scrollable chronicle viewer --- chronicle.lua | 6 +++++ docs/chronicle.rst | 3 +++ gui/chronicle.lua | 66 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 gui/chronicle.lua diff --git a/chronicle.lua b/chronicle.lua index e1730da85..b9943a13d 100644 --- a/chronicle.lua +++ b/chronicle.lua @@ -419,6 +419,12 @@ local function main(args) print(state.entries[i]) end end + elseif cmd == 'view' then + if #state.entries == 0 then + print('Chronicle is empty.') + else + reqscript('gui/chronicle').show() + end elseif cmd == 'summary' then local years = {} for year in pairs(state.item_counts) do table.insert(years, year) end diff --git a/docs/chronicle.rst b/docs/chronicle.rst index 71b6d7839..241e9d542 100644 --- a/docs/chronicle.rst +++ b/docs/chronicle.rst @@ -26,6 +26,7 @@ Usage chronicle clear chronicle masterworks chronicle export [filename] + chronicle view ``chronicle enable`` Start recording events in the current fortress. @@ -45,6 +46,8 @@ Usage ``chronicle export`` Write all recorded events to a text file. If ``filename`` is omitted, the output is saved as ``chronicle.txt`` in your save folder. +``chronicle view`` + Display the full chronicle in a scrollable window. Examples -------- diff --git a/gui/chronicle.lua b/gui/chronicle.lua new file mode 100644 index 000000000..2dfa7b827 --- /dev/null +++ b/gui/chronicle.lua @@ -0,0 +1,66 @@ +-- GUI viewer for chronicle entries +--@module=true + +local chronicle = reqscript('chronicle') +local gui = require('gui') +local widgets = require('gui.widgets') + +ChronicleView = defclass(ChronicleView, gui.FramedScreen) +ChronicleView.ATTRS{ + frame_title='Chronicle', + frame_style=gui.GREY_LINE_FRAME, + frame_width=60, + frame_height=20, + frame_inset=1, +} + +function ChronicleView:init() + self.entries = chronicle.state and chronicle.state.entries or {} + self.start = 1 + self.start_min = 1 + self.start_max = math.max(1, #self.entries - self.frame_height + 1) +end + +function ChronicleView:onRenderBody(dc) + for i=self.start, math.min(#self.entries, self.start + self.frame_height - 1) do + dc:string(self.entries[i]):newline() + end + dc:pen(COLOR_LIGHTCYAN) + if self.start > self.start_min then + dc:seek(self.frame_width-1,0):char(24) + end + if self.start < self.start_max then + dc:seek(self.frame_width-1,self.frame_height-1):char(25) + end +end + +function ChronicleView:onInput(keys) + if keys.LEAVESCREEN or keys.SELECT then + self:dismiss() + view = nil + elseif keys.STANDARDSCROLL_UP then + self.start = math.max(self.start_min, self.start - 1) + elseif keys.STANDARDSCROLL_DOWN then + self.start = math.min(self.start_max, self.start + 1) + elseif keys.STANDARDSCROLL_PAGEUP then + self.start = math.max(self.start_min, self.start - self.frame_height) + elseif keys.STANDARDSCROLL_PAGEDOWN then + self.start = math.min(self.start_max, self.start + self.frame_height) + elseif keys.STANDARDSCROLL_TOP then + self.start = self.start_min + elseif keys.STANDARDSCROLL_BOTTOM then + self.start = self.start_max + else + ChronicleView.super.onInput(self, keys) + end +end + +function show() + view = view and view:raise() or ChronicleView{} + view:show() +end + +if not dfhack_flags.module then + show() +end + From 18d804bc26a8e5bcf67e475eacc3aaea57530082 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sun, 20 Jul 2025 15:25:22 +0200 Subject: [PATCH 26/93] Preserve trimmed chronicle entries --- chronicle.lua | 79 +++++++++++++++++++++++++++++++++++++--------- docs/chronicle.rst | 8 +++++ gui/chronicle.lua | 3 +- 3 files changed, 73 insertions(+), 17 deletions(-) diff --git a/chronicle.lua b/chronicle.lua index b9943a13d..f9e8daf24 100644 --- a/chronicle.lua +++ b/chronicle.lua @@ -11,20 +11,23 @@ chronicle Chronicles fortress events: unit deaths, item creation, and invasions Usage: - chronicle enable - chronicle disable - - chronicle [print] - prints 25 last recorded events - chronicle print [number] - prints last [number] recorded events - chronicle export - saves current chronicle to a txt file - chronicle clear - erases current chronicle (DANGER) - - chronicle summary - shows how much items were produced per category in each year - - chronicle masterworks [enable|disable] - enables or disables logging of masterful crafted items events + chronicle enable + chronicle disable + + chronicle [print] - prints 25 last recorded events + chronicle print [number] - prints last [number] recorded events + chronicle long - prints the full chronicle + chronicle export - saves current chronicle to a txt file + chronicle clear - erases current chronicle (DANGER) + + chronicle summary - shows how much items were produced per category in each year + + chronicle masterworks [enable|disable] - enables or disables logging of masterful crafted items events ]====] local GLOBAL_KEY = 'chronicle' +local MAX_LOG_CHARS = 2^15 -- trim chronicle to most recent ~32KB of text +local FULL_LOG_PATH = dfhack.getSavePath() .. '/chronicle_full.txt' local function get_default_state() return { @@ -130,8 +133,44 @@ local function sanitize(text) return str end +local function read_external_entries() + local f = io.open(FULL_LOG_PATH, 'r') + if not f then return {} end + local lines = {} + for line in f:lines() do table.insert(lines, line) end + f:close() + return lines +end + +local function get_full_entries() + local entries = read_external_entries() + for _,e in ipairs(state.entries) do table.insert(entries, e) end + return entries +end + +local function trim_entries() + local total = 0 + local start_idx = #state.entries + while start_idx > 0 and total <= MAX_LOG_CHARS do + total = total + #state.entries[start_idx] + 1 + start_idx = start_idx - 1 + end + if start_idx > 0 then + local old = {} + for i=1,start_idx do table.insert(old, table.remove(state.entries, 1)) end + local ok, f = pcall(io.open, FULL_LOG_PATH, 'a') + if ok and f then + for _,e in ipairs(old) do f:write(e, '\n') end + f:close() + else + qerror('Cannot open file for writing: ' .. FULL_LOG_PATH) + end + end +end + local function add_entry(text) table.insert(state.entries, sanitize(text)) + trim_entries() persist_state() end @@ -141,6 +180,9 @@ local function export_chronicle(path) if not ok or not f then qerror('Cannot open file for writing: ' .. path) end + for _,entry in ipairs(read_external_entries()) do + f:write(entry, '\n') + end for _,entry in ipairs(state.entries) do f:write(entry, '\n') end @@ -307,9 +349,9 @@ local function on_report(report_id) add_entry(string.format('%s: %s', date, msg)) elseif msg:find(' has come') then add_entry(string.format('%s: %s', date, msg)) - elseif msg:find(' upon you') then + elseif msg:find(' upon you') then add_entry(string.format('%s: %s', date, msg)) - elseif msg:find('It is ') then + elseif msg:find('It is ') then add_entry(string.format('%s: %s', date, msg)) end end @@ -331,7 +373,7 @@ local function do_enable() eventful.enableEvent(eventful.eventType.ITEM_CREATED, 10) eventful.enableEvent(eventful.eventType.INVASION, 10) eventful.enableEvent(eventful.eventType.REPORT, 10) - eventful.enableEvent(eventful.eventType.UNIT_DEATH, 10) + eventful.enableEvent(eventful.eventType.UNIT_DEATH, 10) eventful.onUnitDeath[GLOBAL_KEY] = on_unit_death eventful.onItemCreated[GLOBAL_KEY] = on_item_created eventful.onInvasion[GLOBAL_KEY] = on_invasion @@ -409,6 +451,13 @@ local function main(args) persist_state() elseif cmd == 'export' then export_chronicle(args[2]) + elseif cmd == 'long' then + local entries = get_full_entries() + if #entries == 0 then + print('Chronicle is empty.') + else + for _,entry in ipairs(entries) do print(entry) end + end elseif cmd == 'print' then local count = tonumber(args[2]) or 25 if #state.entries == 0 then @@ -420,7 +469,7 @@ local function main(args) end end elseif cmd == 'view' then - if #state.entries == 0 then + if #get_full_entries() == 0 then print('Chronicle is empty.') else reqscript('gui/chronicle').show() diff --git a/docs/chronicle.rst b/docs/chronicle.rst index 241e9d542..90354ea5c 100644 --- a/docs/chronicle.rst +++ b/docs/chronicle.rst @@ -14,6 +14,10 @@ and are enabled by default. Artifact entries include the full announcement text from the game, and output text is sanitized so that any special characters are replaced with simple Latin equivalents. +The chronicle file is trimmed to about 32KB of the most recent text so that it +doesn't exceed DFHack's persistence limits. Trimmed entries are automatically +appended to ``chronicle_full.txt`` in your save folder so nothing is lost. + Usage ----- @@ -22,6 +26,7 @@ Usage chronicle enable chronicle disable chronicle print [count] + chronicle long chronicle summary chronicle clear chronicle masterworks @@ -46,6 +51,9 @@ Usage ``chronicle export`` Write all recorded events to a text file. If ``filename`` is omitted, the output is saved as ``chronicle.txt`` in your save folder. +``chronicle long`` + Print the entire chronicle, including older entries stored in + ``chronicle_full.txt``. ``chronicle view`` Display the full chronicle in a scrollable window. diff --git a/gui/chronicle.lua b/gui/chronicle.lua index 2dfa7b827..ba9fbe234 100644 --- a/gui/chronicle.lua +++ b/gui/chronicle.lua @@ -15,7 +15,7 @@ ChronicleView.ATTRS{ } function ChronicleView:init() - self.entries = chronicle.state and chronicle.state.entries or {} + self.entries = chronicle.get_full_entries() self.start = 1 self.start_min = 1 self.start_max = math.max(1, #self.entries - self.frame_height + 1) @@ -63,4 +63,3 @@ end if not dfhack_flags.module then show() end - From ce57986cccbf3c3fef75b179417077bae37370dc Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sun, 20 Jul 2025 15:26:39 +0200 Subject: [PATCH 27/93] Add multihaul script --- docs/multihaul.rst | 23 ++++++++++ multihaul.lua | 108 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 docs/multihaul.rst create mode 100644 multihaul.lua diff --git a/docs/multihaul.rst b/docs/multihaul.rst new file mode 100644 index 000000000..011112c8e --- /dev/null +++ b/docs/multihaul.rst @@ -0,0 +1,23 @@ +multihaul +========= + +.. dfhack-tool:: + :summary: Haulers gather multiple nearby items when using bags or wheelbarrows. + :tags: fort productivity items + +This tool allows dwarves to collect several adjacent items at once when +performing hauling jobs with a bag or wheelbarrow. When enabled, new +``StoreItemInStockpile`` jobs will automatically attach up to four additional +items found within one tile of the original item so they can be hauled in a +single trip. + +Usage +----- + +:: + + multihaul enable + multihaul disable + multihaul status + +The script can also be enabled persistently with ``enable multihaul``. diff --git a/multihaul.lua b/multihaul.lua new file mode 100644 index 000000000..a4da12c6a --- /dev/null +++ b/multihaul.lua @@ -0,0 +1,108 @@ +-- Allow haulers to pick up multiple nearby items when using bags or wheelbarrows +--@module = true +--@enable = true + +local eventful = require('plugins.eventful') + +local GLOBAL_KEY = 'multihaul' + +enabled = enabled or false + +function isEnabled() + return enabled +end + +local function persist_state() + dfhack.persistent.saveSiteData(GLOBAL_KEY, {enabled=enabled}) +end + +local function load_state() + local data = dfhack.persistent.getSiteData(GLOBAL_KEY, {}) + enabled = data.enabled or false +end + +local function add_nearby_items(job) + if #job.items == 0 then return end + local container + for _,jitem in ipairs(job.items) do + if jitem.item and (jitem.item:isWheelbarrow() or (jitem.item.flags.container and jitem.item:isBag())) then + container = jitem.item + break + end + end + if not container then return end + + local target = job.items[0].item + if not target then return end + local x,y,z = dfhack.items.getPosition(target) + if not x then return end + + local count = 0 + for _,it in ipairs(df.global.world.items.other.IN_PLAY) do + if it ~= target and not it.flags.in_job and it.flags.on_ground and it.pos.z == z and math.abs(it.pos.x - x) <= 1 and math.abs(it.pos.y - y) <= 1 then + dfhack.job.attachJobItem(job, it, df.job_role_type.Reagent, -1, -1) + count = count + 1 + if count >= 4 then break end + end + end +end + +local function on_new_job(job) + if job.job_type ~= df.job_type.StoreItemInStockpile then return end + add_nearby_items(job) +end + +local function enable(state) + enabled = state + if enabled then + eventful.onJobInitiated[GLOBAL_KEY] = on_new_job + else + eventful.onJobInitiated[GLOBAL_KEY] = nil + end + persist_state() +end + +if dfhack.internal.IN_TEST then + unit_test_hooks = {on_new_job=on_new_job, enable=enable, load_state=load_state} +end + +-- state change handler + +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_MAP_UNLOADED then + enabled = false + eventful.onJobInitiated[GLOBAL_KEY] = nil + return + end + if sc == SC_MAP_LOADED then + load_state() + if enabled then + eventful.onJobInitiated[GLOBAL_KEY] = on_new_job + end + end +end + +if dfhack_flags.module then + return +end + +local args = {...} +if dfhack_flags.enable then + if dfhack_flags.enable_state then + enable(true) + else + enable(false) + end + return +end + +local cmd = args[1] +if cmd == 'enable' then + enable(true) +elseif cmd == 'disable' then + enable(false) +elseif cmd == 'status' or not cmd then + print(enabled and 'multihaul is enabled' or 'multihaul is disabled') +else + qerror('Usage: multihaul [enable|disable|status]') +end From dd177e34c216abf113b590465094b43794d638ac Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sun, 20 Jul 2025 15:52:24 +0200 Subject: [PATCH 28/93] Refine death messages with faction-specific text --- chronicle.lua | 96 +++++++++++++++++++++++++++++++++------------- docs/chronicle.rst | 15 +++++--- 2 files changed, 79 insertions(+), 32 deletions(-) diff --git a/chronicle.lua b/chronicle.lua index b9943a13d..2a11768f2 100644 --- a/chronicle.lua +++ b/chronicle.lua @@ -11,17 +11,17 @@ chronicle Chronicles fortress events: unit deaths, item creation, and invasions Usage: - chronicle enable - chronicle disable - - chronicle [print] - prints 25 last recorded events - chronicle print [number] - prints last [number] recorded events - chronicle export - saves current chronicle to a txt file - chronicle clear - erases current chronicle (DANGER) - - chronicle summary - shows how much items were produced per category in each year - - chronicle masterworks [enable|disable] - enables or disables logging of masterful crafted items events + chronicle enable + chronicle disable + + chronicle [print] - prints 25 last recorded events + chronicle print [number] - prints last [number] recorded events + chronicle export - saves current chronicle to a txt file + chronicle clear - erases current chronicle (DANGER) + + chronicle summary - shows how much items were produced per category in each year + + chronicle masterworks [enable|disable] - enables or disables logging of masterful crafted items events ]====] local GLOBAL_KEY = 'chronicle' @@ -171,23 +171,67 @@ local function describe_unit(unit) return name end +local FORT_DEATH_NO_KILLER = { + '%s was tragically killed', + '%s met an untimely end', + '%s perished in sorrow' +} + +local FORT_DEATH_WITH_KILLER = { + '%s was murdered by %s', + '%s fell victim to %s', + '%s was slain by %s' +} + +local ENEMY_DEATH_WITH_KILLER = { + '%s granted a glorious death to the pathetic %s', + '%s dispatched the wretched %s', + '%s vanquished pitiful %s' +} + +local ENEMY_DEATH_NO_KILLER = { + '%s met their demise', + '%s found their demise', + '%s succumbed to defeat' +} + +local function random_choice(tbl) + return tbl[math.random(#tbl)] +end + local function format_death_text(unit) - local str = unit.name.has_name and '' or 'The ' - str = str .. describe_unit(unit) - str = str .. ' ' .. death_string(unit.counters.death_cause) + local victim = describe_unit(unit) local incident = df.incident.find(unit.counters.death_id) - if incident then - if incident.criminal then - local killer = df.unit.find(incident.criminal) - if killer then - str = str .. (', killed by the %s'):format(get_race_name(killer.race)) - if killer.name.has_name then - str = str .. (' %s'):format(dfhack.translation.translateName(dfhack.units.getVisibleName(killer))) - end + local killer + if incident and incident.criminal then + killer = df.unit.find(incident.criminal) + end + + if dfhack.units.isFortControlled(unit) then + if killer then + local killer_name = describe_unit(killer) + return string.format(random_choice(FORT_DEATH_WITH_KILLER), victim, killer_name) + else + return string.format(random_choice(FORT_DEATH_NO_KILLER), victim) + end + elseif dfhack.units.isInvader(unit) then + if killer then + local killer_name = describe_unit(killer) + return string.format(random_choice(ENEMY_DEATH_WITH_KILLER), killer_name, victim) + else + return string.format(random_choice(ENEMY_DEATH_NO_KILLER), victim) + end + else + local str = (unit.name.has_name and '' or 'The ') .. victim + str = str .. ' ' .. death_string(unit.counters.death_cause) + if killer then + str = str .. (', killed by the %s'):format(get_race_name(killer.race)) + if killer.name.has_name then + str = str .. (' %s'):format(dfhack.translation.translateName(dfhack.units.getVisibleName(killer))) end end + return str end - return str end local CATEGORY_MAP = { @@ -307,9 +351,9 @@ local function on_report(report_id) add_entry(string.format('%s: %s', date, msg)) elseif msg:find(' has come') then add_entry(string.format('%s: %s', date, msg)) - elseif msg:find(' upon you') then + elseif msg:find(' upon you') then add_entry(string.format('%s: %s', date, msg)) - elseif msg:find('It is ') then + elseif msg:find('It is ') then add_entry(string.format('%s: %s', date, msg)) end end @@ -331,7 +375,7 @@ local function do_enable() eventful.enableEvent(eventful.eventType.ITEM_CREATED, 10) eventful.enableEvent(eventful.eventType.INVASION, 10) eventful.enableEvent(eventful.eventType.REPORT, 10) - eventful.enableEvent(eventful.eventType.UNIT_DEATH, 10) + eventful.enableEvent(eventful.eventType.UNIT_DEATH, 10) eventful.onUnitDeath[GLOBAL_KEY] = on_unit_death eventful.onItemCreated[GLOBAL_KEY] = on_item_created eventful.onInvasion[GLOBAL_KEY] = on_invasion diff --git a/docs/chronicle.rst b/docs/chronicle.rst index 241e9d542..c1f711f05 100644 --- a/docs/chronicle.rst +++ b/docs/chronicle.rst @@ -7,12 +7,15 @@ chronicle This tool automatically records notable events in a chronicle that is stored with your save. Unit deaths now include the cause of death as well as any -titles, nicknames, or noble positions held by the fallen. Artifact creation -events, invasions, mission reports, and yearly totals of crafted items are also -recorded. Announcements for masterwork creations can be toggled on or off -and are enabled by default. Artifact entries include the full announcement text -from the game, and output text is sanitized so that any special characters are -replaced with simple Latin equivalents. +titles, nicknames, or noble positions held by the fallen. Death entries also +differentiate fortress citizens, invading enemies, and other visitors. Fortress +citizens are memorialized with tragic or murderous phrasing, while invading +enemies receive a mocking epitaph. Artifact creation events, invasions, mission +reports, and yearly totals of +crafted items are also recorded. Announcements for masterwork creations can be +toggled on or off and are enabled by default. Artifact entries include the full +announcement text from the game, and output text is sanitized so that any +special characters are replaced with simple Latin equivalents. Usage ----- From c61e2c1d773ffde2b232f8aa4966a628e3828bea Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sun, 20 Jul 2025 16:15:38 +0200 Subject: [PATCH 29/93] Add config options and debugging to multihaul --- docs/multihaul.rst | 22 +++++++++++++---- multihaul.lua | 60 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 73 insertions(+), 9 deletions(-) diff --git a/docs/multihaul.rst b/docs/multihaul.rst index 011112c8e..faf99e264 100644 --- a/docs/multihaul.rst +++ b/docs/multihaul.rst @@ -7,17 +7,31 @@ multihaul This tool allows dwarves to collect several adjacent items at once when performing hauling jobs with a bag or wheelbarrow. When enabled, new -``StoreItemInStockpile`` jobs will automatically attach up to four additional -items found within one tile of the original item so they can be hauled in a -single trip. +``StoreItemInStockpile`` jobs will automatically attach nearby items so they can +be hauled in a single trip. By default, up to four additional items within one +tile of the original item are collected. Usage ----- :: - multihaul enable + multihaul enable [] multihaul disable multihaul status + multihaul config [] The script can also be enabled persistently with ``enable multihaul``. + +Options +------- + +``--radius `` + Search this many tiles around the target item for additional items. Default + is ``1``. +``--max-items `` + Attach at most this many additional items to each hauling job. Default is + ``4``. +``--debug`` + Show debug messages via ``dfhack.gui.showAnnouncement`` when items are + attached. Use ``--no-debug`` to disable. diff --git a/multihaul.lua b/multihaul.lua index a4da12c6a..623bda7af 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -3,22 +3,34 @@ --@enable = true local eventful = require('plugins.eventful') +local utils = require('utils') local GLOBAL_KEY = 'multihaul' enabled = enabled or false +debug_enabled = debug_enabled or false +radius = radius or 1 +max_items = max_items or 4 function isEnabled() return enabled end local function persist_state() - dfhack.persistent.saveSiteData(GLOBAL_KEY, {enabled=enabled}) + dfhack.persistent.saveSiteData(GLOBAL_KEY, { + enabled=enabled, + debug_enabled=debug_enabled, + radius=radius, + max_items=max_items, + }) end local function load_state() local data = dfhack.persistent.getSiteData(GLOBAL_KEY, {}) enabled = data.enabled or false + debug_enabled = data.debug_enabled or false + radius = data.radius or 1 + max_items = data.max_items or 4 end local function add_nearby_items(job) @@ -39,12 +51,23 @@ local function add_nearby_items(job) local count = 0 for _,it in ipairs(df.global.world.items.other.IN_PLAY) do - if it ~= target and not it.flags.in_job and it.flags.on_ground and it.pos.z == z and math.abs(it.pos.x - x) <= 1 and math.abs(it.pos.y - y) <= 1 then + if it ~= target and not it.flags.in_job and it.flags.on_ground and it.pos.z == z and math.abs(it.pos.x - x) <= radius and math.abs(it.pos.y - y) <= radius then dfhack.job.attachJobItem(job, it, df.job_role_type.Reagent, -1, -1) count = count + 1 - if count >= 4 then break end + if debug_enabled then + dfhack.gui.showAnnouncement( + ('multihaul: added %s to hauling job'):format( + dfhack.items.getDescription(it, 0)), + COLOR_CYAN) + end + if count >= max_items then break end end end + if debug_enabled and count > 0 then + dfhack.gui.showAnnouncement( + ('multihaul: added %d item(s) nearby'):format(count), + COLOR_CYAN) + end end local function on_new_job(job) @@ -96,13 +119,40 @@ if dfhack_flags.enable then return end +local function parse_options(start_idx) + local i = start_idx + while i <= #args do + local a = args[i] + if a == '--debug' then + debug_enabled = true + elseif a == '--no-debug' then + debug_enabled = false + elseif a == '--radius' then + i = i + 1 + radius = tonumber(args[i]) or radius + elseif a == '--max-items' then + i = i + 1 + max_items = tonumber(args[i]) or max_items + end + i = i + 1 + end +end + local cmd = args[1] if cmd == 'enable' then + parse_options(2) enable(true) elseif cmd == 'disable' then enable(false) elseif cmd == 'status' or not cmd then - print(enabled and 'multihaul is enabled' or 'multihaul is disabled') + print((enabled and 'multihaul is enabled' or 'multihaul is disabled')) + print(('radius=%d max-items=%d debug=%s') + :format(radius, max_items, debug_enabled and 'on' or 'off')) +elseif cmd == 'config' then + parse_options(2) + persist_state() + print(('multihaul config: radius=%d max-items=%d debug=%s') + :format(radius, max_items, debug_enabled and 'on' or 'off')) else - qerror('Usage: multihaul [enable|disable|status]') + qerror('Usage: multihaul [enable|disable|status|config] [--radius N] [--max-items N] [--debug|--no-debug]') end From 969dc3b46cab8a50a74ced11937d1ff8d82ba3c0 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sun, 20 Jul 2025 17:08:48 +0200 Subject: [PATCH 30/93] chronicle: unified notification capture pattern --- chronicle.lua | 43 ++++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/chronicle.lua b/chronicle.lua index ace51f5f7..70c113a1e 100644 --- a/chronicle.lua +++ b/chronicle.lua @@ -305,6 +305,21 @@ local IGNORE_TYPES = { CORPSE=true, CORPSEPIECE=true, REMAINS=true, } +local ANNOUNCEMENT_PATTERNS = { + 'the enemy have come', + 'a vile force of darkness has arrived', + 'an ambush', + 'snatcher', + 'thief', + ' has bestowed the name ', + ' has been found dead', + 'you have ', + ' has come', + ' upon you', + 'tt is ', + 'is visiting', +} + local function get_category(item) local t = df.item_type[item:getType()] return CATEGORY_MAP[t] or 'other' @@ -379,25 +394,15 @@ local function on_report(report_id) add_entry(string.format('%s: %s', date, text)) return end - - local date = format_date(df.global.cur_year, df.global.cur_year_tick) - local msg = transform_notification(text) - - if msg:find('The enemy have come') then - add_entry(string.format('%s: %s', date, msg)) - elseif msg:find(' has bestowed the name ') then - add_entry(string.format('%s: %s', date, msg)) - elseif msg:find(' has been found dead') then - add_entry(string.format('%s: %s', date, msg)) - elseif msg:find('Dwarves have ') then - add_entry(string.format('%s: %s', date, msg)) - elseif msg:find(' has come') then - add_entry(string.format('%s: %s', date, msg)) - elseif msg:find(' upon you') then - add_entry(string.format('%s: %s', date, msg)) - elseif msg:find('It is ') then - add_entry(string.format('%s: %s', date, msg)) - end + + for _,pattern in ipairs(ANNOUNCEMENT_PATTERNS) do + if text:lower():find(pattern) then + local date = format_date(df.global.cur_year, df.global.cur_year_tick) + local msg = transform_notification(text) + add_entry(string.format('%s: %s', date, msg)) + break + end +end end local function transform_notification(text) From bb30c3533fc11255d9f772143ed58b15bb6b3737 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sun, 20 Jul 2025 17:10:45 +0200 Subject: [PATCH 31/93] Add wheelbarrow-multi script --- docs/wheelbarrow-multi.rst | 34 +++++++++ wheelbarrow-multi.lua | 153 +++++++++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 docs/wheelbarrow-multi.rst create mode 100644 wheelbarrow-multi.lua diff --git a/docs/wheelbarrow-multi.rst b/docs/wheelbarrow-multi.rst new file mode 100644 index 000000000..afeb9c952 --- /dev/null +++ b/docs/wheelbarrow-multi.rst @@ -0,0 +1,34 @@ +wheelbarrow-multi +================= + +.. dfhack-tool:: + :summary: Load multiple items into wheelbarrows with one job. + :tags: fort productivity items + +This tool allows dwarves to gather several adjacent items at once when +loading wheelbarrows. When enabled, new ``StoreItemInVehicle`` jobs will +automatically attach nearby items so they can be hauled in a single trip. +By default, up to four additional items within one tile of the original +item are collected. + +Usage +----- + +:: + + wheelbarrow-multi enable [] + wheelbarrow-multi disable + wheelbarrow-multi status + wheelbarrow-multi config [] + +Options +------- + +``--radius `` + Search this many tiles around the target item for additional items. Default + is ``1``. +``--max-items `` + Attach at most this many additional items to each job. Default is ``4``. +``--debug`` + Show debug messages via ``dfhack.gui.showAnnouncement`` when items are + attached. Use ``--no-debug`` to disable. diff --git a/wheelbarrow-multi.lua b/wheelbarrow-multi.lua new file mode 100644 index 000000000..0be17d9fc --- /dev/null +++ b/wheelbarrow-multi.lua @@ -0,0 +1,153 @@ +-- Load multiple items into wheelbarrows at once +--@module = true +--@enable = true + +local eventful = require('plugins.eventful') + +local GLOBAL_KEY = 'wheelbarrow_multi' + +enabled = enabled or false +radius = radius or 1 +max_items = max_items or 4 +debug_enabled = debug_enabled or false + +local function persist_state() + dfhack.persistent.saveSiteData(GLOBAL_KEY, { + enabled=enabled, + radius=radius, + max_items=max_items, + debug_enabled=debug_enabled, + }) +end + +local function load_state() + local data = dfhack.persistent.getSiteData(GLOBAL_KEY, {}) + enabled = data.enabled or false + radius = data.radius or 1 + max_items = data.max_items or 4 + debug_enabled = data.debug_enabled or false +end + +function isEnabled() + return enabled +end + +local function add_nearby_items(job) + if #job.items < 2 then return end + + local wheelbarrow + local target + for _,jitem in ipairs(job.items) do + if jitem.item and jitem.item:isWheelbarrow() then + wheelbarrow = jitem.item + elseif jitem.item then + target = target or jitem.item + end + end + if not wheelbarrow or not target then return end + + local x,y,z = dfhack.items.getPosition(target) + if not x then return end + + local count = 0 + for _,it in ipairs(df.global.world.items.other.IN_PLAY) do + if it ~= target and not it.flags.in_job and it.flags.on_ground and it.pos.z == z and math.abs(it.pos.x - x) <= radius and math.abs(it.pos.y - y) <= radius then + dfhack.job.attachJobItem(job, it, df.job_role_type.Reagent, -1, -1) + count = count + 1 + if debug_enabled then + dfhack.gui.showAnnouncement( + ('wheelbarrow-multi: added %s to loading job'):format( + dfhack.items.getDescription(it, 0)), + COLOR_CYAN) + end + if count >= max_items then break end + end + end + if debug_enabled and count > 0 then + dfhack.gui.showAnnouncement( + ('wheelbarrow-multi: added %d item(s) nearby'):format(count), + COLOR_CYAN) + end +end + +local function on_new_job(job) + if job.job_type ~= df.job_type.StoreItemInVehicle then return end + add_nearby_items(job) +end + +local function enable(state) + enabled = state + if enabled then + eventful.onJobInitiated[GLOBAL_KEY] = on_new_job + else + eventful.onJobInitiated[GLOBAL_KEY] = nil + end + persist_state() +end + +if dfhack.internal.IN_TEST then + unit_test_hooks = {on_new_job=on_new_job, enable=enable, load_state=load_state} +end + +-- handle state changes + +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_MAP_UNLOADED then + enabled = false + eventful.onJobInitiated[GLOBAL_KEY] = nil + return + end + if sc == SC_MAP_LOADED then + load_state() + if enabled then + eventful.onJobInitiated[GLOBAL_KEY] = on_new_job + end + end +end + +if dfhack_flags.module then return end + +local args = {...} +if dfhack_flags.enable then + enable(dfhack_flags.enable_state) + return +end + +local function parse_options(start_idx) + local i = start_idx + while i <= #args do + local a = args[i] + if a == '--debug' then + debug_enabled = true + elseif a == '--no-debug' then + debug_enabled = false + elseif a == '--radius' then + i = i + 1 + radius = tonumber(args[i]) or radius + elseif a == '--max-items' then + i = i + 1 + max_items = tonumber(args[i]) or max_items + end + i = i + 1 + end +end + +local cmd = args[1] +if cmd == 'enable' then + parse_options(2) + enable(true) +elseif cmd == 'disable' then + enable(false) +elseif cmd == 'status' or not cmd then + print((enabled and 'wheelbarrow-multi is enabled' or 'wheelbarrow-multi is disabled')) + print(('radius=%d max-items=%d debug=%s') + :format(radius, max_items, debug_enabled and 'on' or 'off')) +elseif cmd == 'config' then + parse_options(2) + persist_state() + print(('wheelbarrow-multi config: radius=%d max-items=%d debug=%s') + :format(radius, max_items, debug_enabled and 'on' or 'off')) +else + qerror('Usage: wheelbarrow-multi [enable|disable|status|config] [--radius N] [--max-items N] [--debug|--no-debug]') +end + From 4918c2530d6bec1f43be17c3b5d86dcd6b70b5ae Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sun, 20 Jul 2025 17:22:12 +0200 Subject: [PATCH 32/93] fix chronicle typos and update help --- chronicle.lua | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/chronicle.lua b/chronicle.lua index 70c113a1e..04a91136c 100644 --- a/chronicle.lua +++ b/chronicle.lua @@ -4,6 +4,9 @@ local eventful = require('plugins.eventful') local utils = require('utils') +-- ensure our random choices vary between runs +math.randomseed(os.time()) + local help = [====[ chronicle ======== @@ -19,10 +22,11 @@ Usage: chronicle long - prints the full chronicle chronicle export - saves current chronicle to a txt file chronicle clear - erases current chronicle (DANGER) + chronicle view - shows the full chronicle in a scrollable window chronicle summary - shows how much items were produced per category in each year - chronicle masterworks [enable|disable] - enables or disables logging of masterful crafted items events + chronicle masterworks [enable|disable] - enables or disables logging of masterwork creation announcements ]====] local GLOBAL_KEY = 'chronicle' @@ -110,6 +114,10 @@ local function transliterate(str) return str end +local function escape_pattern(str) + return str:gsub('([%^%$%(%)%%%.%[%]%*%+%-%?])', '%%%1') +end + local function sanitize(text) -- convert game strings to UTF-8 and remove non-printable characters local str = dfhack.df2utf(text or '') @@ -208,7 +216,8 @@ end local function describe_unit(unit) local name = dfhack.units.getReadableName(unit) if unit.name.nickname ~= '' and not name:find(unit.name.nickname, 1, true) then - name = name:gsub(unit.name.first_name, unit.name.first_name .. ' "' .. unit.name.nickname .. '"') + local pattern = escape_pattern(unit.name.first_name) + name = name:gsub(pattern, unit.name.first_name .. ' "' .. unit.name.nickname .. '"', 1) end return name end @@ -316,8 +325,8 @@ local ANNOUNCEMENT_PATTERNS = { 'you have ', ' has come', ' upon you', - 'tt is ', - 'is visiting', + ' it is ', + ' is visiting', } local function get_category(item) @@ -394,15 +403,15 @@ local function on_report(report_id) add_entry(string.format('%s: %s', date, text)) return end - - for _,pattern in ipairs(ANNOUNCEMENT_PATTERNS) do + + for _,pattern in ipairs(ANNOUNCEMENT_PATTERNS) do if text:lower():find(pattern) then - local date = format_date(df.global.cur_year, df.global.cur_year_tick) - local msg = transform_notification(text) + local date = format_date(df.global.cur_year, df.global.cur_year_tick) + local msg = transform_notification(text) add_entry(string.format('%s: %s', date, msg)) break end -end + end end local function transform_notification(text) From dbcfc0a7d5e2aa87de302ff5626fb87769fd891e Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sun, 20 Jul 2025 17:26:29 +0200 Subject: [PATCH 33/93] Delete wheelbarrow-multi.lua --- wheelbarrow-multi.lua | 153 ------------------------------------------ 1 file changed, 153 deletions(-) delete mode 100644 wheelbarrow-multi.lua diff --git a/wheelbarrow-multi.lua b/wheelbarrow-multi.lua deleted file mode 100644 index 0be17d9fc..000000000 --- a/wheelbarrow-multi.lua +++ /dev/null @@ -1,153 +0,0 @@ --- Load multiple items into wheelbarrows at once ---@module = true ---@enable = true - -local eventful = require('plugins.eventful') - -local GLOBAL_KEY = 'wheelbarrow_multi' - -enabled = enabled or false -radius = radius or 1 -max_items = max_items or 4 -debug_enabled = debug_enabled or false - -local function persist_state() - dfhack.persistent.saveSiteData(GLOBAL_KEY, { - enabled=enabled, - radius=radius, - max_items=max_items, - debug_enabled=debug_enabled, - }) -end - -local function load_state() - local data = dfhack.persistent.getSiteData(GLOBAL_KEY, {}) - enabled = data.enabled or false - radius = data.radius or 1 - max_items = data.max_items or 4 - debug_enabled = data.debug_enabled or false -end - -function isEnabled() - return enabled -end - -local function add_nearby_items(job) - if #job.items < 2 then return end - - local wheelbarrow - local target - for _,jitem in ipairs(job.items) do - if jitem.item and jitem.item:isWheelbarrow() then - wheelbarrow = jitem.item - elseif jitem.item then - target = target or jitem.item - end - end - if not wheelbarrow or not target then return end - - local x,y,z = dfhack.items.getPosition(target) - if not x then return end - - local count = 0 - for _,it in ipairs(df.global.world.items.other.IN_PLAY) do - if it ~= target and not it.flags.in_job and it.flags.on_ground and it.pos.z == z and math.abs(it.pos.x - x) <= radius and math.abs(it.pos.y - y) <= radius then - dfhack.job.attachJobItem(job, it, df.job_role_type.Reagent, -1, -1) - count = count + 1 - if debug_enabled then - dfhack.gui.showAnnouncement( - ('wheelbarrow-multi: added %s to loading job'):format( - dfhack.items.getDescription(it, 0)), - COLOR_CYAN) - end - if count >= max_items then break end - end - end - if debug_enabled and count > 0 then - dfhack.gui.showAnnouncement( - ('wheelbarrow-multi: added %d item(s) nearby'):format(count), - COLOR_CYAN) - end -end - -local function on_new_job(job) - if job.job_type ~= df.job_type.StoreItemInVehicle then return end - add_nearby_items(job) -end - -local function enable(state) - enabled = state - if enabled then - eventful.onJobInitiated[GLOBAL_KEY] = on_new_job - else - eventful.onJobInitiated[GLOBAL_KEY] = nil - end - persist_state() -end - -if dfhack.internal.IN_TEST then - unit_test_hooks = {on_new_job=on_new_job, enable=enable, load_state=load_state} -end - --- handle state changes - -dfhack.onStateChange[GLOBAL_KEY] = function(sc) - if sc == SC_MAP_UNLOADED then - enabled = false - eventful.onJobInitiated[GLOBAL_KEY] = nil - return - end - if sc == SC_MAP_LOADED then - load_state() - if enabled then - eventful.onJobInitiated[GLOBAL_KEY] = on_new_job - end - end -end - -if dfhack_flags.module then return end - -local args = {...} -if dfhack_flags.enable then - enable(dfhack_flags.enable_state) - return -end - -local function parse_options(start_idx) - local i = start_idx - while i <= #args do - local a = args[i] - if a == '--debug' then - debug_enabled = true - elseif a == '--no-debug' then - debug_enabled = false - elseif a == '--radius' then - i = i + 1 - radius = tonumber(args[i]) or radius - elseif a == '--max-items' then - i = i + 1 - max_items = tonumber(args[i]) or max_items - end - i = i + 1 - end -end - -local cmd = args[1] -if cmd == 'enable' then - parse_options(2) - enable(true) -elseif cmd == 'disable' then - enable(false) -elseif cmd == 'status' or not cmd then - print((enabled and 'wheelbarrow-multi is enabled' or 'wheelbarrow-multi is disabled')) - print(('radius=%d max-items=%d debug=%s') - :format(radius, max_items, debug_enabled and 'on' or 'off')) -elseif cmd == 'config' then - parse_options(2) - persist_state() - print(('wheelbarrow-multi config: radius=%d max-items=%d debug=%s') - :format(radius, max_items, debug_enabled and 'on' or 'off')) -else - qerror('Usage: wheelbarrow-multi [enable|disable|status|config] [--radius N] [--max-items N] [--debug|--no-debug]') -end - From e1d73ab6255217a2ec6d9d36fe41f5739c77b1fc Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sun, 20 Jul 2025 17:26:36 +0200 Subject: [PATCH 34/93] Delete multihaul.lua --- multihaul.lua | 158 -------------------------------------------------- 1 file changed, 158 deletions(-) delete mode 100644 multihaul.lua diff --git a/multihaul.lua b/multihaul.lua deleted file mode 100644 index 623bda7af..000000000 --- a/multihaul.lua +++ /dev/null @@ -1,158 +0,0 @@ --- Allow haulers to pick up multiple nearby items when using bags or wheelbarrows ---@module = true ---@enable = true - -local eventful = require('plugins.eventful') -local utils = require('utils') - -local GLOBAL_KEY = 'multihaul' - -enabled = enabled or false -debug_enabled = debug_enabled or false -radius = radius or 1 -max_items = max_items or 4 - -function isEnabled() - return enabled -end - -local function persist_state() - dfhack.persistent.saveSiteData(GLOBAL_KEY, { - enabled=enabled, - debug_enabled=debug_enabled, - radius=radius, - max_items=max_items, - }) -end - -local function load_state() - local data = dfhack.persistent.getSiteData(GLOBAL_KEY, {}) - enabled = data.enabled or false - debug_enabled = data.debug_enabled or false - radius = data.radius or 1 - max_items = data.max_items or 4 -end - -local function add_nearby_items(job) - if #job.items == 0 then return end - local container - for _,jitem in ipairs(job.items) do - if jitem.item and (jitem.item:isWheelbarrow() or (jitem.item.flags.container and jitem.item:isBag())) then - container = jitem.item - break - end - end - if not container then return end - - local target = job.items[0].item - if not target then return end - local x,y,z = dfhack.items.getPosition(target) - if not x then return end - - local count = 0 - for _,it in ipairs(df.global.world.items.other.IN_PLAY) do - if it ~= target and not it.flags.in_job and it.flags.on_ground and it.pos.z == z and math.abs(it.pos.x - x) <= radius and math.abs(it.pos.y - y) <= radius then - dfhack.job.attachJobItem(job, it, df.job_role_type.Reagent, -1, -1) - count = count + 1 - if debug_enabled then - dfhack.gui.showAnnouncement( - ('multihaul: added %s to hauling job'):format( - dfhack.items.getDescription(it, 0)), - COLOR_CYAN) - end - if count >= max_items then break end - end - end - if debug_enabled and count > 0 then - dfhack.gui.showAnnouncement( - ('multihaul: added %d item(s) nearby'):format(count), - COLOR_CYAN) - end -end - -local function on_new_job(job) - if job.job_type ~= df.job_type.StoreItemInStockpile then return end - add_nearby_items(job) -end - -local function enable(state) - enabled = state - if enabled then - eventful.onJobInitiated[GLOBAL_KEY] = on_new_job - else - eventful.onJobInitiated[GLOBAL_KEY] = nil - end - persist_state() -end - -if dfhack.internal.IN_TEST then - unit_test_hooks = {on_new_job=on_new_job, enable=enable, load_state=load_state} -end - --- state change handler - -dfhack.onStateChange[GLOBAL_KEY] = function(sc) - if sc == SC_MAP_UNLOADED then - enabled = false - eventful.onJobInitiated[GLOBAL_KEY] = nil - return - end - if sc == SC_MAP_LOADED then - load_state() - if enabled then - eventful.onJobInitiated[GLOBAL_KEY] = on_new_job - end - end -end - -if dfhack_flags.module then - return -end - -local args = {...} -if dfhack_flags.enable then - if dfhack_flags.enable_state then - enable(true) - else - enable(false) - end - return -end - -local function parse_options(start_idx) - local i = start_idx - while i <= #args do - local a = args[i] - if a == '--debug' then - debug_enabled = true - elseif a == '--no-debug' then - debug_enabled = false - elseif a == '--radius' then - i = i + 1 - radius = tonumber(args[i]) or radius - elseif a == '--max-items' then - i = i + 1 - max_items = tonumber(args[i]) or max_items - end - i = i + 1 - end -end - -local cmd = args[1] -if cmd == 'enable' then - parse_options(2) - enable(true) -elseif cmd == 'disable' then - enable(false) -elseif cmd == 'status' or not cmd then - print((enabled and 'multihaul is enabled' or 'multihaul is disabled')) - print(('radius=%d max-items=%d debug=%s') - :format(radius, max_items, debug_enabled and 'on' or 'off')) -elseif cmd == 'config' then - parse_options(2) - persist_state() - print(('multihaul config: radius=%d max-items=%d debug=%s') - :format(radius, max_items, debug_enabled and 'on' or 'off')) -else - qerror('Usage: multihaul [enable|disable|status|config] [--radius N] [--max-items N] [--debug|--no-debug]') -end From ef86f400b9197a6c4397f8d02da0df07676cedc5 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sun, 20 Jul 2025 18:17:04 +0200 Subject: [PATCH 35/93] Add stack-bodyparts script --- changelog.txt | 1 + docs/stack-bodyparts.rst | 17 +++++++++++++++++ stack-bodyparts.lua | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 docs/stack-bodyparts.rst create mode 100644 stack-bodyparts.lua diff --git a/changelog.txt b/changelog.txt index a937e2fb2..3ed354e15 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,6 +29,7 @@ Template for new versions: # Future ## New Tools +- `stack-bodyparts`: combine severed body parts into stackable piles that can be stored in bins or bags ## New Features diff --git a/docs/stack-bodyparts.rst b/docs/stack-bodyparts.rst new file mode 100644 index 000000000..3c6776d12 --- /dev/null +++ b/docs/stack-bodyparts.rst @@ -0,0 +1,17 @@ +stack-bodyparts +=============== + +.. dfhack-tool:: + :summary: Stack teeth and other body parts so they can be stored in containers. + :tags: fort productivity items + +This tool enables stacking for corpse pieces (teeth, horns, etc.) so they can be gathered in bins or bags. Existing parts in stockpiles are combined automatically. + +Usage +----- + +:: + + stack-bodyparts [all|here] [--dry-run] + +Run with ``here`` to process only the selected stockpile. ``all`` (the default) processes every stockpile on the map. The ``--dry-run`` option shows what would be combined without modifying any items. diff --git a/stack-bodyparts.lua b/stack-bodyparts.lua new file mode 100644 index 000000000..c6697e12a --- /dev/null +++ b/stack-bodyparts.lua @@ -0,0 +1,36 @@ +-- Stack severed body parts into piles that can be stored in containers +--@module = true +--[====[ +stack-bodyparts +=============== +Makes teeth and other body parts stackable so they can be gathered in bins or bags. +Running this tool will also combine existing parts in stockpiles. +]====] + +local argparse = require('argparse') + +local opts, args = {help=false, here=false, dry_run=false}, {...} +argparse.processArgsGetopt(args, { + {'h', 'help', handler=function() opts.help=true end}, + {nil, 'here', handler=function() opts.here=true end}, + {nil, 'dry-run', handler=function() opts.dry_run=true end}, +}) + +if opts.help then + print(dfhack.script_help()) + return +end + +-- mark corpse pieces as stackable +local cp_attr = df.item_type.attrs[df.item_type.CORPSEPIECE] +if not cp_attr.is_stackable then + cp_attr.is_stackable = true +end + +-- run combine to merge existing parts +local cmd = {'combine', opts.here and 'here' or 'all', '--types=parts'} +if opts.dry_run then table.insert(cmd, '--dry-run') end + +if dfhack.isMapLoaded() and df.global.gamemode == df.game_mode.DWARF then + dfhack.run_command(table.unpack(cmd)) +end From c95408da0bf1f101f858cc017dd3b9787deda06f Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sun, 20 Jul 2025 18:25:23 +0200 Subject: [PATCH 36/93] Revert "Add stack-bodyparts utility" --- changelog.txt | 1 - docs/stack-bodyparts.rst | 17 ----------------- stack-bodyparts.lua | 36 ------------------------------------ 3 files changed, 54 deletions(-) delete mode 100644 docs/stack-bodyparts.rst delete mode 100644 stack-bodyparts.lua diff --git a/changelog.txt b/changelog.txt index 3ed354e15..a937e2fb2 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,7 +29,6 @@ Template for new versions: # Future ## New Tools -- `stack-bodyparts`: combine severed body parts into stackable piles that can be stored in bins or bags ## New Features diff --git a/docs/stack-bodyparts.rst b/docs/stack-bodyparts.rst deleted file mode 100644 index 3c6776d12..000000000 --- a/docs/stack-bodyparts.rst +++ /dev/null @@ -1,17 +0,0 @@ -stack-bodyparts -=============== - -.. dfhack-tool:: - :summary: Stack teeth and other body parts so they can be stored in containers. - :tags: fort productivity items - -This tool enables stacking for corpse pieces (teeth, horns, etc.) so they can be gathered in bins or bags. Existing parts in stockpiles are combined automatically. - -Usage ------ - -:: - - stack-bodyparts [all|here] [--dry-run] - -Run with ``here`` to process only the selected stockpile. ``all`` (the default) processes every stockpile on the map. The ``--dry-run`` option shows what would be combined without modifying any items. diff --git a/stack-bodyparts.lua b/stack-bodyparts.lua deleted file mode 100644 index c6697e12a..000000000 --- a/stack-bodyparts.lua +++ /dev/null @@ -1,36 +0,0 @@ --- Stack severed body parts into piles that can be stored in containers ---@module = true ---[====[ -stack-bodyparts -=============== -Makes teeth and other body parts stackable so they can be gathered in bins or bags. -Running this tool will also combine existing parts in stockpiles. -]====] - -local argparse = require('argparse') - -local opts, args = {help=false, here=false, dry_run=false}, {...} -argparse.processArgsGetopt(args, { - {'h', 'help', handler=function() opts.help=true end}, - {nil, 'here', handler=function() opts.here=true end}, - {nil, 'dry-run', handler=function() opts.dry_run=true end}, -}) - -if opts.help then - print(dfhack.script_help()) - return -end - --- mark corpse pieces as stackable -local cp_attr = df.item_type.attrs[df.item_type.CORPSEPIECE] -if not cp_attr.is_stackable then - cp_attr.is_stackable = true -end - --- run combine to merge existing parts -local cmd = {'combine', opts.here and 'here' or 'all', '--types=parts'} -if opts.dry_run then table.insert(cmd, '--dry-run') end - -if dfhack.isMapLoaded() and df.global.gamemode == df.game_mode.DWARF then - dfhack.run_command(table.unpack(cmd)) -end From 6d8f5e3d1a982239355a567caa8ff08a0d04ca70 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sun, 20 Jul 2025 18:31:14 +0200 Subject: [PATCH 37/93] chronicle.lua: fixed multiple notification logging --- chronicle.lua | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/chronicle.lua b/chronicle.lua index 04a91136c..a97bd855f 100644 --- a/chronicle.lua +++ b/chronicle.lua @@ -4,9 +4,6 @@ local eventful = require('plugins.eventful') local utils = require('utils') --- ensure our random choices vary between runs -math.randomseed(os.time()) - local help = [====[ chronicle ======== @@ -22,11 +19,10 @@ Usage: chronicle long - prints the full chronicle chronicle export - saves current chronicle to a txt file chronicle clear - erases current chronicle (DANGER) - chronicle view - shows the full chronicle in a scrollable window chronicle summary - shows how much items were produced per category in each year - chronicle masterworks [enable|disable] - enables or disables logging of masterwork creation announcements + chronicle masterworks [enable|disable] - enables or disables logging of masterful crafted items events ]====] local GLOBAL_KEY = 'chronicle' @@ -114,10 +110,6 @@ local function transliterate(str) return str end -local function escape_pattern(str) - return str:gsub('([%^%$%(%)%%%.%[%]%*%+%-%?])', '%%%1') -end - local function sanitize(text) -- convert game strings to UTF-8 and remove non-printable characters local str = dfhack.df2utf(text or '') @@ -216,8 +208,7 @@ end local function describe_unit(unit) local name = dfhack.units.getReadableName(unit) if unit.name.nickname ~= '' and not name:find(unit.name.nickname, 1, true) then - local pattern = escape_pattern(unit.name.first_name) - name = name:gsub(pattern, unit.name.first_name .. ' "' .. unit.name.nickname .. '"', 1) + name = name:gsub(unit.name.first_name, unit.name.first_name .. ' "' .. unit.name.nickname .. '"') end return name end @@ -325,8 +316,8 @@ local ANNOUNCEMENT_PATTERNS = { 'you have ', ' has come', ' upon you', - ' it is ', - ' is visiting', + 'tt is ', + 'is visiting', } local function get_category(item) @@ -376,10 +367,22 @@ local function on_invasion(invasion_id) end -- capture artifact announcements from reports +local function transform_notification(text) + -- "You have " >> "Dwarves have " + if text:sub(1, 9) == "You have " then + text = "Dwarves have " .. text:sub(10) + end + + -- "Now you will know why you fear the night." >> "Gods have mercy!" + text = text:gsub("Now you will know why you fear the night%.", "Gods have mercy!") + + return text +end + local pending_artifact_report local function on_report(report_id) local rep = df.report.find(report_id) - if not rep or not rep.flags.announcement then return end + if not rep then return end local text = sanitize(rep.text) if pending_artifact_report then if text:find(' offers it to ') or text:find(' claims it ') then @@ -403,15 +406,15 @@ local function on_report(report_id) add_entry(string.format('%s: %s', date, text)) return end - - for _,pattern in ipairs(ANNOUNCEMENT_PATTERNS) do + + for _,pattern in ipairs(ANNOUNCEMENT_PATTERNS) do if text:lower():find(pattern) then - local date = format_date(df.global.cur_year, df.global.cur_year_tick) - local msg = transform_notification(text) + local date = format_date(df.global.cur_year, df.global.cur_year_tick) + local msg = transform_notification(text) add_entry(string.format('%s: %s', date, msg)) break end - end +end end local function transform_notification(text) From 3ee0432fd3695a2583d08573766d7deef11bff91 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sun, 20 Jul 2025 19:02:59 +0200 Subject: [PATCH 38/93] chronicle.lua: more lines, separate lines for Bloodsuckers and Great dangers Bloodsucker names would not be mentioned from now on to a spoiler reason --- chronicle.lua | 71 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 66 insertions(+), 5 deletions(-) diff --git a/chronicle.lua b/chronicle.lua index a97bd855f..c60455b68 100644 --- a/chronicle.lua +++ b/chronicle.lua @@ -216,27 +216,79 @@ end local FORT_DEATH_NO_KILLER = { '%s has tragically died', '%s met an untimely end', - '%s perished in sorrow' + '%s perished in sorrow', + '%s breathed their last in the halls of the fort', + '%s was lost to misfortune', + '%s passed into the stone', + '%s left this world too soon', + '%s faded from dwarven memory' } local FORT_DEATH_WITH_KILLER = { '%s was murdered by %s', '%s fell victim to %s', - '%s was slain by %s' + '%s was slain by %s', + '%s was claimed by the hand of %s', + '%s met their doom at the hands of %s', + '%s could not withstand the wrath of %s', + '%s was bested in deadly combat by %s', + '%s was struck down by %s' +} + +local FORT_DEATH_STRANGE = { + '%s met their mysterious demise', + '%s blood was drained completely', + '%s fate was sealed by darkest evil', + '%s disappeared without a trace', + '%s succumbed to an unnatural end', + '%s was claimed by forces unknown', + '%s fell prey to a sinister fate' } local ENEMY_DEATH_WITH_KILLER = { '%s granted a glorious death to %s', '%s dispatched the wretched %s', - '%s vanquished pitiful %s' + '%s vanquished pitiful %s', + '%s sent %s to the afterlife', + '%s laid low the hated %s', + '%s showed no mercy to %s', + '%s brought an end to %s’s rampage', + '%s struck down the enemy %s' } local ENEMY_DEATH_NO_KILLER = { '%s met their demise', '%s found their end', - '%s succumbed to death' + '%s succumbed to death', + '%s was brought low by fate', + '%s faded from existence', + '%s’s threat was ended', + '%s was erased from this world' +} + +local DANGEROUS_ENEMY_DEATH_WITH_KILLER = { + '%s has claimed victory over the dreaded %s!', + '%s has felled the fearsome %s in battle!', + '%s delivered the final blow to the infamous %s!', + '%s stood triumphant over the legendary %s!', + '%s shattered the legend of %s!', + '%s put an end to the reign of terror of %s!', + '%s conquered the monstrous %s in epic battle!', + '%s broke the might of %s!' } +local DANGEROUS_ENEMY_DEATH_NO_KILLER = { + '%s has fallen, their legend ends here.', + '%s has perished, their menace is no more.', + '%s was undone, their terror brought to an end.', + '%s vanished into myth, never to threaten again.', + '%s was swept away by fate itself.', + '%s crumbled under their own power.', + '%s slipped into oblivion, their tale finished.', + '%s faded into legend, never to rise again.' +} + + local function random_choice(tbl) return tbl[math.random(#tbl)] end @@ -250,7 +302,9 @@ local function format_death_text(unit) end if dfhack.units.isFortControlled(unit) then - if killer then + if dfhack.units.isBloodsucker(killer) then + return string.format(random_choice(FORT_DEATH_STRANGE), victim) + elseif killer then local killer_name = describe_unit(killer) return string.format(random_choice(FORT_DEATH_WITH_KILLER), victim, killer_name) else @@ -263,6 +317,13 @@ local function format_death_text(unit) else return string.format(random_choice(ENEMY_DEATH_NO_KILLER), victim) end + elseif dfhack.units.isGreatDanger(unit) then + if killer then + local killer_name = describe_unit(killer) + return string.format(random_choice(ENEMY_DEATH_WITH_KILLER), killer_name, victim) + else + return string.format(random_choice(ENEMY_DEATH_NO_KILLER), victim) + end else local str = (unit.name.has_name and '' or 'The ') .. victim str = str .. ' ' .. death_string(unit.counters.death_cause) From f5b19f3a022f5abd81b4fffef20b94a277ac2d3a Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sun, 20 Jul 2025 19:03:41 +0200 Subject: [PATCH 39/93] chronicle.lua: ticks to detect events increased --- chronicle.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/chronicle.lua b/chronicle.lua index c60455b68..6efe88e92 100644 --- a/chronicle.lua +++ b/chronicle.lua @@ -492,10 +492,10 @@ end local function do_enable() state.enabled = true - eventful.enableEvent(eventful.eventType.ITEM_CREATED, 10) - eventful.enableEvent(eventful.eventType.INVASION, 10) - eventful.enableEvent(eventful.eventType.REPORT, 10) - eventful.enableEvent(eventful.eventType.UNIT_DEATH, 10) + eventful.enableEvent(eventful.eventType.ITEM_CREATED, 100) + eventful.enableEvent(eventful.eventType.INVASION, 100) + eventful.enableEvent(eventful.eventType.REPORT, 100) + eventful.enableEvent(eventful.eventType.UNIT_DEATH, 100) eventful.onUnitDeath[GLOBAL_KEY] = on_unit_death eventful.onItemCreated[GLOBAL_KEY] = on_item_created eventful.onInvasion[GLOBAL_KEY] = on_invasion From d44dcc3cccb463385addc1aaa7974188bb5b918d Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sun, 20 Jul 2025 22:07:11 +0200 Subject: [PATCH 40/93] Restored multihaul.lua Working version!! --- multihaul.lua | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 multihaul.lua diff --git a/multihaul.lua b/multihaul.lua new file mode 100644 index 000000000..674cc74c9 --- /dev/null +++ b/multihaul.lua @@ -0,0 +1,150 @@ +-- Allow haulers to pick up multiple nearby items when using bags or wheelbarrows +--@module = true +--@enable = true + +local eventful = require('plugins.eventful') +local utils = require('utils') + +local GLOBAL_KEY = 'multihaul' + +enabled = enabled or false +debug_enabled = debug_enabled or false +radius = radius or 1 +max_items = max_items or 4 + +function isEnabled() + return enabled +end + +local function persist_state() + dfhack.persistent.saveSiteData(GLOBAL_KEY, { + enabled=enabled, + debug_enabled=debug_enabled, + radius=radius, + max_items=max_items, + }) +end + +local function load_state() + local data = dfhack.persistent.getSiteData(GLOBAL_KEY, {}) + enabled = data.enabled or false + debug_enabled = data.debug_enabled or false + radius = data.radius or 1 + max_items = data.max_items or 4 +end + +local function add_nearby_items(job) + if #job.items == 0 then return end + + local target = job.items[0].item + if not target then return end + local x,y,z = dfhack.items.getPosition(target) + if not x then return end + + local count = 0 + for _,it in ipairs(df.global.world.items.other.IN_PLAY) do + if it ~= target and not it.flags.in_job and it.flags.on_ground and it.pos.z == z and math.abs(it.pos.x - x) <= radius and math.abs(it.pos.y - y) <= radius then + dfhack.job.attachJobItem(job, it, df.job_role_type.Hauled, -1, -1) + count = count + 1 + if debug_enabled then + dfhack.gui.showAnnouncement( + ('multihaul: added %s to hauling job'):format( + dfhack.items.getDescription(it, 0)), + COLOR_CYAN) + end + if count >= max_items then break end + end + end + if debug_enabled and count > 0 then + dfhack.gui.showAnnouncement( + ('multihaul: added %d item(s) nearby'):format(count), + COLOR_CYAN) + end +end + +local function on_new_job(job) + if job.job_type ~= df.job_type.StoreItemInStockpile then return end + add_nearby_items(job) +end + +local function enable(state) + enabled = state + if enabled then + eventful.onJobInitiated[GLOBAL_KEY] = on_new_job + else + eventful.onJobInitiated[GLOBAL_KEY] = nil + end + persist_state() +end + +if dfhack.internal.IN_TEST then + unit_test_hooks = {on_new_job=on_new_job, enable=enable, load_state=load_state} +end + +-- state change handler + +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_MAP_UNLOADED then + enabled = false + eventful.onJobInitiated[GLOBAL_KEY] = nil + return + end + if sc == SC_MAP_LOADED then + load_state() + if enabled then + eventful.onJobInitiated[GLOBAL_KEY] = on_new_job + end + end +end + +if dfhack_flags.module then + return +end + +local args = {...} +if dfhack_flags.enable then + if dfhack_flags.enable_state then + enable(true) + else + enable(false) + end + return +end + +local function parse_options(start_idx) + local i = start_idx + while i <= #args do + local a = args[i] + if a == '--debug' then + debug_enabled = true + elseif a == '--no-debug' then + debug_enabled = false + elseif a == '--radius' then + i = i + 1 + radius = tonumber(args[i]) or radius + elseif a == '--max-items' then + i = i + 1 + max_items = tonumber(args[i]) or max_items + end + i = i + 1 + end +end + +local cmd = args[1] +if cmd == 'enable' then + parse_options(2) + enable(true) +elseif cmd == 'disable' then + enable(false) +elseif cmd == 'status' or not cmd then + print((enabled and 'multihaul is enabled' or 'multihaul is disabled')) + print(('radius=%d max-items=%d debug=%s') + :format(radius, max_items, debug_enabled and 'on' or 'off')) +elseif cmd == 'config' then + parse_options(2) + persist_state() + print(('multihaul config: radius=%d max-items=%d debug=%s') + :format(radius, max_items, debug_enabled and 'on' or 'off')) +else + qerror('Usage: multihaul [enable|disable|status|config] [--radius N] [--max-items N] [--debug|--no-debug]') +end From 996bec836186026125923ac29ce08cda2bbd815b Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sun, 20 Jul 2025 22:17:07 +0200 Subject: [PATCH 41/93] Fix multihaul unloading and limit by container size --- multihaul.lua | 75 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 55 insertions(+), 20 deletions(-) diff --git a/multihaul.lua b/multihaul.lua index 674cc74c9..82a01918c 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -10,7 +10,9 @@ local GLOBAL_KEY = 'multihaul' enabled = enabled or false debug_enabled = debug_enabled or false radius = radius or 1 -max_items = max_items or 4 + +-- tracks extra items added to hauling jobs so we can unload them on completion +job_added_items = job_added_items or {} function isEnabled() return enabled @@ -21,7 +23,6 @@ local function persist_state() enabled=enabled, debug_enabled=debug_enabled, radius=radius, - max_items=max_items, }) end @@ -30,7 +31,6 @@ local function load_state() enabled = data.enabled or false debug_enabled = data.debug_enabled or false radius = data.radius or 1 - max_items = data.max_items or 4 end local function add_nearby_items(job) @@ -41,24 +41,36 @@ local function add_nearby_items(job) local x,y,z = dfhack.items.getPosition(target) if not x then return end - local count = 0 + local remaining = dfhack.items.getCapacity(target) + for _,i in ipairs(dfhack.items.getContainedItems(target)) do + remaining = remaining - i:getVolume() + end + + local added = {} for _,it in ipairs(df.global.world.items.other.IN_PLAY) do if it ~= target and not it.flags.in_job and it.flags.on_ground and it.pos.z == z and math.abs(it.pos.x - x) <= radius and math.abs(it.pos.y - y) <= radius then - dfhack.job.attachJobItem(job, it, df.job_role_type.Hauled, -1, -1) - count = count + 1 + local vol = it:getVolume() + if vol > remaining then break end + -- attach to job item 0 so the game knows to unload it + dfhack.job.attachJobItem(job, it, df.job_role_type.Hauled, 0, -1) + table.insert(added, it.id) + remaining = remaining - vol if debug_enabled then dfhack.gui.showAnnouncement( ('multihaul: added %s to hauling job'):format( dfhack.items.getDescription(it, 0)), COLOR_CYAN) end - if count >= max_items then break end + if remaining <= 0 then break end end end - if debug_enabled and count > 0 then - dfhack.gui.showAnnouncement( - ('multihaul: added %d item(s) nearby'):format(count), - COLOR_CYAN) + if #added > 0 then + job_added_items[job.id] = added + if debug_enabled then + dfhack.gui.showAnnouncement( + ('multihaul: added %d item(s) nearby'):format(#added), + COLOR_CYAN) + end end end @@ -67,18 +79,42 @@ local function on_new_job(job) add_nearby_items(job) end +local function on_job_completed(job) + local items = job_added_items[job.id] + if not items then return end + job_added_items[job.id] = nil + for _,id in ipairs(items) do + local it = df.item.find(id) + if it then + local cont = dfhack.items.getContainer(it) + if cont then + dfhack.items.moveToGround(it, cont.pos) + end + if it.flags.in_job then + local ref = dfhack.items.getSpecificRef(it, df.specific_ref_type.JOB) + if ref then + dfhack.job.removeJob(ref.data.job) + end + end + end + end +end + local function enable(state) enabled = state if enabled then eventful.onJobInitiated[GLOBAL_KEY] = on_new_job + eventful.onJobCompleted[GLOBAL_KEY] = on_job_completed else eventful.onJobInitiated[GLOBAL_KEY] = nil + eventful.onJobCompleted[GLOBAL_KEY] = nil end persist_state() end if dfhack.internal.IN_TEST then - unit_test_hooks = {on_new_job=on_new_job, enable=enable, load_state=load_state} + unit_test_hooks = {on_new_job=on_new_job, enable=enable, + load_state=load_state, on_job_completed=on_job_completed} end -- state change handler @@ -87,12 +123,14 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) if sc == SC_MAP_UNLOADED then enabled = false eventful.onJobInitiated[GLOBAL_KEY] = nil + eventful.onJobCompleted[GLOBAL_KEY] = nil return end if sc == SC_MAP_LOADED then load_state() if enabled then eventful.onJobInitiated[GLOBAL_KEY] = on_new_job + eventful.onJobCompleted[GLOBAL_KEY] = on_job_completed end end end @@ -122,9 +160,6 @@ local function parse_options(start_idx) elseif a == '--radius' then i = i + 1 radius = tonumber(args[i]) or radius - elseif a == '--max-items' then - i = i + 1 - max_items = tonumber(args[i]) or max_items end i = i + 1 end @@ -138,13 +173,13 @@ elseif cmd == 'disable' then enable(false) elseif cmd == 'status' or not cmd then print((enabled and 'multihaul is enabled' or 'multihaul is disabled')) - print(('radius=%d max-items=%d debug=%s') - :format(radius, max_items, debug_enabled and 'on' or 'off')) + print(('radius=%d debug=%s') + :format(radius, debug_enabled and 'on' or 'off')) elseif cmd == 'config' then parse_options(2) persist_state() - print(('multihaul config: radius=%d max-items=%d debug=%s') - :format(radius, max_items, debug_enabled and 'on' or 'off')) + print(('multihaul config: radius=%d debug=%s') + :format(radius, debug_enabled and 'on' or 'off')) else - qerror('Usage: multihaul [enable|disable|status|config] [--radius N] [--max-items N] [--debug|--no-debug]') + qerror('Usage: multihaul [enable|disable|status|config] [--radius N] [--debug|--no-debug]') end From 989ae8df5f99b8a9946be6d405bc6d89b9503439 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 00:29:41 +0200 Subject: [PATCH 42/93] multihaul.lua: reverted to item count since capacity is calculated by itself --- multihaul.lua | 97 +++++++++++++++++++++++++++------------------------ 1 file changed, 51 insertions(+), 46 deletions(-) diff --git a/multihaul.lua b/multihaul.lua index 82a01918c..6165b912c 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -9,10 +9,8 @@ local GLOBAL_KEY = 'multihaul' enabled = enabled or false debug_enabled = debug_enabled or false -radius = radius or 1 - --- tracks extra items added to hauling jobs so we can unload them on completion -job_added_items = job_added_items or {} +radius = radius or 10 +max_items = max_items or 10 function isEnabled() return enabled @@ -23,6 +21,7 @@ local function persist_state() enabled=enabled, debug_enabled=debug_enabled, radius=radius, + max_items=max_items, }) end @@ -31,6 +30,7 @@ local function load_state() enabled = data.enabled or false debug_enabled = data.debug_enabled or false radius = data.radius or 1 + max_items = data.max_items or 4 end local function add_nearby_items(job) @@ -41,64 +41,66 @@ local function add_nearby_items(job) local x,y,z = dfhack.items.getPosition(target) if not x then return end - local remaining = dfhack.items.getCapacity(target) - for _,i in ipairs(dfhack.items.getContainedItems(target)) do - remaining = remaining - i:getVolume() - end - - local added = {} + local count = 0 for _,it in ipairs(df.global.world.items.other.IN_PLAY) do if it ~= target and not it.flags.in_job and it.flags.on_ground and it.pos.z == z and math.abs(it.pos.x - x) <= radius and math.abs(it.pos.y - y) <= radius then - local vol = it:getVolume() - if vol > remaining then break end - -- attach to job item 0 so the game knows to unload it - dfhack.job.attachJobItem(job, it, df.job_role_type.Hauled, 0, -1) - table.insert(added, it.id) - remaining = remaining - vol + dfhack.job.attachJobItem(job, it, df.job_role_type.Hauled, -1, -1) + count = count + 1 if debug_enabled then dfhack.gui.showAnnouncement( ('multihaul: added %s to hauling job'):format( dfhack.items.getDescription(it, 0)), COLOR_CYAN) end - if remaining <= 0 then break end + if count >= max_items then break end end end - if #added > 0 then - job_added_items[job.id] = added - if debug_enabled then - dfhack.gui.showAnnouncement( - ('multihaul: added %d item(s) nearby'):format(#added), - COLOR_CYAN) - end + if debug_enabled and count > 0 then + dfhack.gui.showAnnouncement( + ('multihaul: added %d item(s) nearby'):format(count), + COLOR_CYAN) end end local function on_new_job(job) - if job.job_type ~= df.job_type.StoreItemInStockpile then return end + if debug_enabled then + dfhack.gui.showAnnouncement('multihaul: on_new_job called', COLOR_GREEN) + end + if job.job_type ~= df.job_type.StoreItemInStockpile then return end + if debug_enabled then + dfhack.gui.showAnnouncement('multihaul: on_new_job called on StoreItemInStockpile', COLOR_GREEN) + end add_nearby_items(job) end local function on_job_completed(job) - local items = job_added_items[job.id] - if not items then return end - job_added_items[job.id] = nil - for _,id in ipairs(items) do - local it = df.item.find(id) - if it then - local cont = dfhack.items.getContainer(it) - if cont then - dfhack.items.moveToGround(it, cont.pos) - end - if it.flags.in_job then - local ref = dfhack.items.getSpecificRef(it, df.specific_ref_type.JOB) - if ref then - dfhack.job.removeJob(ref.data.job) - end + if debug_enabled then + dfhack.gui.showAnnouncement('multihaul: on_job_completed called', COLOR_GREEN) + end + if job.job_type ~= df.job_type.StoreItemInStockpile then return end + if debug_enabled then + dfhack.gui.showAnnouncement('multihaul: on_job_completed called on StoreItemInStockpile', COLOR_GREEN) + end + if not job_items[job.id] then return end + local this_job_items = job_items[job.id] + dfhack.gui.showAnnouncement('Trying to empty ',COLOR_CYAN) + for _, item in ipairs(this_job_items) do + if dfhack.items.getCapacity(item) > 0 then + emptyContainer(item) + end + end +end + +local function emptyContainer(container) + local items = dfhack.items.getContainedItems(container) + if #items > 0 then + dfhack.gui.showAnnouncement('Emptying ',COLOR_CYAN) + local pos = xyz2pos(dfhack.items.getPosition(container)) + for _, item in ipairs(items) do + dfhack.items.moveToGround(item, pos) end end end -end local function enable(state) enabled = state @@ -160,6 +162,9 @@ local function parse_options(start_idx) elseif a == '--radius' then i = i + 1 radius = tonumber(args[i]) or radius + elseif a == '--max-items' then + i = i + 1 + max_items = tonumber(args[i]) or max_items end i = i + 1 end @@ -173,13 +178,13 @@ elseif cmd == 'disable' then enable(false) elseif cmd == 'status' or not cmd then print((enabled and 'multihaul is enabled' or 'multihaul is disabled')) - print(('radius=%d debug=%s') - :format(radius, debug_enabled and 'on' or 'off')) + print(('radius=%d max-items=%d debug=%s') + :format(radius, max_items, debug_enabled and 'on' or 'off')) elseif cmd == 'config' then parse_options(2) persist_state() - print(('multihaul config: radius=%d debug=%s') - :format(radius, debug_enabled and 'on' or 'off')) + print(('multihaul config: radius=%d max-items=%d debug=%s') + :format(radius, max_items, debug_enabled and 'on' or 'off')) else - qerror('Usage: multihaul [enable|disable|status|config] [--radius N] [--debug|--no-debug]') + qerror('Usage: multihaul [enable|disable|status|config] [--radius N] [--max-items N] [--debug|--no-debug]') end From 2d23d4fa220d020bd01eed7be409c83e77beafe9 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 00:52:21 +0200 Subject: [PATCH 43/93] Debugging multihaul.lua Got everything inside job complete commented --- multihaul.lua | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/multihaul.lua b/multihaul.lua index 6165b912c..dde76e79e 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -29,8 +29,8 @@ local function load_state() local data = dfhack.persistent.getSiteData(GLOBAL_KEY, {}) enabled = data.enabled or false debug_enabled = data.debug_enabled or false - radius = data.radius or 1 - max_items = data.max_items or 4 + radius = data.radius or 10 + max_items = data.max_items or 10 end local function add_nearby_items(job) @@ -46,20 +46,18 @@ local function add_nearby_items(job) if it ~= target and not it.flags.in_job and it.flags.on_ground and it.pos.z == z and math.abs(it.pos.x - x) <= radius and math.abs(it.pos.y - y) <= radius then dfhack.job.attachJobItem(job, it, df.job_role_type.Hauled, -1, -1) count = count + 1 - if debug_enabled then - dfhack.gui.showAnnouncement( - ('multihaul: added %s to hauling job'):format( - dfhack.items.getDescription(it, 0)), - COLOR_CYAN) - end + --if debug_enabled then + -- dfhack.gui.showAnnouncement( + -- ('multihaul: added %s to hauling job'):format( + -- dfhack.items.getDescription(it, 0)), + -- COLOR_CYAN) + --end if count >= max_items then break end end end - if debug_enabled and count > 0 then - dfhack.gui.showAnnouncement( - ('multihaul: added %d item(s) nearby'):format(count), - COLOR_CYAN) - end + --if debug_enabled and count > 0 then + --dfhack.gui.showAnnouncement(('multihaul: added %d item(s) nearby'):format(count),COLOR_CYAN) + --end end local function on_new_job(job) @@ -81,14 +79,16 @@ local function on_job_completed(job) if debug_enabled then dfhack.gui.showAnnouncement('multihaul: on_job_completed called on StoreItemInStockpile', COLOR_GREEN) end - if not job_items[job.id] then return end - local this_job_items = job_items[job.id] - dfhack.gui.showAnnouncement('Trying to empty ',COLOR_CYAN) - for _, item in ipairs(this_job_items) do - if dfhack.items.getCapacity(item) > 0 then - emptyContainer(item) - end - end + --if #job.items == 0 then return end + --local container + --for _,jitem in ipairs(job.items) do + -- if jitem.item and (jitem.item:isWheelbarrow() or (jitem.item.flags.container and jitem.item:isBag())) then + -- container = jitem.item + - break + -- end + --end + --if not container then return end + --emptyContainer(container) end local function emptyContainer(container) From e4f035f0eabde79210436cfad45cb21476867193 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 01:02:06 +0200 Subject: [PATCH 44/93] Debug multihaul.lua: I got it called --- multihaul.lua | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/multihaul.lua b/multihaul.lua index dde76e79e..6bba0fdf1 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -75,20 +75,19 @@ local function on_job_completed(job) if debug_enabled then dfhack.gui.showAnnouncement('multihaul: on_job_completed called', COLOR_GREEN) end - if job.job_type ~= df.job_type.StoreItemInStockpile then return end + if job.job_type ~= df.job_type.StoreItemInStockpile or #job.items == 0 then return end if debug_enabled then - dfhack.gui.showAnnouncement('multihaul: on_job_completed called on StoreItemInStockpile', COLOR_GREEN) + dfhack.gui.showAnnouncement('multihaul: on_job_completed called on StoreItemInStockpile with job items', COLOR_GREEN) end - --if #job.items == 0 then return end - --local container - --for _,jitem in ipairs(job.items) do - -- if jitem.item and (jitem.item:isWheelbarrow() or (jitem.item.flags.container and jitem.item:isBag())) then - -- container = jitem.item - - break - -- end - --end - --if not container then return end - --emptyContainer(container) + local container + for _,jitem in ipairs(job.items) do + if jitem.item and (dfhack.items.getCapacity(jitem.item)>0) then + container = jitem.item + break + end + end + if not container then return end + emptyContainer(container) end local function emptyContainer(container) @@ -103,6 +102,7 @@ local function emptyContainer(container) end local function enable(state) + eventful.enableEvent(eventful.eventType.JOB_COMPLETED, 1) enabled = state if enabled then eventful.onJobInitiated[GLOBAL_KEY] = on_new_job @@ -131,6 +131,7 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) if sc == SC_MAP_LOADED then load_state() if enabled then + eventful.enableEvent(eventful.eventType.JOB_COMPLETED, 1) eventful.onJobInitiated[GLOBAL_KEY] = on_new_job eventful.onJobCompleted[GLOBAL_KEY] = on_job_completed end From f1566b2560d6eaa3135e277ff4cd4f233a56033d Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 01:54:24 +0200 Subject: [PATCH 45/93] multihaul.lua: clean version with working multi gathering and not working multi unloading --- multihaul.lua | 74 ++++++++++++++++++++------------------------------- 1 file changed, 29 insertions(+), 45 deletions(-) diff --git a/multihaul.lua b/multihaul.lua index 6bba0fdf1..8b157c560 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -46,77 +46,64 @@ local function add_nearby_items(job) if it ~= target and not it.flags.in_job and it.flags.on_ground and it.pos.z == z and math.abs(it.pos.x - x) <= radius and math.abs(it.pos.y - y) <= radius then dfhack.job.attachJobItem(job, it, df.job_role_type.Hauled, -1, -1) count = count + 1 - --if debug_enabled then - -- dfhack.gui.showAnnouncement( - -- ('multihaul: added %s to hauling job'):format( - -- dfhack.items.getDescription(it, 0)), - -- COLOR_CYAN) - --end + if debug_enabled then + dfhack.gui.showAnnouncement( + ('multihaul: added %s to hauling job'):format( + dfhack.items.getDescription(it, 0)), + COLOR_CYAN) + end if count >= max_items then break end end end - --if debug_enabled and count > 0 then - --dfhack.gui.showAnnouncement(('multihaul: added %d item(s) nearby'):format(count),COLOR_CYAN) - --end + if debug_enabled and count > 0 then + dfhack.gui.showAnnouncement(('multihaul: added %d item(s) nearby'):format(count),COLOR_CYAN) + end end local function on_new_job(job) - if debug_enabled then - dfhack.gui.showAnnouncement('multihaul: on_new_job called', COLOR_GREEN) - end if job.job_type ~= df.job_type.StoreItemInStockpile then return end - if debug_enabled then - dfhack.gui.showAnnouncement('multihaul: on_new_job called on StoreItemInStockpile', COLOR_GREEN) - end add_nearby_items(job) -end - -local function on_job_completed(job) - if debug_enabled then - dfhack.gui.showAnnouncement('multihaul: on_job_completed called', COLOR_GREEN) - end - if job.job_type ~= df.job_type.StoreItemInStockpile or #job.items == 0 then return end - if debug_enabled then - dfhack.gui.showAnnouncement('multihaul: on_job_completed called on StoreItemInStockpile with job items', COLOR_GREEN) - end - local container - for _,jitem in ipairs(job.items) do + for _,jitem in ipairs(job.items) do if jitem.item and (dfhack.items.getCapacity(jitem.item)>0) then container = jitem.item break end end - if not container then return end - emptyContainer(container) + if not container then return + else emptyContainedItems(container) + end end - -local function emptyContainer(container) - local items = dfhack.items.getContainedItems(container) - if #items > 0 then - dfhack.gui.showAnnouncement('Emptying ',COLOR_CYAN) - local pos = xyz2pos(dfhack.items.getPosition(container)) - for _, item in ipairs(items) do - dfhack.items.moveToGround(item, pos) + +local function emptyContainedItems(wheelbarrow) + local items = dfhack.items.getContainedItems(wheelbarrow) + if #items == 0 then return end + e_count = e_count + 1 + for _,item in ipairs(items) do + if not dryrun then + if item.flags.in_job then + local job_ref = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) + if job_ref then + dfhack.job.removeJob(job_ref.data.job) + end end + dfhack.items.moveToGround(item, wheelbarrow.pos) end + i_count = i_count + 1 end +end local function enable(state) - eventful.enableEvent(eventful.eventType.JOB_COMPLETED, 1) enabled = state if enabled then eventful.onJobInitiated[GLOBAL_KEY] = on_new_job - eventful.onJobCompleted[GLOBAL_KEY] = on_job_completed else eventful.onJobInitiated[GLOBAL_KEY] = nil - eventful.onJobCompleted[GLOBAL_KEY] = nil end persist_state() end if dfhack.internal.IN_TEST then - unit_test_hooks = {on_new_job=on_new_job, enable=enable, - load_state=load_state, on_job_completed=on_job_completed} + unit_test_hooks = {on_new_job=on_new_job, enable=enable, load_state=load_state} end -- state change handler @@ -125,15 +112,12 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) if sc == SC_MAP_UNLOADED then enabled = false eventful.onJobInitiated[GLOBAL_KEY] = nil - eventful.onJobCompleted[GLOBAL_KEY] = nil return end if sc == SC_MAP_LOADED then load_state() if enabled then - eventful.enableEvent(eventful.eventType.JOB_COMPLETED, 1) eventful.onJobInitiated[GLOBAL_KEY] = on_new_job - eventful.onJobCompleted[GLOBAL_KEY] = on_job_completed end end end From 5d1699ee0146aa996295a65beea2579e826049e1 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 02:07:23 +0200 Subject: [PATCH 46/93] multihaul.lua: fixed unneeded code copied from emptywheelbarrow --- multihaul.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/multihaul.lua b/multihaul.lua index 8b157c560..f07c700ed 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -75,11 +75,13 @@ local function on_new_job(job) end local function emptyContainedItems(wheelbarrow) + if debug_enabled and count > 0 then + dfhack.gui.showAnnouncement('multihaul: trying to empty the wheelbarrow',COLOR_CYAN) + end local items = dfhack.items.getContainedItems(wheelbarrow) if #items == 0 then return end e_count = e_count + 1 for _,item in ipairs(items) do - if not dryrun then if item.flags.in_job then local job_ref = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) if job_ref then @@ -87,9 +89,8 @@ local function emptyContainedItems(wheelbarrow) end end dfhack.items.moveToGround(item, wheelbarrow.pos) - end i_count = i_count + 1 - end +end end local function enable(state) From 0b31ff1dde36c4ad1627002176daee3e4e6fb17e Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 02:11:09 +0200 Subject: [PATCH 47/93] fix multihaul script --- multihaul.lua | 45 ++++++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/multihaul.lua b/multihaul.lua index f07c700ed..92b94eee8 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -12,6 +12,9 @@ debug_enabled = debug_enabled or false radius = radius or 10 max_items = max_items or 10 +-- forward declaration for function defined below +local emptyContainedItems + function isEnabled() return enabled end @@ -61,36 +64,40 @@ local function add_nearby_items(job) end local function on_new_job(job) - if job.job_type ~= df.job_type.StoreItemInStockpile then return end + if job.job_type ~= df.job_type.StoreItemInStockpile then return end + add_nearby_items(job) - for _,jitem in ipairs(job.items) do - if jitem.item and (dfhack.items.getCapacity(jitem.item)>0) then + + local container + for _, jitem in ipairs(job.items) do + if jitem.item and (dfhack.items.getCapacity(jitem.item) > 0) then container = jitem.item break end end - if not container then return - else emptyContainedItems(container) - end + + if container then + emptyContainedItems(container) + end end local function emptyContainedItems(wheelbarrow) - if debug_enabled and count > 0 then - dfhack.gui.showAnnouncement('multihaul: trying to empty the wheelbarrow',COLOR_CYAN) - end local items = dfhack.items.getContainedItems(wheelbarrow) if #items == 0 then return end - e_count = e_count + 1 - for _,item in ipairs(items) do - if item.flags.in_job then - local job_ref = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) - if job_ref then - dfhack.job.removeJob(job_ref.data.job) - end + + if debug_enabled then + dfhack.gui.showAnnouncement('multihaul: emptying wheelbarrow', COLOR_CYAN) + end + + for _, item in ipairs(items) do + if item.flags.in_job then + local job_ref = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) + if job_ref then + dfhack.job.removeJob(job_ref.data.job) end - dfhack.items.moveToGround(item, wheelbarrow.pos) - i_count = i_count + 1 -end + end + dfhack.items.moveToGround(item, wheelbarrow.pos) + end end local function enable(state) From fef261b6595ab7693883c19959f85feb40c9d24f Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 02:21:52 +0200 Subject: [PATCH 48/93] multihaul.lua: WORKING VERSION HURRAY Unloading works perfectly, no more errors in a console --- multihaul.lua | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/multihaul.lua b/multihaul.lua index 92b94eee8..fc7b6efa3 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -12,9 +12,6 @@ debug_enabled = debug_enabled or false radius = radius or 10 max_items = max_items or 10 --- forward declaration for function defined below -local emptyContainedItems - function isEnabled() return enabled end @@ -63,24 +60,6 @@ local function add_nearby_items(job) end end -local function on_new_job(job) - if job.job_type ~= df.job_type.StoreItemInStockpile then return end - - add_nearby_items(job) - - local container - for _, jitem in ipairs(job.items) do - if jitem.item and (dfhack.items.getCapacity(jitem.item) > 0) then - container = jitem.item - break - end - end - - if container then - emptyContainedItems(container) - end -end - local function emptyContainedItems(wheelbarrow) local items = dfhack.items.getContainedItems(wheelbarrow) if #items == 0 then return end @@ -100,6 +79,24 @@ local function emptyContainedItems(wheelbarrow) end end +local function on_new_job(job) + if job.job_type ~= df.job_type.StoreItemInStockpile then return end + + add_nearby_items(job) + + local container + for _, jitem in ipairs(job.items) do + if jitem.item and (dfhack.items.getCapacity(jitem.item) > 0) then + container = jitem.item + break + end + end + + if container then + emptyContainedItems(container) + end +end + local function enable(state) enabled = state if enabled then From 37c55c50b5a2d2a4f4ab51ff0cae426c37daa2a0 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 02:46:18 +0200 Subject: [PATCH 49/93] Restrict multihaul to wheelbarrow jobs --- docs/multihaul.rst | 4 ++-- multihaul.lua | 17 ++++++++--------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/docs/multihaul.rst b/docs/multihaul.rst index faf99e264..f2c61e427 100644 --- a/docs/multihaul.rst +++ b/docs/multihaul.rst @@ -2,11 +2,11 @@ multihaul ========= .. dfhack-tool:: - :summary: Haulers gather multiple nearby items when using bags or wheelbarrows. + :summary: Haulers gather multiple nearby items when using wheelbarrows. :tags: fort productivity items This tool allows dwarves to collect several adjacent items at once when -performing hauling jobs with a bag or wheelbarrow. When enabled, new +performing hauling jobs with a wheelbarrow. When enabled, new ``StoreItemInStockpile`` jobs will automatically attach nearby items so they can be hauled in a single trip. By default, up to four additional items within one tile of the original item are collected. diff --git a/multihaul.lua b/multihaul.lua index fc7b6efa3..b8ef2dcb8 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -1,4 +1,4 @@ --- Allow haulers to pick up multiple nearby items when using bags or wheelbarrows +-- Allow haulers to pick up multiple nearby items when using wheelbarrows --@module = true --@enable = true @@ -82,19 +82,18 @@ end local function on_new_job(job) if job.job_type ~= df.job_type.StoreItemInStockpile then return end - add_nearby_items(job) - - local container + local wheelbarrow for _, jitem in ipairs(job.items) do - if jitem.item and (dfhack.items.getCapacity(jitem.item) > 0) then - container = jitem.item + if jitem.item and df.item_toolst:is_instance(jitem.item) and jitem.item:isWheelbarrow() then + wheelbarrow = jitem.item break end end - if container then - emptyContainedItems(container) - end + if not wheelbarrow then return end + + add_nearby_items(job) + emptyContainedItems(wheelbarrow) end local function enable(state) From 2572537ca5ad9d8b8402dd4f1b8f238649423345 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 12:52:43 +0200 Subject: [PATCH 50/93] multihaul: restrict to identical items and verify wheelbarrow --- docs/multihaul.rst | 7 ++++--- multihaul.lua | 28 ++++++++++++++++++++-------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/docs/multihaul.rst b/docs/multihaul.rst index f2c61e427..cfe4a6f06 100644 --- a/docs/multihaul.rst +++ b/docs/multihaul.rst @@ -7,9 +7,10 @@ multihaul This tool allows dwarves to collect several adjacent items at once when performing hauling jobs with a wheelbarrow. When enabled, new -``StoreItemInStockpile`` jobs will automatically attach nearby items so they can -be hauled in a single trip. By default, up to four additional items within one -tile of the original item are collected. +``StoreItemInStockpile`` jobs will automatically attach identical nearby items so +they can be hauled in a single trip. The script only triggers when a +wheelbarrow is definitively attached to the job. By default, up to four +additional items within one tile of the original item are collected. Usage ----- diff --git a/multihaul.lua b/multihaul.lua index b8ef2dcb8..59a394d6d 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -33,6 +33,11 @@ local function load_state() max_items = data.max_items or 10 end +local function items_identical(a, b) + return a:getType() == b:getType() and a:getSubtype() == b:getSubtype() and + a.mat_type == b.mat_type and a.mat_index == b.mat_index +end + local function add_nearby_items(job) if #job.items == 0 then return end @@ -43,7 +48,9 @@ local function add_nearby_items(job) local count = 0 for _,it in ipairs(df.global.world.items.other.IN_PLAY) do - if it ~= target and not it.flags.in_job and it.flags.on_ground and it.pos.z == z and math.abs(it.pos.x - x) <= radius and math.abs(it.pos.y - y) <= radius then + if it ~= target and not it.flags.in_job and it.flags.on_ground and + it.pos.z == z and math.abs(it.pos.x - x) <= radius and + math.abs(it.pos.y - y) <= radius and items_identical(it, target) then dfhack.job.attachJobItem(job, it, df.job_role_type.Hauled, -1, -1) count = count + 1 if debug_enabled then @@ -79,17 +86,22 @@ local function emptyContainedItems(wheelbarrow) end end -local function on_new_job(job) - if job.job_type ~= df.job_type.StoreItemInStockpile then return end - - local wheelbarrow +local function find_attached_wheelbarrow(job) for _, jitem in ipairs(job.items) do - if jitem.item and df.item_toolst:is_instance(jitem.item) and jitem.item:isWheelbarrow() then - wheelbarrow = jitem.item - break + local item = jitem.item + if item and df.item_toolst:is_instance(item) and item:isWheelbarrow() then + local ref = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) + if ref and ref.data.job == job then + return item + end end end +end + +local function on_new_job(job) + if job.job_type ~= df.job_type.StoreItemInStockpile then return end + local wheelbarrow = find_attached_wheelbarrow(job) if not wheelbarrow then return end add_nearby_items(job) From 81f3db258346fa5403ec866802f633201a5a0966 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 13:08:01 +0200 Subject: [PATCH 51/93] multihaul: fix wheelbarrow role --- changelog.txt | 1 + docs/multihaul.rst | 2 ++ multihaul.lua | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/changelog.txt b/changelog.txt index a937e2fb2..b0e98a34d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -36,6 +36,7 @@ Template for new versions: - `gui/journal`: fix typo which caused the table of contents to always be regenerated even when not needed - `gui/mod-manager`: gracefully handle mods with missing or broken ``info.txt`` files - `uniform-unstick`: resolve overlap with new buttons in 51.13 +- `multihaul`: skip wheelbarrows that are not push vehicles and clear stuck jobs ## Misc Improvements diff --git a/docs/multihaul.rst b/docs/multihaul.rst index cfe4a6f06..a32dbbdeb 100644 --- a/docs/multihaul.rst +++ b/docs/multihaul.rst @@ -11,6 +11,8 @@ performing hauling jobs with a wheelbarrow. When enabled, new they can be hauled in a single trip. The script only triggers when a wheelbarrow is definitively attached to the job. By default, up to four additional items within one tile of the original item are collected. +Jobs with wheelbarrows that are not assigned as push vehicles are ignored and +any stuck hauling jobs are automatically cleared. Usage ----- diff --git a/multihaul.lua b/multihaul.lua index 59a394d6d..70d602720 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -86,10 +86,32 @@ local function emptyContainedItems(wheelbarrow) end end +local function wheelbarrow_needs_reset(job) + for _, jitem in ipairs(job.items) do + local item = jitem.item + if item and df.item_toolst:is_instance(item) and item:isWheelbarrow() then + if jitem.role ~= df.job_role_type.PushHaulVehicle then + return true + end + end + end + return false +end + +local function clear_job_items(job) + if debug_enabled then + dfhack.gui.showAnnouncement('multihaul: clearing stuck hauling job', COLOR_CYAN) + end + job.items:resize(0) +end + local function find_attached_wheelbarrow(job) for _, jitem in ipairs(job.items) do local item = jitem.item if item and df.item_toolst:is_instance(item) and item:isWheelbarrow() then + if jitem.role ~= df.job_role_type.PushHaulVehicle then + return nil + end local ref = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) if ref and ref.data.job == job then return item @@ -101,6 +123,11 @@ end local function on_new_job(job) if job.job_type ~= df.job_type.StoreItemInStockpile then return end + if wheelbarrow_needs_reset(job) then + clear_job_items(job) + return + end + local wheelbarrow = find_attached_wheelbarrow(job) if not wheelbarrow then return end @@ -112,6 +139,11 @@ local function enable(state) enabled = state if enabled then eventful.onJobInitiated[GLOBAL_KEY] = on_new_job + for _, job in utils.listpairs(df.global.world.jobs.list) do + if job.job_type == df.job_type.StoreItemInStockpile and wheelbarrow_needs_reset(job) then + clear_job_items(job) + end + end else eventful.onJobInitiated[GLOBAL_KEY] = nil end @@ -134,6 +166,11 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) load_state() if enabled then eventful.onJobInitiated[GLOBAL_KEY] = on_new_job + for _, job in utils.listpairs(df.global.world.jobs.list) do + if job.job_type == df.job_type.StoreItemInStockpile and wheelbarrow_needs_reset(job) then + clear_job_items(job) + end + end end end end From 58b368848d3842b3c126b931597ebfb2d01c46ff Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 13:46:52 +0200 Subject: [PATCH 52/93] multihaul.lua: removed unnesessary checks and fixes --- multihaul.lua | 36 ++---------------------------------- 1 file changed, 2 insertions(+), 34 deletions(-) diff --git a/multihaul.lua b/multihaul.lua index 70d602720..49bf28814 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -50,7 +50,8 @@ local function add_nearby_items(job) for _,it in ipairs(df.global.world.items.other.IN_PLAY) do if it ~= target and not it.flags.in_job and it.flags.on_ground and it.pos.z == z and math.abs(it.pos.x - x) <= radius and - math.abs(it.pos.y - y) <= radius and items_identical(it, target) then + math.abs(it.pos.y - y) <= radius --and items_identical(it, target) + then dfhack.job.attachJobItem(job, it, df.job_role_type.Hauled, -1, -1) count = count + 1 if debug_enabled then @@ -62,9 +63,6 @@ local function add_nearby_items(job) if count >= max_items then break end end end - if debug_enabled and count > 0 then - dfhack.gui.showAnnouncement(('multihaul: added %d item(s) nearby'):format(count),COLOR_CYAN) - end end local function emptyContainedItems(wheelbarrow) @@ -86,18 +84,6 @@ local function emptyContainedItems(wheelbarrow) end end -local function wheelbarrow_needs_reset(job) - for _, jitem in ipairs(job.items) do - local item = jitem.item - if item and df.item_toolst:is_instance(item) and item:isWheelbarrow() then - if jitem.role ~= df.job_role_type.PushHaulVehicle then - return true - end - end - end - return false -end - local function clear_job_items(job) if debug_enabled then dfhack.gui.showAnnouncement('multihaul: clearing stuck hauling job', COLOR_CYAN) @@ -123,11 +109,6 @@ end local function on_new_job(job) if job.job_type ~= df.job_type.StoreItemInStockpile then return end - if wheelbarrow_needs_reset(job) then - clear_job_items(job) - return - end - local wheelbarrow = find_attached_wheelbarrow(job) if not wheelbarrow then return end @@ -139,11 +120,6 @@ local function enable(state) enabled = state if enabled then eventful.onJobInitiated[GLOBAL_KEY] = on_new_job - for _, job in utils.listpairs(df.global.world.jobs.list) do - if job.job_type == df.job_type.StoreItemInStockpile and wheelbarrow_needs_reset(job) then - clear_job_items(job) - end - end else eventful.onJobInitiated[GLOBAL_KEY] = nil end @@ -164,14 +140,6 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) end if sc == SC_MAP_LOADED then load_state() - if enabled then - eventful.onJobInitiated[GLOBAL_KEY] = on_new_job - for _, job in utils.listpairs(df.global.world.jobs.list) do - if job.job_type == df.job_type.StoreItemInStockpile and wheelbarrow_needs_reset(job) then - clear_job_items(job) - end - end - end end end From cfb1e9571d36b51d62e59a8715feac7ce0255a71 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 13:54:00 +0200 Subject: [PATCH 53/93] multihaul: filter items by stockpile settings --- docs/multihaul.rst | 1 + multihaul.lua | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/multihaul.rst b/docs/multihaul.rst index a32dbbdeb..dc481f631 100644 --- a/docs/multihaul.rst +++ b/docs/multihaul.rst @@ -13,6 +13,7 @@ wheelbarrow is definitively attached to the job. By default, up to four additional items within one tile of the original item are collected. Jobs with wheelbarrows that are not assigned as push vehicles are ignored and any stuck hauling jobs are automatically cleared. +Only items that match the destination stockpile filters are added to the job. Usage ----- diff --git a/multihaul.lua b/multihaul.lua index 49bf28814..3f634b0dc 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -33,6 +33,11 @@ local function load_state() max_items = data.max_items or 10 end +local function get_job_stockpile(job) + local ref = dfhack.job.getGeneralRef(job, df.general_ref_type.BUILDING_HOLDER) + return ref and df.building.find(ref.building_id) or nil +end + local function items_identical(a, b) return a:getType() == b:getType() and a:getSubtype() == b:getSubtype() and a.mat_type == b.mat_type and a.mat_index == b.mat_index @@ -43,6 +48,8 @@ local function add_nearby_items(job) local target = job.items[0].item if not target then return end + local stockpile = get_job_stockpile(job) + if not stockpile then return end local x,y,z = dfhack.items.getPosition(target) if not x then return end @@ -50,8 +57,9 @@ local function add_nearby_items(job) for _,it in ipairs(df.global.world.items.other.IN_PLAY) do if it ~= target and not it.flags.in_job and it.flags.on_ground and it.pos.z == z and math.abs(it.pos.x - x) <= radius and - math.abs(it.pos.y - y) <= radius --and items_identical(it, target) - then + math.abs(it.pos.y - y) <= radius and + dfhack.buildings.isItemAllowedInStockpile(it, stockpile) --and items_identical(it, target) + then dfhack.job.attachJobItem(job, it, df.job_role_type.Hauled, -1, -1) count = count + 1 if debug_enabled then From 986eeb552b59a9038ba70c4833e4dc06cafe1f73 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:05:33 +0200 Subject: [PATCH 54/93] multihaul: add matching modes --- changelog.txt | 4 +++- docs/multihaul.rst | 14 ++++++++++---- multihaul.lua | 41 +++++++++++++++++++++++++++++++++-------- 3 files changed, 46 insertions(+), 13 deletions(-) diff --git a/changelog.txt b/changelog.txt index b0e98a34d..47d9dd7b4 100644 --- a/changelog.txt +++ b/changelog.txt @@ -15,7 +15,6 @@ Template for new versions: ## New Tools ## New Features - ## Fixes - `gui/gm-unit`: remove reference to ``think_counter``, removed in v51.12 - fixed references to removed ``unit.curse`` compound @@ -32,6 +31,9 @@ Template for new versions: ## New Features +- `multihaul`: add ``--mode`` option to choose between ``any``, ``sametype``, + and ``identical`` item matching + ## Fixes - `gui/journal`: fix typo which caused the table of contents to always be regenerated even when not needed - `gui/mod-manager`: gracefully handle mods with missing or broken ``info.txt`` files diff --git a/docs/multihaul.rst b/docs/multihaul.rst index dc481f631..edccc0f36 100644 --- a/docs/multihaul.rst +++ b/docs/multihaul.rst @@ -7,10 +7,11 @@ multihaul This tool allows dwarves to collect several adjacent items at once when performing hauling jobs with a wheelbarrow. When enabled, new -``StoreItemInStockpile`` jobs will automatically attach identical nearby items so -they can be hauled in a single trip. The script only triggers when a -wheelbarrow is definitively attached to the job. By default, up to four -additional items within one tile of the original item are collected. +``StoreItemInStockpile`` jobs will automatically attach nearby items so +they can be hauled in a single trip. Which items qualify can be controlled +with the ``--mode`` option. The script only triggers when a wheelbarrow is +definitively attached to the job. By default, up to four additional items within +one tile of the original item are collected. Jobs with wheelbarrows that are not assigned as push vehicles are ignored and any stuck hauling jobs are automatically cleared. Only items that match the destination stockpile filters are added to the job. @@ -36,6 +37,11 @@ Options ``--max-items `` Attach at most this many additional items to each hauling job. Default is ``4``. +``--mode `` + Control which nearby items are attached. ``any`` collects any allowed items, + ``sametype`` only collects items of the same type and subtype, and + ``identical`` requires the items to match type, subtype, and material. The + default is ``any``. ``--debug`` Show debug messages via ``dfhack.gui.showAnnouncement`` when items are attached. Use ``--no-debug`` to disable. diff --git a/multihaul.lua b/multihaul.lua index 3f634b0dc..cec09f21b 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -11,6 +11,7 @@ enabled = enabled or false debug_enabled = debug_enabled or false radius = radius or 10 max_items = max_items or 10 +mode = mode or 'any' function isEnabled() return enabled @@ -22,6 +23,7 @@ local function persist_state() debug_enabled=debug_enabled, radius=radius, max_items=max_items, + mode=mode, }) end @@ -31,6 +33,7 @@ local function load_state() debug_enabled = data.debug_enabled or false radius = data.radius or 10 max_items = data.max_items or 10 + mode = data.mode or 'any' end local function get_job_stockpile(job) @@ -43,6 +46,10 @@ local function items_identical(a, b) a.mat_type == b.mat_type and a.mat_index == b.mat_index end +local function items_sametype(a, b) + return a:getType() == b:getType() and a:getSubtype() == b:getSubtype() +end + local function add_nearby_items(job) if #job.items == 0 then return end @@ -53,13 +60,23 @@ local function add_nearby_items(job) local x,y,z = dfhack.items.getPosition(target) if not x then return end + local function matches(it) + if mode == 'identical' then + return items_identical(it, target) + elseif mode == 'sametype' then + return items_sametype(it, target) + else + return true + end + end + local count = 0 for _,it in ipairs(df.global.world.items.other.IN_PLAY) do if it ~= target and not it.flags.in_job and it.flags.on_ground and it.pos.z == z and math.abs(it.pos.x - x) <= radius and math.abs(it.pos.y - y) <= radius and - dfhack.buildings.isItemAllowedInStockpile(it, stockpile) --and items_identical(it, target) - then + dfhack.buildings.isItemAllowedInStockpile(it, stockpile) and + matches(it) then dfhack.job.attachJobItem(job, it, df.job_role_type.Hauled, -1, -1) count = count + 1 if debug_enabled then @@ -176,9 +193,17 @@ local function parse_options(start_idx) elseif a == '--radius' then i = i + 1 radius = tonumber(args[i]) or radius - elseif a == '--max-items' then + elseif a == '--max-items' then i = i + 1 max_items = tonumber(args[i]) or max_items + elseif a == '--mode' then + i = i + 1 + local m = args[i] + if m == 'any' or m == 'sametype' or m == 'identical' then + mode = m + else + qerror('invalid mode: ' .. tostring(m)) + end end i = i + 1 end @@ -192,13 +217,13 @@ elseif cmd == 'disable' then enable(false) elseif cmd == 'status' or not cmd then print((enabled and 'multihaul is enabled' or 'multihaul is disabled')) - print(('radius=%d max-items=%d debug=%s') - :format(radius, max_items, debug_enabled and 'on' or 'off')) + print(('radius=%d max-items=%d mode=%s debug=%s') + :format(radius, max_items, mode, debug_enabled and 'on' or 'off')) elseif cmd == 'config' then parse_options(2) persist_state() - print(('multihaul config: radius=%d max-items=%d debug=%s') - :format(radius, max_items, debug_enabled and 'on' or 'off')) + print(('multihaul config: radius=%d max-items=%d mode=%s debug=%s') + :format(radius, max_items, mode, debug_enabled and 'on' or 'off')) else - qerror('Usage: multihaul [enable|disable|status|config] [--radius N] [--max-items N] [--debug|--no-debug]') + qerror('Usage: multihaul [enable|disable|status|config] [--radius N] [--max-items N] [--mode MODE] [--debug|--no-debug]') end From c2a4bd1d1b6ecc143fa274d93b5668523243a4c3 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:11:52 +0200 Subject: [PATCH 55/93] multihaul.lua: changed default mode and removed isItemAllowedInStockpile sadly isItemAllowedInStockpile or anything close does not exsist --- multihaul.lua | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/multihaul.lua b/multihaul.lua index cec09f21b..83f38c709 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -11,7 +11,7 @@ enabled = enabled or false debug_enabled = debug_enabled or false radius = radius or 10 max_items = max_items or 10 -mode = mode or 'any' +mode = mode or 'sametype' function isEnabled() return enabled @@ -33,7 +33,7 @@ local function load_state() debug_enabled = data.debug_enabled or false radius = data.radius or 10 max_items = data.max_items or 10 - mode = data.mode or 'any' + mode = data.mode or 'sametype' end local function get_job_stockpile(job) @@ -75,7 +75,6 @@ local function add_nearby_items(job) if it ~= target and not it.flags.in_job and it.flags.on_ground and it.pos.z == z and math.abs(it.pos.x - x) <= radius and math.abs(it.pos.y - y) <= radius and - dfhack.buildings.isItemAllowedInStockpile(it, stockpile) and matches(it) then dfhack.job.attachJobItem(job, it, df.job_role_type.Hauled, -1, -1) count = count + 1 From 48cd99aa7ea9d54b6585d1066057f35d90a21563 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:36:22 +0200 Subject: [PATCH 56/93] multihaul.lua: splited type and subtype modes --- multihaul.lua | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/multihaul.lua b/multihaul.lua index 83f38c709..9ce0c8ff8 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -47,6 +47,10 @@ local function items_identical(a, b) end local function items_sametype(a, b) + return a:getType() == b:getType() +end + +local function items_samesubtype(a, b) return a:getType() == b:getType() and a:getSubtype() == b:getSubtype() end @@ -65,7 +69,9 @@ local function add_nearby_items(job) return items_identical(it, target) elseif mode == 'sametype' then return items_sametype(it, target) - else + elseif mode == 'samesubtype' then + return items_samesubtype(it, target) + else return true end end @@ -80,8 +86,8 @@ local function add_nearby_items(job) count = count + 1 if debug_enabled then dfhack.gui.showAnnouncement( - ('multihaul: added %s to hauling job'):format( - dfhack.items.getDescription(it, 0)), + ('multihaul: added %s to hauling job of %s'):format( + dfhack.items.getDescription(it, 0), dfhack.items.getDescription(target, 0)), COLOR_CYAN) end if count >= max_items then break end @@ -198,7 +204,7 @@ local function parse_options(start_idx) elseif a == '--mode' then i = i + 1 local m = args[i] - if m == 'any' or m == 'sametype' or m == 'identical' then + if m == 'any' or m == 'sametype' or m == 'samesubtype' or m == 'identical' then mode = m else qerror('invalid mode: ' .. tostring(m)) From 8cbf4b3b2de2f035ba8482ff7ea17a0dc5cc2776 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:43:49 +0200 Subject: [PATCH 57/93] refactor multihaul state and update docs --- docs/multihaul.rst | 16 +++++----- multihaul.lua | 78 ++++++++++++++++++++++------------------------ 2 files changed, 45 insertions(+), 49 deletions(-) diff --git a/docs/multihaul.rst b/docs/multihaul.rst index edccc0f36..1d0227ea9 100644 --- a/docs/multihaul.rst +++ b/docs/multihaul.rst @@ -10,8 +10,8 @@ performing hauling jobs with a wheelbarrow. When enabled, new ``StoreItemInStockpile`` jobs will automatically attach nearby items so they can be hauled in a single trip. Which items qualify can be controlled with the ``--mode`` option. The script only triggers when a wheelbarrow is -definitively attached to the job. By default, up to four additional items within -one tile of the original item are collected. +definitively attached to the job. By default, up to ten additional items within +10 tiles of the original item are collected. Jobs with wheelbarrows that are not assigned as push vehicles are ignored and any stuck hauling jobs are automatically cleared. Only items that match the destination stockpile filters are added to the job. @@ -33,15 +33,15 @@ Options ``--radius `` Search this many tiles around the target item for additional items. Default - is ``1``. + is ``10``. ``--max-items `` Attach at most this many additional items to each hauling job. Default is - ``4``. -``--mode `` + ``10``. +``--mode `` Control which nearby items are attached. ``any`` collects any allowed items, - ``sametype`` only collects items of the same type and subtype, and - ``identical`` requires the items to match type, subtype, and material. The - default is ``any``. + ``sametype`` only matches the item type, ``samesubtype`` requires type and + subtype to match, and ``identical`` additionally matches material. The + default is ``sametype``. ``--debug`` Show debug messages via ``dfhack.gui.showAnnouncement`` when items are attached. Use ``--no-debug`` to disable. diff --git a/multihaul.lua b/multihaul.lua index 9ce0c8ff8..d93898c68 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -7,33 +7,29 @@ local utils = require('utils') local GLOBAL_KEY = 'multihaul' -enabled = enabled or false -debug_enabled = debug_enabled or false -radius = radius or 10 -max_items = max_items or 10 -mode = mode or 'sametype' +local function get_default_state() + return { + enabled=false, + debug_enabled=false, + radius=10, + max_items=10, + mode='sametype', + } +end + +state = state or get_default_state() function isEnabled() - return enabled + return state.enabled end local function persist_state() - dfhack.persistent.saveSiteData(GLOBAL_KEY, { - enabled=enabled, - debug_enabled=debug_enabled, - radius=radius, - max_items=max_items, - mode=mode, - }) + dfhack.persistent.saveSiteData(GLOBAL_KEY, state) end local function load_state() - local data = dfhack.persistent.getSiteData(GLOBAL_KEY, {}) - enabled = data.enabled or false - debug_enabled = data.debug_enabled or false - radius = data.radius or 10 - max_items = data.max_items or 10 - mode = data.mode or 'sametype' + state = get_default_state() + utils.assign(state, dfhack.persistent.getSiteData(GLOBAL_KEY, state)) end local function get_job_stockpile(job) @@ -65,13 +61,13 @@ local function add_nearby_items(job) if not x then return end local function matches(it) - if mode == 'identical' then + if state.mode == 'identical' then return items_identical(it, target) - elseif mode == 'sametype' then + elseif state.mode == 'sametype' then return items_sametype(it, target) - elseif mode == 'samesubtype' then + elseif state.mode == 'samesubtype' then return items_samesubtype(it, target) - else + else return true end end @@ -79,18 +75,18 @@ local function add_nearby_items(job) local count = 0 for _,it in ipairs(df.global.world.items.other.IN_PLAY) do if it ~= target and not it.flags.in_job and it.flags.on_ground and - it.pos.z == z and math.abs(it.pos.x - x) <= radius and - math.abs(it.pos.y - y) <= radius and + it.pos.z == z and math.abs(it.pos.x - x) <= state.radius and + math.abs(it.pos.y - y) <= state.radius and matches(it) then dfhack.job.attachJobItem(job, it, df.job_role_type.Hauled, -1, -1) count = count + 1 - if debug_enabled then + if state.debug_enabled then dfhack.gui.showAnnouncement( ('multihaul: added %s to hauling job of %s'):format( dfhack.items.getDescription(it, 0), dfhack.items.getDescription(target, 0)), COLOR_CYAN) end - if count >= max_items then break end + if count >= state.max_items then break end end end end @@ -99,7 +95,7 @@ local function emptyContainedItems(wheelbarrow) local items = dfhack.items.getContainedItems(wheelbarrow) if #items == 0 then return end - if debug_enabled then + if state.debug_enabled then dfhack.gui.showAnnouncement('multihaul: emptying wheelbarrow', COLOR_CYAN) end @@ -115,7 +111,7 @@ local function emptyContainedItems(wheelbarrow) end local function clear_job_items(job) - if debug_enabled then + if state.debug_enabled then dfhack.gui.showAnnouncement('multihaul: clearing stuck hauling job', COLOR_CYAN) end job.items:resize(0) @@ -146,9 +142,9 @@ local function on_new_job(job) emptyContainedItems(wheelbarrow) end -local function enable(state) - enabled = state - if enabled then +local function enable(val) + state.enabled = val + if state.enabled then eventful.onJobInitiated[GLOBAL_KEY] = on_new_job else eventful.onJobInitiated[GLOBAL_KEY] = nil @@ -164,7 +160,7 @@ end dfhack.onStateChange[GLOBAL_KEY] = function(sc) if sc == SC_MAP_UNLOADED then - enabled = false + state.enabled = false eventful.onJobInitiated[GLOBAL_KEY] = nil return end @@ -192,20 +188,20 @@ local function parse_options(start_idx) while i <= #args do local a = args[i] if a == '--debug' then - debug_enabled = true + state.debug_enabled = true elseif a == '--no-debug' then - debug_enabled = false + state.debug_enabled = false elseif a == '--radius' then i = i + 1 - radius = tonumber(args[i]) or radius + state.radius = tonumber(args[i]) or state.radius elseif a == '--max-items' then i = i + 1 - max_items = tonumber(args[i]) or max_items + state.max_items = tonumber(args[i]) or state.max_items elseif a == '--mode' then i = i + 1 local m = args[i] if m == 'any' or m == 'sametype' or m == 'samesubtype' or m == 'identical' then - mode = m + state.mode = m else qerror('invalid mode: ' .. tostring(m)) end @@ -221,14 +217,14 @@ if cmd == 'enable' then elseif cmd == 'disable' then enable(false) elseif cmd == 'status' or not cmd then - print((enabled and 'multihaul is enabled' or 'multihaul is disabled')) + print((state.enabled and 'multihaul is enabled' or 'multihaul is disabled')) print(('radius=%d max-items=%d mode=%s debug=%s') - :format(radius, max_items, mode, debug_enabled and 'on' or 'off')) + :format(state.radius, state.max_items, state.mode, state.debug_enabled and 'on' or 'off')) elseif cmd == 'config' then parse_options(2) persist_state() print(('multihaul config: radius=%d max-items=%d mode=%s debug=%s') - :format(radius, max_items, mode, debug_enabled and 'on' or 'off')) + :format(state.radius, state.max_items, state.mode, state.debug_enabled and 'on' or 'off')) else qerror('Usage: multihaul [enable|disable|status|config] [--radius N] [--max-items N] [--mode MODE] [--debug|--no-debug]') end From bfa99d6b2e15f15ab904b7a1c0cbf3b8e5945230 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:45:40 +0200 Subject: [PATCH 58/93] Delete changelog.txt --- changelog.txt | 1025 ------------------------------------------------- 1 file changed, 1025 deletions(-) delete mode 100644 changelog.txt diff --git a/changelog.txt b/changelog.txt deleted file mode 100644 index 47d9dd7b4..000000000 --- a/changelog.txt +++ /dev/null @@ -1,1025 +0,0 @@ -===[[[ -This file contains changes specific to the scripts repo. See docs/changelog.txt -in the dfhack repo for a full description, or -https://docs.dfhack.org/en/latest/docs/dev/Documentation.html#building-the-changelogs - -NOTE: currently, gen_changelog.py expects a "Future" section to exist at the -top of this file (even if no changes are listed under it), or you will get a -"Entry without section" error. Also, to maintain proper sorting in the generated -changelogs when making a new release, docs/changelog.txt in the dfhack repo must -have the new release listed in the right place, even if no changes were made in -that repo. - -Template for new versions: - -## New Tools - -## New Features -## Fixes -- `gui/gm-unit`: remove reference to ``think_counter``, removed in v51.12 -- fixed references to removed ``unit.curse`` compound - -## Misc Improvements - -## Removed - -]]] - -# Future - -## New Tools - -## New Features - -- `multihaul`: add ``--mode`` option to choose between ``any``, ``sametype``, - and ``identical`` item matching - -## Fixes -- `gui/journal`: fix typo which caused the table of contents to always be regenerated even when not needed -- `gui/mod-manager`: gracefully handle mods with missing or broken ``info.txt`` files -- `uniform-unstick`: resolve overlap with new buttons in 51.13 -- `multihaul`: skip wheelbarrows that are not push vehicles and clear stuck jobs - -## Misc Improvements - -## Removed - -# 51.12-r1 - -## New Tools -- `deteriorate`: (reinstated) allow corpses, body parts, food, and/or damaged clothes to rot away -- `modtools/moddable-gods`: (reinstated) create new deities from scratch - -## New Features -- `gui/spectate`: added "Prefer nicknamed" to the list of options -- `gui/mod-manager`: when run in a loaded world, shows a list of active mods -- click to export the list to the clipboard for easy sharing or posting -- `gui/blueprint`: now records zone designations -- `gui/design`: add option to draw N-point stars, hollow or filled or inverted, and change the main axis to orient in any direction - -## Fixes -- `starvingdead`: properly restore to correct enabled state when loading a new game that is different from the first game loaded in this session -- `starvingdead`: ensure undead decay does not happen faster than the declared decay rate when saving and loading the game -- `gui/design`: prevent line thickness from extending outside the map boundary - -## Misc Improvements -- `remove-stress`: also applied to long-term stress, immediately removing stressed and haggard statuses - -# 51.11-r1 - -## Fixes -- `list-agreements`: fix date math when determining petition age -- `gui/petitions`: fix date math when determining petition age -- `gui/rename`: fix commandline processing when manually specifying target ids -- `gui/sandbox`: restore metal equipment options when spawning units - -## Misc Improvements -- `fix/loyaltycascade`: now also breaks up brawls and other intra-fort conflicts that *look* like loyalty cascades -- `makeown`: remove selected unit from any current conflicts so they don't just start attacking other citizens when you make them a citizen of your fort - -# 51.09-r1 - -## New Features -- `gui/mass-remove`: add a button to the bottom toolbar when eraser mode is active for launching `gui/mass-remove` -- `idle-crafting`: default to only considering happy and ecstatic units for the highest need threshold -- `gui/sitemap`: add a button to the toolbar at the bottom left corner of the screen for launching `gui/sitemap` - -## Fixes -- `idle-crafting`: check that units still have crafting needs before creating a job for them -- `gui/journal`: prevent pause/unpause events from leaking through the UI when keys are mashed - -# 51.07-r1 - -## New Tools -- `devel/export-map`: export map tile data to a JSON file -- `autocheese`: automatically make cheese using barrels that have accumulated sufficient milk -- `gui/spectate`: interactive UI for configuring `spectate` -- `gui/notes`: UI for adding and managing notes attached to tiles on the map -- `launch`: (reinstated) new adventurer fighting move: thrash your enemies with a flying suplex -- `putontable`: (reinstated) make an item appear on a table - -## New Features -- `advtools`: ``advtools.fastcombat`` overlay (enabled by default) allows you to skip combat animations and the announcement "More" button by mashing the movement keys -- `gui/journal`: now working in adventure mode -- journal is per-adventurer, so if you unretire an adventurer, you get the same journal -- `emigration`: ``nobles`` command for sending freeloader barons back to the sites that they rule over -- `toggle-kbd-cursor`: support adventure mode (Alt-k keybinding now toggles Look mode) - -## Fixes -- `hfs-pit`: use correct wall types when making pits with walls -- `gui/liquids`: don't add liquids to wall tiles -- `gui/liquids`: using the remove tool with magma selected will no longer create unexpected unpathable tiles -- `idle-crafting`: do not assign crafting jobs to nobles holding meetings (avoids dangling jobs) -- `rejuvenate`: update unit portrait and sprite when aging up babies and children -- `rejuvenate`: recalculate labor assignments for unit when aging up babies and children (so they can start accepting jobs) - -## Misc Improvements -- `hide-tutorials`: handle tutorial popups for adventure mode -- `hide-tutorials`: new ``reset`` command that will re-enable popups in the current game (in case you hid them all and now want them back) -- `gui/notify`: moody dwarf notification turns red when they can't reach workshop or items -- `gui/notify`: save reminder now appears in adventure mode -- `gui/notify`: save reminder changes color to yellow at 30 minutes and to orange at 60 minutes -- `gui/confirm`: in the delete manager order confirmation dialog, show a description of which order you have selected to delete -- `gui/create-item`: now accepts a ``pos`` argument of where to spawn items -- `modtools/create-item`: exported ``hackWish`` function now supports ``opts.pos`` for determining spawn location -- `hfs-pit`: improve placement of stairs w/r/t eerie pits and ramp tops -- `position`: add adventurer tile position -- `position`: add global site position -- `position`: when a tile is selected, display relevant map block and intra-block offset -- `gui/sitemap`: shift click to start following the selected unit or artifact -- `prioritize`: when prioritizing jobs of a specified type, also output how many of those jobs were already prioritized before you ran the command -- `prioritize`: don't include already-prioritized jobs in the output of ``prioritize -j`` -- `gui/design`: only display vanilla dimensions tooltip if the DFHack dimensions tooltip is disabled -- `devel/query`: support adventure mode -- `devel/tree-info`: support adventure mode -- `hfs-pit`: support adventure mode -- `colonies`: support adventure mode -- `position`: report position of the adventure mode look cursor, if active - -# 51.04-r1.1 - -## Fixes -- `advtools`: fix dfhack-added conversation options not appearing in the ask whereabouts conversation tree -- `gui/rename`: fix error when changing the language of a unit's name - -## Misc Improvements -- `assign-preferences`: new ``--show`` option to display the preferences of the selected unit -- `pref-adjust`: new ``show`` command to display the preferences of the selected unit - -## Removed -- `gui/control-panel`: removed ``craft-age-wear`` tweak for Windows users; the tweak doesn't currently load on Windows - -# 51.02-r1 - -## Fixes -- `deathcause`: fix error when retrieving the name of a historical figure - -# 50.15-r2 - -## New Tools -- `fix/stuck-squad`: allow squads and messengers returning from missions to rescue squads that have gotten stuck on the world map -- `gui/rename`: (reinstated) give new in-game language-based names to anything that can be named (units, governments, fortresses, the world, etc.) - -## New Features -- `gui/settings-manager`: new overlay on the Labor -> Standing Orders tab for configuring the number of barrels to reserve for job use (so you can brew alcohol and not have all your barrels claimed by stockpiles for container storage) -- `gui/settings-manager`: standing orders save/load now includes the reserved barrels setting -- `gui/rename`: add overlay to worldgen screen allowing you to rename the world before the new world is saved -- `gui/rename`: add overlay to the "Prepare carefully" embark screen that transparently fixes a DF bug where you can't give units nicknames or custom professions -- `gui/notify`: new notification type: save reminder; appears if you have gone more than 15 minutes without saving; click to autosave - -## Fixes -- `fix/dry-buckets`: don't empty buckets for wells that are actively in use -- `gui/unit-info-viewer`: skill progress bars now show correct XP thresholds for skills past Legendary+5 -- `caravan`: no longer incorrectly identify wood-based plant items and plant-based soaps as being ethically unsuitable for trading with the elves -- `gui/design`: don't require an extra right click on the first cancel of building area designations -- `gui/gm-unit`: refresh unit sprite when profession is changed - -## Misc Improvements -- `immortal-cravings`: goblins and other naturally non-eating/non-drinking races will now also satisfy their needs for eating and drinking -- `caravan`: add filter for written works in display furniture assignment dialog -- `fix/wildlife`: don't vaporize stuck wildlife that is onscreen -- kill them instead (as if they died from old age) -- `gui/sitemap`: show primary group affiliation for visitors and invaders (e.g. civilization name or performance troupe) - -# 50.14-r2 - -## New Tools -- `fix/wildlife`: prevent wildlife from getting stuck when trying to exit the map. This fix needs to be enabled manually in `gui/control-panel` on the Bug Fixes tab since not all players want this bug to be fixed (you can intentionally stall wildlife incursions by trapping wildlife in an enclosed area so they are not caged but still cannot escape). -- `immortal-cravings`: allow immortals to satisfy their cravings for food and drink -- `justice`: pardon a criminal's prison sentence - -## New Features -- `force`: add support for a ``Wildlife`` event to allow additional wildlife to enter the map - -## Fixes -- `gui/quickfort`: only print a help blueprint's text once even if the repeat setting is enabled -- `makeown`: quell any active enemy or conflict relationships with converted creatures -- `makeown`: halt any hostile jobs the unit may be engaged in, like kidnapping -- `fix/loyaltycascade`: allow the fix to work on non-dwarven citizens -- `control-panel`: fix error when setting numeric preferences from the commandline -- `gui/quickfort`: fix build mode evaluation rules to allow placement of furniture and constructions on tiles with stair shapes or without orthagonal floors -- `emigration`: save-and-reload no longer resets the emigration cycle timeout -- `geld`, `ungeld`: save-and-reload no longer loses changes done by `geld` and `ungeld` for units who are historical figures -- `rejuvenate`: fix error when specifying ``--age`` parameter -- `gui/notify`: don't classify (peacefully) visiting night creatures as hostile -- `exportlegends`: ensure historical figure race filter is usable after re-entering legends mode with a different loaded world - -## Misc Improvements -- `idle-crafting`: also support making shell crafts for workshops with linked input stockpiles -- `gui/gm-editor`: automatically resolve and display names for ``language_name`` fields -- `fix/stuck-worship`: reduced console output by default. Added ``--verbose`` and ``--quiet`` options. -- `gui/design`: add dimensions tooltip to vanilla zone painting interface -- `necronomicon`: new ``--world`` option to list all secret-containing items in the entire world -- `gui/design`: new ``gui/design.rightclick`` overlay that allows you to cancel out of partially drawn box and minecart designations without canceling completely out of drawing mode - -## Removed -- `modtools/force`: merged into `force` - -# 50.13-r5 - -## New Tools -- `embark-anyone`: allows you to embark as any civilization, including dead and non-dwarven civs -- `idle-crafting`: allow dwarves to independently satisfy their need to craft objects -- `gui/family-affairs`: (reinstated) inspect or meddle with pregnancies, marriages, or lover relationships -- `notes`: attach notes to locations on a fort map - -## New Features -- `caravan`: DFHack dialogs for trade screens (both ``Bring goods to depot`` and the ``Trade`` barter screen) can now filter by item origins (foreign vs. fort-made) and can filter bins by whether they have a mix of ethically acceptable and unacceptable items in them -- `caravan`: If you have managed to select an item that is ethically unacceptable to the merchant, an "Ethics warning" badge will now appear next to the "Trade" button. Clicking on the badge will show you which items that you have selected are problematic. The dialog has a button that you can click to deselect the problematic items in the trade list. -- `confirm`: If you have ethically unacceptable items selected for trade, the "Are you sure you want to trade" confirmation will warn you about them -- `quickfort`: ``#zone`` blueprints now integrated with `preserve-rooms` so you can create a zone and automatically assign it to a noble or administrative role -- `exportlegends`: option to filter by race on historical figures page - -## Fixes -- `timestream`: ensure child growth events (that is, a child's transition to adulthood) are not skipped; existing "overage" children will be automatically fixed within a year -- `empty-bin`: ``--liquids`` option now correctly empties containers filled with LIQUID_MISC (like lye) -- `gui/design`: don't overcount "affected tiles" for Line & Freeform drawing tools -- `deep-embark`: fix error when embarking where there is no land to stand on (e.g. when embarking in the ocean with `gui/embark-anywhere`) -- `deep-embark`: fix failure to transport units and items when embarking where there is no room to spawn the starting wagon -- `gui/create-item`, `modtools/create-item`: items of type "VERMIN", "PET", "REMANS", "FISH", "RAW FISH", and "EGG" no longer spawn creature item "nothing" and will now stack correctly -- `rejuvenate`: don't set a lifespan limit for creatures that are immortal (e.g. elves, goblins) -- `rejuvenate`: properly disconnect babies from mothers when aging babies up to adults - -## Misc Improvements -- `gui/sitemap`: show whether a unit is friendly, hostile, or wild -- `gui/sitemap`: show whether a unit is caged -- `gui/control-panel`: include option for turning off dumping of old clothes for `tailor`, for players who have magma pit dumps and want to save old clothes from being dumped into the magma -- `position`: report current historical era (e.g., "Age of Myth"), site/adventurer world coords, and mouse map tile coords -- `position`: option to copy keyboard cursor position to the clipboard -- `assign-minecarts`: reassign vehicles to routes where the vehicle has been destroyed (or has otherwise gone missing) -- `fix/dry-buckets`: prompt DF to recheck requests for aid (e.g. "bring water" jobs) when a bucket is unclogged and becomes available for use -- `exterminate`: show descriptive names for the listed races in addition to their IDs -- `exterminate`: show actual names for unique creatures such as forgotten beasts and titans -- `fix/ownership`: now also checks and fixes room ownership links - -## Documentation -- `gui/embark-anywhere`: add information about how the game determines world tile pathability and instructions for bridging two landmasses - -# 50.13-r4 - -## New Features -- `gui/journal`: new automatic table of contents. add lines that start with "# ", like "# Entry for 502-04-02", to add hyperlinked headers to the table of contents - -## Fixes -- `full-heal`: fix ``-r --all_citizens`` option combination not resurrecting citizens -- `open-legends`: don't intercept text bound for vanilla legends mode search widgets -- `gui/unit-info-viewer`: correctly display skill levels when rust is involved -- `timestream`: fix dwarves spending too long eating and drinking -- `timestream`: fix jobs not being created at a sufficient rate, leading to dwarves standing around doing nothing -- `locate-ore`: fix sometimes selecting an incorrect tile when there are multiple mineral veins in a single map block -- `gui/settings-manager`: fix position of "settings restored" message on embark when the player has no saved embark profiles -- `build-now`: fix error when building buildings that (in previous DF versions) required the architecture labor -- `prioritize`: fix incorrect restoring of saved settings on Windows -- `list-waves`: no longer gets confused by units that leave the map and then return (e.g. squads who go out on raids) -- `fix/dead-units`: fix error when removing dead units from burrows and the unit with the greatest ID was dead -- `makeown`: ensure names given to adopted units (or units created with `gui/sandbox`) are respected later in legends mode -- `gui/autodump`: prevent dumping into walls or invalid map areas -- `gui/autodump`: properly turn items into projectiles when they are teleported into mid-air - -## Misc Improvements -- `build-now`: if `suspendmanager` is running, run an unsuspend cycle immediately before scanning for buildings to build -- `list-waves`: now outputs the names of the dwarves in each migration wave -- `list-waves`: can now display information about specific migration waves (e.g. ``list-waves 0`` to identify your starting 7 dwarves) -- `allneeds`: display distribution of needs by how severely they are affecting the dwarf - -# 50.13-r3 - -## New Tools -- `advtools`: collection of useful commands and overlays for adventure mode -- `advtools`: added an overlay that automatically fixes corrupt throwing/shooting state, preventing save/load crashes -- `advtools`: advtools party - promotes one of your companions to become a controllable adventurer -- `advtools`: advtools pets - fixes pets you gift in adventure mode. -- `pop-control`: (reinstated) limit the maximum size of migrant waves -- `bodyswap`: (reinstated) take control of another unit in adventure mode -- `gui/sitemap`: list and zoom to people, locations, and artifacts -- `devel/tree-info`: print a technical visualization of tree data -- `gui/tiletypes`: interface for modifying map tiles and tile properties -- `fix/population-cap`: fixes the situation where you continue to get migrant waves even when you are above your configured population cap -- `fix/occupancy`: fixes issues where you can't build somewhere because the game tells you an item/unit/building is in the way but there's nothing there -- `fix/sleepers`: (reinstated) fixes sleeping units belonging to a camp that never wake up. -- `timestream`: (reinstated) keep the game running quickly even when there are large numbers of units on the map -- `gui/journal`: fort journal with a multi-line text editor -- `devel/luacov`: (reinstated) add Lua script coverage reporting for use in testing and performance analysis - -## New Features -- `buildingplan`: dimension tooltip is now displayed for constructions and buildings that are designated over an area, like bridges and farm plots -- `gui/notify`: new notification type: injured citizens; click to zoom to injured units; also displays a warning if your hospital is not functional (or if you have no hospital) -- `gui/notify`: new notification type: drowning and suffocation progress bars for adventure mode -- `prioritize`: new info panel on under-construction buildings showing if the construction job has been taken and by whom. click to zoom to builder; toggle high priority status for job if it's not yet taken and you need it to be built ASAP -- `gui/unit-info-viewer`: new overlay for displaying progress bars for skills on the unit info sheet -- `gui/pathable`: new "Depot" mode that shows whether wagons can path to your trade depot -- `advtools`: automatically add a conversation option to "ask whereabouts of" for all your relationships (before, you could only ask whereabouts of people involved in rumors) -- `gui/design`: all-new visually-driven UI for much improved usability - -## Fixes -- `assign-profile`: fix handling of ``unit`` option for setting target unit id -- `gui/gm-unit`: correctly display skill levels above Legendary+5 -- `gui/gm-unit`: fix errors when editing/randomizing colors and body appearance -- `quickfort`: fix incorrect handling of stockpiles that are split into multiple separate areas but are given the same label (indicating that they should be part of the same stockpile) -- `makeown`: set animals to tame and domesticated -- `gui/sandbox`: spawned citizens can now be useful military squad members -- `gui/sandbox`: spawned undead now have a purple shade (only after save and reload, though) -- `caravan`: fix errors in trade dialog if all fort items are traded away while the trade dialog is showing fort items and the `confirm` trade confirmation is shown -- `control-panel`: restore non-default values of per-save enabled/disabled settings for repeat-based commands -- `confirm`: fix confirmation prompt behavior when overwriting a hotkey zoom location -- `quickfort`: allow farm plots to be built on muddy stone (as per vanilla behavior) -- `suspend`: remove broken ``--onlyblocking`` option; restore functionality to ``suspend all`` -- `gui/create-item`: allow creation of adamantine thread, wool, and yarn -- `gui/notify`: the notification panel no longer responds to the Enter key so Enter key is passed through to the vanilla UI -- `clear-smoke`: properly tag smoke flows for garbage collection to avoid memory leak -- `warn-stranded`: don't warn for babies carried by mothers who happen to be gathering fruit from trees -- `prioritize`: also boost priority of already-claimed jobs when boosting priority of a job type so those jobs are not interrupted -- `ban-cooking`: ban all seed producing items from being cooked when 'seeds' is chosen instead of just brewable seed producing items - -## Misc Improvements -- `item`: option for ignoring uncollected spider webs when you search for "silk" -- `gui/launcher`: "space space to toggle pause" behavior is skipped if the game was paused when `gui/launcher` came up to prevent accidental unpausing -- `gui/unit-info-viewer`: add precise unit size in cc (cubic centimeters) for comparison against the wiki values. you can set your preferred number format for large numbers like this in the preferences of `control-panel` or `gui/control-panel` -- `gui/unit-info-viewer`: now displays a unit's weight relative to a similarly-sized well-known creature (dwarves, elephants, or cats) -- `gui/unit-info-viewer`: shows a unit's size compared to the average for the unit's race -- `caravan`: optional overlay to hide vanilla "bring trade goods to depot" button (if you prefer to always use the DFHack version and don't want to accidentally click on the vanilla button). enable ``caravan.movegoods_hider`` in `gui/control-panel` UI Overlays tab to use. -- `caravan`: bring goods to depot screen now shows (approximate) distance from item to depot -- `gui/design`: circles are more circular (now matches more pleasing shape generated by ``digcircle``) -- `gui/quickfort`: you can now delete your blueprints from the blueprint load dialog -- `caravan`: remember filter settings for pedestal item assignment dialog -- `quickfort`: new ``delete`` command for deleting player-owned blueprints (library and mod-added blueprints cannot be deleted) -- `quickfort`: support enabling `logistics` features for autoforbid and autoclaim on stockpiles -- `gui/quickfort`: allow farm plots, dirt roads, and paved roads to be designated around partial obstructions without calling it an error, matching vanilla behavior -- `gui/launcher`: refresh default tag filter when mortal mode is toggled in `gui/control-panel` so changes to which tools autocomplete take effect immediately -- `gui/civ-alert`: you can now register multiple burrows as civilian alert safe spaces -- `exterminate`: add ``all`` target for convenient scorched earth tactics -- `empty-bin`: select a stockpile, tile, or building to empty all containers in the stockpile, tile, or building -- `exterminate`: add ``--limit`` option to limit number of exterminated creatures -- `exterminate`: add ``knockout`` and ``traumatize`` method for non-lethal incapacitation -- `caravan`: add shortcut to the trade request screen for selecting item types by value (e.g. so you can quickly select expensive gems or cheap leather) -- `gui/notify`: notification panel extended to apply to adventure mode -- `gui/control-panel`: highlight preferences that have been changed from the defaults -- `gui/quickfort`: buildings can now be constructed in a "high priority" state, giving them first dibs on `buildingplan` materials and setting their construction jobs to the highest priority -- `prioritize`: add ``ButcherAnimal`` to the default prioritization list (``SlaughterAnimal`` was already there, but ``ButcherAnimal`` -- which is different -- was missing) -- `prioritize`: list both unclaimed and total counts for current jobs when the --jobs option is specified -- `prioritize`: boost performance of script by not tracking number of times a job type was prioritized -- `gui/unit-syndromes`: make werecreature syndromes easier to search for - -## Removed -- `max-wave`: merged into `pop-control` -- `devel/find-offsets`, `devel/find-twbt`, `devel/prepare-save`: remove development scripts that are no longer useful -- `fix/item-occupancy`, `fix/tile-occupancy`: merged into `fix/occupancy` -- `adv-fix-sleepers`: renamed to `fix/sleepers` -- `adv-rumors`: merged into `advtools` - -# 50.13-r2 - -## New Tools -- Updated for adventure mode: `gui/sandbox`, `gui/create-item`, `gui/reveal` -- `ghostly`: (reinstated) allow your adventurer to phase through walls -- `markdown`: (reinstated) export description of selected unit or item to a text file -- `adaptation`: (reinstated) inspect or set unit cave adaptation levels -- `fix/engravings`: fix corrupt engraving tiles -- `unretire-anyone`: (reinstated) choose anybody in the world as an adventurer -- `reveal-adv-map`: (reinstated) reveal (or hide) the adventure map -- `resurrect-adv`: (reinstated) allow your adventurer to recover from death -- `flashstep`: (reinstated) teleport your adventurer to the mouse cursor - -## New Features -- `instruments`: new subcommand ``instruments order`` for creating instrument work orders - -## Fixes -- `modtools/create-item`: now functions properly when the ``reaction-gloves`` tweak is active -- `quickfort`: don't designate multiple tiles of the same tree for chopping when applying a tree chopping blueprint to a multi-tile tree -- `gui/quantum`: fix processing when creating a quantum dump instead of a quantum stockpile -- `caravan`: don't include undiscovered divine artifacts in the goods list -- `quickfort`: fix detection of valid tiles for wells -- `combine`: respect container volume limits - -## Misc Improvements -- `gui/autobutcher`: add shortcuts for butchering/unbutchering all animals -- `combine`: reduce combined drink sizes to 25 -- `gui/launcher`: add button for copying output to the system clipboard -- `deathcause`: automatically find and choose a corpse when a pile of mixed items is selected -- `gui/quantum`: add option for whether a minecart automatically gets ordered and/or attached -- `gui/quantum`: when attaching a minecart, show which minecart was attached -- `gui/quantum`: allow multiple feeder stockpiles to be linked to the minecart route -- `prioritize`: add PutItemOnDisplay jobs to the default prioritization list -- when these kinds of jobs are requested by the player, they generally want them done ASAP - -# 50.13-r1.1 - -## Fixes -- `gui/quantum`: accept all item types in the output stockpile as intended -- `deathcause`: fix error on run - -# 50.13-r1 - -## New Tools -- `gui/unit-info-viewer`: (reinstated) give detailed information on a unit, such as egg laying behavior, body size, birth date, age, and information about their afterlife behavior (if a ghost) -- `gui/quantum`: (reinstated) point and click interface for creating quantum stockpiles or quantum dumps - -## Fixes -- `open-legends`: don't interfere with the dragging of vanilla list scrollbars -- `gui/create-item`: properly restrict bags to bag materials by default -- `gui/create-item`: allow gloves and shoes to be made out of textiles by default -- `exterminate`: don't classify dangerous non-invader units as friendly (e.g. snatchers) - -## Misc Improvements -- `open-legends`: allow player to cancel the "DF will now exit" dialog and continue browsing -- `gui/gm-unit`: changes to unit appearance will now immediately be reflected in the unit portrait - -# 50.12-r3 - -## New Tools -- `gui/aquifer`: interactive aquifer visualization and editing -- `open-legends`: (reinstated) open legends mode directly from a loaded fort - -## New Features -- `quickfort`: add options for setting warm/damp dig markers when applying blueprints -- `gui/quickfort`: add options for setting warm/damp dig markers when applying blueprints -- `gui/reveal`: new "aquifer only" mode to only see hidden aquifers but not reveal any tiles -- `gui/notify`: optional notification for general wildlife (not on by default) - -## Fixes -- `fix/loyaltycascade`: fix edge case where loyalties of renegade units were not being fixed -- `quickfort`: reject tiles for building that contain magma or deep water -- `armoks-blessing`: fix error when making "Normal" attributes legendary -- `emigration`: remove units from burrows when they emigrate -- `agitation-rebalance`: fix calculated percent chance of cavern invasion -- `gui/launcher`: don't pop up a result dialog if a command run from minimal mode has no output - -## Misc Improvements -- `gui/reveal`: show aquifers even when not in mining mode -- `gui/control-panel`: add alternate "nodump" version for `cleanowned` that does not cause citizens to toss their old clothes in the dump. this is useful for players who would rather sell old clothes than incinerate them -- `agitation-rebalance`: when more than the maximum allowed cavern invaders are trying to enter the map, prefer keeping the animal people invaders instead of their war animals - -## Removed -- `drain-aquifer`: replaced by ``aquifer drain --all``; an alias now exists so ``drain-aquifer`` will automatically run the new command - -# 50.12-r2.1 - -## Fixes -- `fix/noexert-exhaustion`: fix typo in control panel registry entry which prevented the fix from being run when enabled -- `gui/suspendmanager`: fix script startup errors -- `control-panel`: properly auto-enable newly added bugfixes - -## Misc Improvements -- `gui/unit-syndromes`: make syndromes searchable by their display names (e.g. "necromancer") - -# 50.12-r2 - -## New Tools -- `agitation-rebalance`: alter mechanics of irritation-related attacks so they are less constant and are more responsive to recent player behavior -- `fix/ownership`: fix instances of multiple citizens claiming the same items, resulting in "Store owned item" job loops -- `fix/stuck-worship`: fix prayer so units don't get stuck in uninterruptible "Worship!" states -- `instruments`: provides information on how to craft the instruments used by the player civilization -- `modtools/item-trigger`: (reinstated) modder's resource for triggering scripted content when specific items are used -- `modtools/if-entity`: (reinstated) modder's resource for triggering scripted content depending on the race of the loaded fort -- `devel/block-borders`: (reinstated) highlights boundaries of map blocks or embark tile blocks -- `fix/noexert-exhaustion`: fix "Tired" NOEXERT units. Enabling via `gui/control-panel` prevents NOEXERT units from getting stuck in a "Tired" state - -## New Features -- `gui/settings-manager`: add import, export, and autoload for work details -- `exterminate`: new "disintegrate" kill method that additionally destroys carried items -- `quickfort`: allow setting of workshop profile properties (e.g. labor, skill restrictions) from build blueprints - -## Fixes -- `gui/launcher`: fix history scanning (Up/Down arrow keys) being slow to respond when in minimal mode -- `control-panel`: fix filtering not filtering when running the ``list`` command -- `gui/notify`: don't zoom to forbidden depots for merchants ready to trade notification -- `catsplosion`: only cause pregnancies in adults - -## Misc Improvements -- `gui/launcher`: add interface for browsing and filtering commands by tags -- `gui/launcher`: add support for history search (Alt-s hotkey) when in minimal mode -- `gui/launcher`: add support for the ``clear`` command and clearing the scrollback buffer -- `control-panel`: enable tweaks quietly on fort load so we don't spam the console -- `devel/tile-browser`: simplify interface now that SDL automatically normalizes texture scale -- `exterminate`: make race name matching case and space insensitive -- `gui/gm-editor`: support opening engraved art for inspection -- `gui/notify`: Shift click or Shift Enter on a zoomable notification to zoom to previous target -- `allneeds`: select a dwarf in the UI to see a summary of needs for just that dwarf -- `allneeds`: provide options for sorting the cumulative needs by different criteria -- `prioritize`: print out custom reaction and hauling jobs in the same format that is used for ``prioritize`` command arguments so the player can just copy and paste - -# 50.12-r1 - -## Fixes -- `gui/notify`: persist notification settings when toggled in the UI - -## Misc Improvements -- `gui/launcher`: developer mode hotkey restored to Ctrl-D - -# 50.11-r7 - -## New Tools -- `undump-buildings`: (reinstated) remove dump designation from in-use building materials -- `gui/petitions`: (reinstated) show outstanding (or all historical) petition agreements for guildhalls and temples -- `gui/notify`: display important notifications that vanilla doesn't support yet and provide quick zoom links to notification targets. -- `list-waves`: (reinstated) show migration wave information -- `make-legendary`: (reinstated) make a dwarf legendary in specified skills -- `combat-harden`: (reinstated) set a dwarf's resistance to being affected by visible corpses -- `add-thought`: (reinstated) add custom thoughts to a dwarf -- `devel/input-monitor`: interactive UI for debugging input issues - -## Fixes -- `gui/design`: clicking the center point when there is a design mark behind it will no longer simultaneously enter both mark dragging and center dragging modes. Now you can click once to move the shape, and click twice to move only the mark behind the center point. -- `fix/retrieve-units`: prevent pulling in duplicate units from offscreen -- `warn-stranded`: when there was at least one truly stuck unit and miners were actively mining, the miners were also confusingly shown in the stuck units list -- `source`: fix issue where removing sources would make some other sources inactive -- `caravan`: display book and scroll titles in the goods and trade dialogs instead of generic scroll descriptions -- `item`: avoid error when scanning items that have no quality rating (like bars and other construction materials) -- `gui/blueprint`: changed hotkey for setting blueprint origin tile so it doesn't conflict with default map movement keys -- `gui/control-panel`: fix error when toggling autostart settings - -## Misc Improvements -- `exportlegends`: make progress increase smoothly over the entire export and increase precision of progress percentage -- `gui/autobutcher`: ask for confirmation before zeroing out targets for all races -- `caravan`: move goods to trade depot dialog now allocates more space for the display of the value of very expensive items -- `extinguish`: allow selecting units/items/buildings in the UI to target them for extinguishing; keyboard cursor is only required for extinguishing map tiles that cannot be selected any other way -- `item`: change syntax so descriptions can be searched for without indicating the ``--description`` option. e.g. it's now ``item count royal`` instead of ``item count --description royal`` -- `item`: add ``--verbose`` option to print each item as it is matched -- `gui/mod-manager`: will automatically unmark the default mod profile from being the default if it fails to load (due to missing or incompatible mods) -- `gui/quickfort`: can now dynamically adjust the dig priority of tiles designated by dig blueprints -- `gui/quickfort`: can now opt to apply dig blueprints in marker mode - -## Removed -- `gui/manager-quantity`: the vanilla UI can now modify manager order quantities after creation -- `gui/create-tree`: replaced by `gui/sandbox` -- `warn-starving`: combined into `gui/notify` -- `warn-stealers`: combined into `gui/notify` - -# 50.11-r6 - -## Fixes -- `makeown`: fix error when adopting units that need a historical figure to be created -- `item`: fix missing item categories when using ``--by-type`` - -# 50.11-r5 - -## New Tools -- `control-panel`: new commandline interface for control panel functions -- `uniform-unstick`: (reinstated) force squad members to drop items that they picked up in the wrong order so they can get everything equipped properly -- `gui/reveal`: temporarily unhide terrain and then automatically hide it again when you're ready to unpause -- `gui/teleport`: mouse-driven interface for selecting and teleporting units -- `gui/biomes`: visualize and inspect biome regions on the map -- `gui/embark-anywhere`: bypass those pesky warnings and embark anywhere you want to -- `item`: perform bulk operations on groups of items. - -## New Features -- `uniform-unstick`: add overlay to the squad equipment screen to show a equipment conflict report and give you a one-click button to (attempt to) fix -- `gui/settings-manager`: save and load embark difficulty settings and standing orders; options for auto-load on new embark - -## Fixes -- `source`: water and magma sources and sinks now persist with fort across saves and loads -- `gui/design`: fix incorrect dimensions being shown when you're placing a stockpile, but a start coordinate hasn't been selected yet -- `warn-stranded`: don't warn for citizens who are only transiently stranded, like those on stepladders gathering plants or digging themselves out of a hole -- `ban-cooking`: fix banning creature alcohols resulting in error -- `confirm`: properly detect clicks on the remove zone button even when the unit selection screen is also open (e.g. the vanilla assign animal to pasture panel) -- `caravan`: ensure items are marked for trade when the move trade goods dialog is closed even when they were selected and then the list filters were changed such that the items were no longer actively shown -- `quickfort`: if a blueprint specifies an up/down stair, but the tile the blueprint is applied to cannot make an up stair (e.g. it has already been dug out), still designate a down stair if possible -- `suspendmanager`: correctly handle building collisions with smoothing designations when the building is on the edge of the map -- `empty-bin`: now correctly sends ammunition in carried quivers to the tile underneath the unit instead of teleporting them to an invalid (or possibly just far away) location - -## Misc Improvements -- `warn-stranded`: center the screen on the unit when you select one in the list -- `gui/control-panel`: reduce frequency for `warn-stranded` check to once every 2 days -- `gui/control-panel`: tools are now organized by type: automation, bugfix, and gameplay -- `confirm`: updated confirmation dialogs to use clickable widgets and draggable windows -- `confirm`: added confirmation prompt for right clicking out of the trade agreement screen (so your trade agreement selections aren't lost) -- `confirm`: added confirmation prompts for irreversible actions on the trade screen -- `confirm`: added confirmation prompt for deleting a uniform -- `confirm`: added confirmation prompt for convicting a criminal -- `confirm`: added confirmation prompt for re-running the embark site finder -- `confirm`: added confirmation prompt for reassigning or clearing zoom hotkeys -- `confirm`: added confirmation prompt for exiting the uniform customization page without saving -- `gui/autobutcher`: interface redesigned to better support mouse control -- `gui/launcher`: now persists the most recent 32KB of command output even if you close it and bring it back up -- `gui/quickcmd`: clickable buttons for command add/remove/edit operations -- `uniform-unstick`: warn if a unit belongs to a squad from a different site (can happen with migrants from previous forts) -- `gui/mass-remove`: can now differentiate planned constructions, stockpiles, and regular buildings -- `gui/mass-remove`: can now remove zones -- `gui/mass-remove`: can now cancel removal for buildings and constructions -- `fix/stuck-instruments`: now handles instruments that are left in the "in job" state but that don't have any actual jobs associated with them -- `gui/launcher`: make autocomplete case insensitive - -# 50.11-r4 - -## New Tools -- `build-now`: (reinstated) instantly complete unsuspended buildings that are ready to be built - -## Fixes -- `combine`: prevent stack sizes from growing beyond quantities that you would normally see in vanilla gameplay -- `gui/design`: Center dragging shapes now track the mouse correctly - -## Misc Improvements -- `caravan`: enable searching within containers in trade screen when in "trade bin with contents" mode - -# 50.11-r3 - -## New Tools -- `sync-windmills`: synchronize or randomize movement of active windmills -- `trackstop`: (reimplemented) integrated overlay for changing track stop and roller settings after construction - -## New Features -- `gui/design`: show selected dimensions next to the mouse cursor when designating with vanilla tools, for example when painting a burrow or designating digging -- `quickfort`: new ``burrow`` blueprint mode for designating or manipulating burrows -- `unforbid`: now ignores worn and tattered items by default (X/XX), use -X to bypass -- `fix/dead-units`: gained ability to scrub dead units from burrow membership lists - -## Fixes -- `gui/unit-syndromes`: show the syndrome names properly in the UI -- `emigration`: fix clearing of work details assigned to units that leave the fort - -## Misc Improvements -- `warn-stranded`: don't warn for units that are temporarily on unwalkable tiles (e.g. as they pass under a waterfall) - -## Removed -- `gui/control-panel`: removed always-on system services from the ``System`` tab: `buildingplan`, `confirm`, `logistics`, and `overlay`. The base services should not be turned off by the player. Individual confirmation prompts can be managed via `gui/confirm`, and overlays (including those for `buildingplan` and `logistics`) are managed on the control panel ``Overlays`` tab. -- `gui/control-panel`: removed `autolabor` from the ``Fort`` and ``Autostart`` tabs. The tool does not function correctly with the new labor types, and is causing confusion. You can still enable `autolabor` from the commandline with ``enable autolabor`` if you understand and accept its limitations. - -# 50.11-r2 - -## New Tools -- `add-recipe`: (reinstated) add reactions to your civ (e.g. for high boots if your civ didn't start with the ability to make high boots) -- `fix/corrupt-jobs`: prevents crashes by automatically removing corrupted jobs -- `burial`: (reinstated) create tomb zones for unzoned coffins - -## New Features -- `burial`: new options to configure automatic burial and limit scope to the current z-level -- `drain-aquifer`: gained ability to drain just above or below a certain z-level -- `drain-aquifer`: new option to drain all layers except for the first N aquifer layers, in case you want some aquifer layers but not too many -- `gui/control-panel`: ``drain-aquifer --top 2`` added as an autostart option - -## New Scripts -- `warn-stranded`: new repeatable maintenance script to check for stranded units, similar to `warn-starving` - -## Fixes -- `suspendmanager`: fix errors when constructing near the map edge -- `gui/sandbox`: fix scrollbar moving double distance on click -- `hide-tutorials`: fix the embark tutorial prompt sometimes not being skipped -- `full-heal`: fix removal of corpse after resurrection -- `toggle-kbd-cursor`: clear the cursor position when disabling, preventing the game from sometimes jumping the viewport around when cursor keys are hit - -## Misc Improvements -- `prioritize`: refuse to automatically prioritize dig and smooth/carve job types since it can break the DF job scheduler; instead, print a suggestion that the player use specialized units and vanilla designation priorities -- `gui/overlay`: filter overlays by current context so there are fewer on the screen at once and you can more easily click on the one you want to reposition -- `quickfort`: now allows constructions to be built on top of constructed floors and ramps, just like vanilla. however, to allow blueprints to be safely reapplied to the same area, for example to fill in buildings whose constructions were canceled due to lost items, floors will not be rebuilt on top of floors and ramps will not be rebuilt on top of ramps -- `gui/gm-editor`: for fields with primitive types, change from click to edit to click to select, double-click to edit. this should help prevent accidental modifications to the data and make hotkeys easier to use (since you have to click on a data item to use a hotkey on it) - -# 50.11-r1 - -## New Tools -- `startdwarf`: (reinstated) set number of starting dwarves - -## New Features -- `startdwarf`: overlay scrollbar so you can scroll through your starting dwarves if they don't all fit on the screen -- A new searchable, sortable, filterable dialog for selecting items for display on pedestals and display cases - -## Fixes -- `suspendmanager`: fixed a bug where floor grates, bars, bridges etc. wouldn't be recognised as walkable, leading to unnecessary suspensions in certain cases. - -## Misc Improvements -- `devel/inspect-screen`: display total grid size for UI and map layers -- `suspendmanager`: now suspends constructions that would cave-in immediately on completion - -# 50.10-r1 - -## Fixes -- 'fix/general-strike: fix issue where too many seeds were getting planted in farm plots - -# 50.09-r4 - -## Misc Improvements -- `autofish`: changed ``--raw`` argument format to allow explicit setting to on or off -- `caravan`: move goods to depot screen can now see/search/trade items inside of barrels and pots -- `gui/launcher`: show tagged tools in the autocomplete list when a tag name is typed - -# 50.09-r3 - -## New Tools -- `devel/scan-vtables`: scan and dump likely vtable addresses (for memory research) -- `hide-interface`: hide the vanilla UI elements for clean screenshots or laid-back fortress observing -- `hide-tutorials`: hide the DF tutorial popups; enable in the System tab of `gui/control-panel` -- `set-orientation`: tinker with romantic inclinations (reinstated from back catalog of tools) - -## New Features -- `exportlegends`: new overlay that integrates with the vanilla "Export XML" button. Now you can generate both the vanilla export and the extended data export with a single click! - -## Fixes -- `suspendmanager`: Fix the overlay enabling/disabling `suspendmanager` unexpectedly -- `caravan`: correct price adjustment values in trade agreement details screen -- `caravan`: apply both import and export trade agreement price adjustments to items being both bought or sold to align with how vanilla DF calculates prices -- `caravan`: cancel any active TradeAtDepot jobs if all caravans are instructed to leave -- `emigration`: fix errors loading forts after dwarves assigned to work details or workshops have emigrated -- `emigration`: fix citizens sometimes "emigrating" to the fortress site -- `suspendmanager`: improve the detection on "T" and "+" shaped high walls -- `starvingdead`: ensure sieges end properly when undead siegers starve -- `fix/retrieve-units`: fix retrieved units sometimes becoming duplicated on the map -- `quickfort`: cancel old dig jobs that point to a tile when a new designation is applied to the tile -- `gui/launcher`, `gui/gm-editor`: recover gracefully when the saved frame position is now offscreen -- `gui/sandbox`: correctly load equipment materials in modded games that categorize non-wood plants as wood - -## Misc Improvements -- `devel/lsmem`: added support for filtering by memory addresses and filenames -- `gui/gm-editor`: hold down shift and right click to exit, regardless of how many substructures deep you are -- `quickfort`: linked stockpiles and workshops can now be specified by ID instead of only by name. this is mostly useful when dynamically generating blueprints and applying them via the `quickfort` API -- `gui/quickfort`: blueprint details screen can now be closed with Ctrl-D (the same hotkey used to open the details) -- `suspendmanager`: display a different color for jobs suspended by suspendmanager -- `caravan`: optionally display items within bins in bring goods to depot screen -- `gui/gm-editor`: display in the title bar whether the editor window is scanning for live updates -- `gui/design`: change "auto commit" hotkey from ``c`` to ``Alt-c`` to avoid conflict with the default keybinding for z-level down -- `gui/liquids`: support removing river sources by converting them into stone floors - -# 50.09-r2 - -## New Scripts -- `caravan`: new trade screen UI replacements for bringing goods to trade depot and trading -- `fix/empty-wheelbarrows`: new script to empty stuck rocks from all wheelbarrows on the map - -## Fixes -- `gui/autodump`: when "include items claimed by jobs" is on, actually cancel the job so the item can be teleported -- `gui/gm-unit`: fix commandline processing when a unit id is specified -- `suspendmanager`: take in account already built blocking buildings -- `suspendmanager`: don't consider tree branches as a suitable access path to a building - -## Misc Improvements -- `gui/unit-syndromes`: make lists searchable -- `suspendmanager`: display the suspension reason when viewing a suspended building -- `quickfort`: blueprint libraries are now moddable -- add a ``blueprints/`` directory to your mod and they'll show up in `quickfort` and `gui/quickfort`! - -# 50.09-r1 - -## Misc Improvements -- `caravan`: new overlay for selecting all/none on trade request screen -- `suspendmanager`: don't suspend constructions that are built over open space - -# 50.08-r4 - -## Fixes -- `gui/create-item`: allow blocks to be made out of wood when using the restrictive filters -- `emigration`: reassign home site for emigrating units so they don't just come right back to the fort -- `gui/sandbox`: allow creatures that have separate caste-based graphics to be spawned (like ewes/rams) -- `gui/liquids`: ensure tile temperature is set correctly when painting water or magma -- `workorder`: prevent ``autoMilkCreature`` from over-counting milkable animals, which was leading to cancellation spam for the MilkCreature job -- `gui/quickfort`: allow traffic designations to be applied over buildings -- `gui/quickfort`: protect against meta blueprints recursing infinitely if they include themselves - -## Misc Improvements -- `gui/control-panel`: add some popular startup configuration commands for `autobutcher` and `autofarm` -- `gui/control-panel`: add option for running `fix/blood-del` on new forts (enabled by default) -- `gui/sandbox`: when creating citizens, give them names appropriate for their races -- `gui/autodump`: add option to clear the ``trader`` flag from teleported items, allowing you to reclaim items dropped by merchants -- `quickfort`: significant rewrite for DF v50! now handles zones, locations, stockpile configuration, hauling routes, and more -- `suspendmanager`: now suspends construction jobs on top of floor designations, protecting the designations from being erased -- `prioritize`: add wild animal management tasks and lever pulling to the default list of prioritized job types -- `suspendmanager`: suspend blocking jobs when building high walls or filling corridors -- `workorder`: reduce existing orders for automatic shearing and milking jobs when animals are no longer available -- `gui/quickfort`: adapt "cursor lock" to mouse controls so it's easier to see the full preview for multi-level blueprints before you apply them -- `gui/quickfort`: only display post-blueprint messages once when repeating the blueprint up or down z-levels -- `combine`: reduce max different stacks in containers to 30 to prevent containers from getting overfull - -## Removed -- `gui/automelt`: replaced by an overlay panel that appears when you click on a stockpile - -# 50.08-r2 - -## New Scripts -- `diplomacy`: view or alter diplomatic relationships -- `exportlegends`: (reinstated) export extended legends information for external browsing -- `modtools/create-item`: (reinstated) commandline and API interface for creating items -- `light-aquifers-only`: (reinstated) convert heavy aquifers to light -- `necronomicon`: search fort for items containing the secrets of life and death -- `fix/stuck-instruments`: fix instruments that are attached to invalid jobs, making them unusable. turn on automatic fixing in `gui/control-panel` in the ``Maintenance`` tab. -- `gui/mod-manager`: automatically restore your list of active mods when generating new worlds -- `gui/autodump`: point and click item teleportation and destruction interface (available only if ``armok`` tools are shown) -- `gui/sandbox`: creation interface for units, trees, and items (available only if ``armok`` tools are shown) -- `assign-minecarts`: (reinstated) quickly assign minecarts to hauling routes - -## Fixes -- `quickfort`: properly allow dwarves to smooth, engrave, and carve beneath walkable tiles of buildings -- `deathcause`: fix incorrect weapon sometimes being reported -- `gui/create-item`: allow armor to be made out of leather when using the restrictive filters -- `gui/design`: Fix building and stairs designation -- `quickfort`: fixed detection of tiles where machines are allowed (e.g. water wheels *can* be built on stairs if there is a machine support nearby) -- `quickfort`: fixed rotation of blueprints with carved track tiles - -## Misc Improvements -- `gui/quickfort`: blueprints that designate items for dumping/forbidding/etc. no longer show an error highlight for tiles that have no items on them -- `gui/quickfort`: place (stockpile layout) mode is now supported. note that detailed stockpile configurations were part of query mode and are not yet supported -- `gui/quickfort`: you can now generate manager orders for items required to complete blueprints -- `gui/create-item`: ask for number of items to spawn by default -- `light-aquifers-only`: now available as a fort Autostart option in `gui/control-panel`. note that it will only appear if "armok" tools are configured to be shown on the Preferences tab. -- `gui/gm-editor`: when passing the ``--freeze`` option, further ensure that the game is frozen by halting all rendering (other than for DFHack tool windows) -- `gui/gm-editor`: Alt-A now enables auto-update mode, where you can watch values change live when the game is unpaused - -# 50.08-r1 - -## Fixes -- `deteriorate`: ensure remains of enemy dwarves are properly deteriorated -- `suspendmanager`: Fix over-aggressive suspension of jobs that could still possibly be done (e.g. jobs that are partially submerged in water) - -## Misc Improvements -- `combine`: Now supports ammo, parts, powders, and seeds, and combines into containers -- `deteriorate`: add option to exclude useable parts from deterioration -- `gui/gm-editor`: press ``g`` to move the map to the currently selected item/unit/building -- `gui/gm-editor`: press ``Ctrl-D`` to toggle read-only mode to protect from accidental changes; this state persists across sessions -- `gui/gm-editor`: new ``--freeze`` option for ensuring the game doesn't change while you're inspecting it -- `gui/launcher`: DFHack version now shown in the default help text -- `gui/prerelease-warning`: widgets are now clickable - -# 50.07-r1 - -## Fixes --@ `caravan`: fix trade good list sometimes disappearing when you collapse a bin --@ `gui/gm-editor`: no longer nudges last open window when opening a new one -- `warn-starving`: no longer warns for dead units --@ `gui/control-panel`: the config UI for `automelt` is no longer offered when not in fortress mode - -## Misc Improvements -- `gui/gm-editor`: can now jump to material info objects from a mat_type reference with a mat_index using ``i`` -- `gui/gm-editor`: the key column now auto-fits to the widest key -- `prioritize`: revise and simplify the default list of prioritized jobs -- be sure to tell us if your forts are running noticeably better (or worse!) --@ `gui/control-panel`: add `faststart` to the system services - -# 50.07-beta2 - -## New Scripts -- `fix/general-strike`: fix known causes of the general strike bug (contributed by Putnam) -- `gui/seedwatch`: GUI config and status panel interface for `seedwatch` -- `gui/civ-alert`: configure and trigger civilian alerts - -## Fixes --@ `caravan`: item list length now correct when expanding and collapsing containers --@ `prioritize`: fixed all watched job type names showing as ``nil`` after a game load --@ `suspendmanager`: does not suspend non-blocking jobs such as floor bars or bridges anymore --@ `suspendmanager`: fix occasional bad identification of buildingplan jobs -- `warn-starving`: no longer warns for enemy and neutral units - -## Misc Improvements -- `gui/control-panel`: Now detects overlays from scripts named with capital letters -- `gui/cp437-table`: now has larger key buttons and clickable backspace/submit/cancel buttons, making it fully usable on the Steam Deck and other systems that don't have an accessible keyboard --@ `gui/design`: Now supports placing constructions using 'Building' mode. Inner and Outer tile constructions are configurable. Uses buildingplan filters set up with the regular buildingplan interface. -- `exterminate`: add support for ``vaporize`` kill method for when you don't want to leave a corpse -- `combine`: you can select a target stockpile in the UI instead of having to use the keyboard cursor -- `combine`: added ``--quiet`` option for no output when there are no changes -- `stripcaged`: added ``--skip-forbidden`` option for greater control over which items are marked for dumping -- `stripcaged`: items that are marked for dumping are now automatically unforbidden (unless ``--skip-forbidden`` is set) --@ `gui/control-panel`: added ``combine all`` maintenance option for automatic combining of partial stacks in stockpiles --@ `gui/control-panel`: added ``general-strike`` maintenance option for automatic fixing of (at least one cause of) the general strike bug -- `gui/cp437-table`: dialog is now fully controllable with the mouse, including highlighting which key you are hovering over and adding a clickable backspace button - -## Removed -- `autounsuspend`: replaced by `suspendmanager` --@ `gui/dig`: renamed to `gui/design` - -# 50.07-beta1 - -## New Scripts -- `suspendmanager`: automatic job suspension management (replaces `autounsuspend`) -- `gui/suspendmanager`: graphical configuration interface for `suspendmanager` -- `suspend`: suspends building construction jobs - -## Fixes --@ `quicksave`: now reliably triggers an autosave, even if one has been performed recently -- `gui/launcher`: tab characters in command output now appear as a space instead of a code page 437 "blob" - -## Misc Improvements -- `quickfort`: now reads player-created blueprints from ``dfhack-config/blueprints/`` instead of the old ``blueprints/`` directory. Be sure to move over your personal blueprints to the new directory! -- `gui/gm-editor`: can now open the selected stockpile if run without parameters - -# 50.07-alpha3 - -## Fixes --@ `gui/create-item`: fix generic corpsepiece spawning - -## Misc Improvements -- `gui/create-item`: added ability to spawn 'whole' corpsepieces (every layer of a part) --@ `gui/dig`: Allow placing an extra point (curve) while still placing the second main point --@ `gui/dig`: Allow placing n-point shapes, shape rotation/mirroring --@ `gui/dig`: Allow second bezier point, mirror-mode for freeform shapes, symmetry mode - -# 50.07-alpha2 - -## New Scripts -- `combine`: combines stacks of food and plant items. - -## Fixes --@ `troubleshoot-item`: fix printing of job details for chosen item --@ `makeown`: fixes errors caused by using makeown on an invader --@ `gui/blueprint`: correctly use setting presets passed on the commandline --@ `gui/quickfort`: correctly use settings presets passed on the commandline -- `devel/query`: can now properly index vectors in the --table argument --@ `forbid`: fix detection of unreachable items for items in containers --@ `unforbid`: fix detection of unreachable items for items in containers - -## Misc Improvements -- `troubleshoot-item`: output as bullet point list with indenting, with item description and ID at top -- `troubleshoot-item`: reports on items that are hidden, artifacts, in containers, and held by a unit -- `troubleshoot-item`: reports on the contents of containers with counts for each contained item type -- `devel/visualize-structure`: now automatically inspects the contents of most pointer fields, rather than inspecting the pointers themselves -- `devel/query`: will now search for jobs at the map coordinate highlighted, if no explicit job is highlighted and there is a map tile highlighted -- `caravan`: add trade screen overlay that assists with selecting groups of items and collapsing groups in the UI -- `gui/gm-editor`: will now inspect a selected building itself if the building has no current jobs - -## Removed -- `combine-drinks`: replaced by `combine` -- `combine-plants`: replaced by `combine` - -# 50.07-alpha1 - -## New Scripts -- `gui/design`: digging and construction designation tool with shapes and patterns -- `makeown`: makes the selected unit a citizen of your fortress - -## Fixes --@ `gui/unit-syndromes`: allow the window widgets to be interacted with --@ `fix/protect-nicks`: now works by setting the historical figure nickname --@ `gui/liquids`: fixed issues with unit pathing after adding/removing liquids --@ `gui/dig`: Fix for 'continuing' auto-stair designation. Avoid nil index issue for tile_type - -## Misc Improvements -- `gui/gm-editor`: now supports multiple independent data inspection windows -- `gui/gm-editor`: now prints out contents of coordinate vars instead of just the type -- `rejuvenate`: now takes an --age parameter to choose a desired age. --@ `gui/dig` : Added 'Line' shape that also can draw curves, added draggable center handle - -# 50.05-alpha3.1 - -## Fixes --@ `gui/launcher`: no longer resets to the Help tab on every keystroke - -# 50.05-alpha3 - -## New Scripts -- `autofish`: auto-manage fishing labors to control your stock of fish -- `gui/autofish`: GUI config and status panel interface for autofish -- `gui/automelt`: GUI config and status panel interface for automelt -- `gui/control-panel`: quick access to DFHack configuration -- `fix/civil-war`: removes negative relations with own government -- `fix/protect-nicks`: restore nicknames when DF loses them -- `forbid`: forbid and list forbidden items on the map -- `gui/unit-syndromes`: browser for syndrome information - -## Fixes -- `build-now`: now correctly avoids adjusting non-empty tiles above constructions that it builds -- `catsplosion`: now only affects live, active units -- `quickfort`: allow floor bars, floor grates, and hatches to be placed over all stair types like vanilla allows - -## Misc Improvements -- `ban-cooking`: ban announcements are now hidden by default; use new option ``--verbose`` to show them. -- `ban-cooking`: report number of items banned. -- `build-now`: now handles dirt roads and initializes farm plots properly -- `devel/click-monitor`: report on middle mouse button actions --@ `gui/autochop`: hide uninteresting burrows by default --@ `gui/blueprint`: allow map movement with the keyboard while the UI is open -- `gui/create-item`: support spawning corpse pieces (e.g. shells) under "body part" -- `gui/create-item`: added search and filter capabilities to the selection lists -- `gui/launcher`: make command output scrollback separate from the help text so players can continue to see the output of the previous command as they type the next one -- `gui/launcher`: allow double spacebar to pause/unpause the game, even while typing a command -- `gui/launcher`: clarify what is being shown in the autocomplete list (all commands, autocompletion of partially typed command, or commands related to typed command) -- `gui/launcher`: support running commands directly from the autocomplete list via double-clicking -- `gui/liquids`: interface overhaul, also now allows spawning river sources, setting/adding/removing liquid levels, and cleaning water from being salty or stagnant -- `gui/overlay`: now focuses on repositioning overlay widgets; enabling, disabling, and getting help for overlay widgets has moved to the new `gui/control-panel` --@ `gui/quickcmd`: now acts like a regular window instead of a modal dialog -- `gui/quickfort`: don't close the window when applying a blueprint so players can apply the same blueprint multiple times more easily -- `locate-ore`: now only searches revealed tiles by default -- `modtools/spawn-liquid`: sets tile temperature to stable levels when spawning water or magma --@ `prioritize`: pushing minecarts is now included in the default prioritization list -- `prioritize`: now automatically starts boosting the default list of job types when enabled -- `unforbid`: avoids unforbidding unreachable and underwater items by default -- `gui/create-item`: added whole corpse spawning alongside corpsepieces. (under "corpse") - -## Removed -- `show-unit-syndromes`: replaced by `gui/unit-syndromes`; html export is no longer supported - -# 50.05-alpha2 - -## Fixes --@ `gui/gm-editor`: fix errors displayed while viewing help screen -- `build-now`: don't error on constructions that do not have an item attached - -## Removed -- `create-items`: replaced by `gui/create-item` ``--multi`` - -# 50.05-alpha1 - -## New Scripts -- `gui/autochop`: configuration frontend and status monitor for the `autochop` plugin -- `devel/tile-browser`: page through available textures and see their texture ids -- `allneeds`: list all unmet needs sorted by how many dwarves suffer from them. - -## Fixes -- `make-legendary`: "MilitaryUnarmed" option now functional - -## Misc Improvements -- `autounsuspend`: now saves its state with your fort -- `emigration`: now saves its state with your fort -- `prioritize`: now saves its state with your fort -- `unsuspend`: overlay now displays different letters for different suspend states so they can be differentiated in graphics mode (P=planning, x=suspended, X=repeatedly suspended) -- `unsuspend`: overlay now shows a marker all the time when in graphics mode. ascii mode still only shows when paused so that you can see what's underneath. -- `gui/gm-editor`: converted to a movable, resizable, mouse-enabled window -- `gui/launcher`: now supports a smaller, minimal mode. click the toggle in the launcher UI or start in minimal mode via the ``Ctrl-Shift-P`` keybinding -- `gui/launcher`: can now be dragged from anywhere on the window body -- `gui/launcher`: now remembers its size and position between invocations -- `gui/gm-unit`: converted to a movable, resizable, mouse-enabled window -- `gui/cp437-table`: converted to a movable, mouse-enabled window -- `gui/quickcmd`: converted to a movable, resizable, mouse-enabled window -- `gui/quickcmd`: commands are now stored globally so you don't have to recreate commands for every fort -- `devel/inspect-screen`: updated for new rendering semantics and can now also inspect map textures -- `exterminate`: added drown method. magma and drown methods will now clean up liquids automatically. - -## Documentation -- `devel/hello-world`: updated to be a better example from which to start new gui scripts From 20dd70d0cc05b7393294d04c558485994a552532 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:50:30 +0200 Subject: [PATCH 59/93] multihaul: docs updated --- docs/multihaul.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/multihaul.rst b/docs/multihaul.rst index 1d0227ea9..d575187c5 100644 --- a/docs/multihaul.rst +++ b/docs/multihaul.rst @@ -3,7 +3,7 @@ multihaul .. dfhack-tool:: :summary: Haulers gather multiple nearby items when using wheelbarrows. - :tags: fort productivity items + :tags: fort productivity items stockpile This tool allows dwarves to collect several adjacent items at once when performing hauling jobs with a wheelbarrow. When enabled, new @@ -38,8 +38,8 @@ Options Attach at most this many additional items to each hauling job. Default is ``10``. ``--mode `` - Control which nearby items are attached. ``any`` collects any allowed items, - ``sametype`` only matches the item type, ``samesubtype`` requires type and + Control which nearby items are attached. ``any`` collects any items nearby, even if they are not related to an original jpb item, + ``sametype`` only matches the item type (like STONE or WOOD), ``samesubtype`` requires type and subtype to match, and ``identical`` additionally matches material. The default is ``sametype``. ``--debug`` From 640ca99e0627676216bff0ab7538cad1cc21efd6 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:54:11 +0200 Subject: [PATCH 60/93] multihaul: help updated --- docs/multihaul.rst | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/multihaul.rst b/docs/multihaul.rst index d575187c5..35ef2355a 100644 --- a/docs/multihaul.rst +++ b/docs/multihaul.rst @@ -8,13 +8,12 @@ multihaul This tool allows dwarves to collect several adjacent items at once when performing hauling jobs with a wheelbarrow. When enabled, new ``StoreItemInStockpile`` jobs will automatically attach nearby items so -they can be hauled in a single trip. Which items qualify can be controlled -with the ``--mode`` option. The script only triggers when a wheelbarrow is +they can be hauled in a single trip. Items claimed by another jobs would be ignored. The script only triggers when a wheelbarrow is definitively attached to the job. By default, up to ten additional items within 10 tiles of the original item are collected. -Jobs with wheelbarrows that are not assigned as push vehicles are ignored and -any stuck hauling jobs are automatically cleared. -Only items that match the destination stockpile filters are added to the job. +Warning: Destination stockpile filters are currently ignored by the job (because of DF logic). Which items qualify can be controlled +with the ``--mode`` option. +Basic usage of wheelbarrows remains the same: dwarfs would use them only if hauling item is heavier than 75 Usage ----- @@ -38,7 +37,7 @@ Options Attach at most this many additional items to each hauling job. Default is ``10``. ``--mode `` - Control which nearby items are attached. ``any`` collects any items nearby, even if they are not related to an original jpb item, + Control which nearby items are attached. ``any`` collects any items nearby, even if they are not related to an original job item, ``sametype`` only matches the item type (like STONE or WOOD), ``samesubtype`` requires type and subtype to match, and ``identical`` additionally matches material. The default is ``sametype``. From 347d2a290534222701c649050d8b9967316c436b Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 17:25:58 +0200 Subject: [PATCH 61/93] Delete gui/chronicle.lua --- gui/chronicle.lua | 65 ----------------------------------------------- 1 file changed, 65 deletions(-) delete mode 100644 gui/chronicle.lua diff --git a/gui/chronicle.lua b/gui/chronicle.lua deleted file mode 100644 index ba9fbe234..000000000 --- a/gui/chronicle.lua +++ /dev/null @@ -1,65 +0,0 @@ --- GUI viewer for chronicle entries ---@module=true - -local chronicle = reqscript('chronicle') -local gui = require('gui') -local widgets = require('gui.widgets') - -ChronicleView = defclass(ChronicleView, gui.FramedScreen) -ChronicleView.ATTRS{ - frame_title='Chronicle', - frame_style=gui.GREY_LINE_FRAME, - frame_width=60, - frame_height=20, - frame_inset=1, -} - -function ChronicleView:init() - self.entries = chronicle.get_full_entries() - self.start = 1 - self.start_min = 1 - self.start_max = math.max(1, #self.entries - self.frame_height + 1) -end - -function ChronicleView:onRenderBody(dc) - for i=self.start, math.min(#self.entries, self.start + self.frame_height - 1) do - dc:string(self.entries[i]):newline() - end - dc:pen(COLOR_LIGHTCYAN) - if self.start > self.start_min then - dc:seek(self.frame_width-1,0):char(24) - end - if self.start < self.start_max then - dc:seek(self.frame_width-1,self.frame_height-1):char(25) - end -end - -function ChronicleView:onInput(keys) - if keys.LEAVESCREEN or keys.SELECT then - self:dismiss() - view = nil - elseif keys.STANDARDSCROLL_UP then - self.start = math.max(self.start_min, self.start - 1) - elseif keys.STANDARDSCROLL_DOWN then - self.start = math.min(self.start_max, self.start + 1) - elseif keys.STANDARDSCROLL_PAGEUP then - self.start = math.max(self.start_min, self.start - self.frame_height) - elseif keys.STANDARDSCROLL_PAGEDOWN then - self.start = math.min(self.start_max, self.start + self.frame_height) - elseif keys.STANDARDSCROLL_TOP then - self.start = self.start_min - elseif keys.STANDARDSCROLL_BOTTOM then - self.start = self.start_max - else - ChronicleView.super.onInput(self, keys) - end -end - -function show() - view = view and view:raise() or ChronicleView{} - view:show() -end - -if not dfhack_flags.module then - show() -end From b187046d04c15fbec371ada4884aee6e02168711 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 17:26:26 +0200 Subject: [PATCH 62/93] Delete chronicle.lua --- chronicle.lua | 625 -------------------------------------------------- 1 file changed, 625 deletions(-) delete mode 100644 chronicle.lua diff --git a/chronicle.lua b/chronicle.lua deleted file mode 100644 index 6efe88e92..000000000 --- a/chronicle.lua +++ /dev/null @@ -1,625 +0,0 @@ --- Chronicles fortress events: unit deaths, item creation, and invasions ---@module = true - -local eventful = require('plugins.eventful') -local utils = require('utils') - -local help = [====[ -chronicle -======== - -Chronicles fortress events: unit deaths, item creation, and invasions - -Usage: - chronicle enable - chronicle disable - - chronicle [print] - prints 25 last recorded events - chronicle print [number] - prints last [number] recorded events - chronicle long - prints the full chronicle - chronicle export - saves current chronicle to a txt file - chronicle clear - erases current chronicle (DANGER) - - chronicle summary - shows how much items were produced per category in each year - - chronicle masterworks [enable|disable] - enables or disables logging of masterful crafted items events -]====] - -local GLOBAL_KEY = 'chronicle' -local MAX_LOG_CHARS = 2^15 -- trim chronicle to most recent ~32KB of text -local FULL_LOG_PATH = dfhack.getSavePath() .. '/chronicle_full.txt' - -local function get_default_state() - return { - entries = {}, - last_artifact_id = -1, - known_invasions = {}, - item_counts = {}, -- item creation summary per year - log_masterworks = true, -- capture "masterpiece" announcements - } -end - -state = state or get_default_state() - -local function persist_state() - dfhack.persistent.saveSiteData(GLOBAL_KEY, state) -end - -local months = { - 'Granite', 'Slate', 'Felsite', - 'Hematite', 'Malachite', 'Galena', - 'Limestone', 'Sandstone', 'Timber', - 'Moonstone', 'Opal', 'Obsidian', -} - -local seasons = { - 'Early Spring', 'Mid Spring', 'Late Spring', - 'Early Summer', 'Mid Summer', 'Late Summer', - 'Early Autumn', 'Mid Autumn', 'Late Autumn', - 'Early Winter', 'Mid Winter', 'Late Winter', -} - -local function ordinal(n) - local rem100 = n % 100 - local rem10 = n % 10 - local suffix = 'th' - if rem100 < 11 or rem100 > 13 then - if rem10 == 1 then suffix = 'st' - elseif rem10 == 2 then suffix = 'nd' - elseif rem10 == 3 then suffix = 'rd' - end - end - return ('%d%s'):format(n, suffix) -end - -local function format_date(year, ticks) - local day_of_year = math.floor(ticks / 1200) + 1 - local month = math.floor((day_of_year - 1) / 28) + 1 - local day = ((day_of_year - 1) % 28) + 1 - local month_name = months[month] or ('Month' .. tostring(month)) - local season = seasons[month] or 'Unknown Season' - return string.format('%s %s, %s of Year %d', ordinal(day), month_name, season, year) -end - -local function transliterate(str) - -- replace unicode punctuation with ASCII equivalents - str = str:gsub('[\226\128\152\226\128\153]', "'") -- single quotes - str = str:gsub('[\226\128\156\226\128\157]', '"') -- double quotes - str = str:gsub('\226\128\147', '-') -- en dash - str = str:gsub('\226\128\148', '-') -- em dash - str = str:gsub('\226\128\166', '...') -- ellipsis - - local accent_map = { - ['á']='a', ['à']='a', ['ä']='a', ['â']='a', ['ã']='a', ['å']='a', - ['Á']='A', ['À']='A', ['Ä']='A', ['Â']='A', ['Ã']='A', ['Å']='A', - ['é']='e', ['è']='e', ['ë']='e', ['ê']='e', - ['É']='E', ['È']='E', ['Ë']='E', ['Ê']='E', - ['í']='i', ['ì']='i', ['ï']='i', ['î']='i', - ['Í']='I', ['Ì']='I', ['Ï']='I', ['Î']='I', - ['ó']='o', ['ò']='o', ['ö']='o', ['ô']='o', ['õ']='o', - ['Ó']='O', ['Ò']='O', ['Ö']='O', ['Ô']='O', ['Õ']='O', - ['ú']='u', ['ù']='u', ['ü']='u', ['û']='u', - ['Ú']='U', ['Ù']='U', ['Ü']='U', ['Û']='U', - ['ç']='c', ['Ç']='C', ['ñ']='n', ['Ñ']='N', ['ß']='ss', - ['Æ']='AE', ['æ']='ae', ['Ø']='O', ['ø']='o', - ['Þ']='Th', ['þ']='th', ['Ð']='Dh', ['ð']='dh', - } - for k,v in pairs(accent_map) do - str = str:gsub(k, v) - end - return str -end - -local function sanitize(text) - -- convert game strings to UTF-8 and remove non-printable characters - local str = dfhack.df2utf(text or '') - -- strip control characters that may have leaked through - str = str:gsub('[%z\1-\31]', '') - str = transliterate(str) - -- strip quality wrappers from item names - -- e.g. -item-, +item+, *item*, ≡item≡, ☼item☼, «item» - str = str:gsub('%-([^%-]+)%-', '%1') - str = str:gsub('%+([^%+]+)%+', '%1') - str = str:gsub('%*([^%*]+)%*', '%1') - str = str:gsub('≡([^≡]+)≡', '%1') - str = str:gsub('☼([^☼]+)☼', '%1') - str = str:gsub('«([^»]+)»', '%1') - -- remove any stray wrapper characters that might remain - str = str:gsub('[☼≡«»]', '') - -- strip any remaining characters outside of latin letters, digits, and - -- basic punctuation - str = str:gsub("[^A-Za-z0-9%s%.:,;!'\"%?()%+%-]", '') - - return str -end - -local function read_external_entries() - local f = io.open(FULL_LOG_PATH, 'r') - if not f then return {} end - local lines = {} - for line in f:lines() do table.insert(lines, line) end - f:close() - return lines -end - -local function get_full_entries() - local entries = read_external_entries() - for _,e in ipairs(state.entries) do table.insert(entries, e) end - return entries -end - -local function trim_entries() - local total = 0 - local start_idx = #state.entries - while start_idx > 0 and total <= MAX_LOG_CHARS do - total = total + #state.entries[start_idx] + 1 - start_idx = start_idx - 1 - end - if start_idx > 0 then - local old = {} - for i=1,start_idx do table.insert(old, table.remove(state.entries, 1)) end - local ok, f = pcall(io.open, FULL_LOG_PATH, 'a') - if ok and f then - for _,e in ipairs(old) do f:write(e, '\n') end - f:close() - else - qerror('Cannot open file for writing: ' .. FULL_LOG_PATH) - end - end -end - -local function add_entry(text) - table.insert(state.entries, sanitize(text)) - trim_entries() - persist_state() -end - -local function export_chronicle(path) - path = path or (dfhack.getSavePath() .. '/chronicle.txt') - local ok, f = pcall(io.open, path, 'w') - if not ok or not f then - qerror('Cannot open file for writing: ' .. path) - end - for _,entry in ipairs(read_external_entries()) do - f:write(entry, '\n') - end - for _,entry in ipairs(state.entries) do - f:write(entry, '\n') - end - f:close() - print('Chronicle written to: ' .. path) -end - -local DEATH_TYPES = reqscript('gui/unit-info-viewer').DEATH_TYPES - -local function trim(str) - return str:gsub('^%s+', ''):gsub('%s+$', '') -end - -local function get_race_name(race_id) - return df.creature_raw.find(race_id).name[0] -end - -local function death_string(cause) - if cause == -1 then return 'died' end - return trim(DEATH_TYPES[cause] or 'died') -end - -local function describe_unit(unit) - local name = dfhack.units.getReadableName(unit) - if unit.name.nickname ~= '' and not name:find(unit.name.nickname, 1, true) then - name = name:gsub(unit.name.first_name, unit.name.first_name .. ' "' .. unit.name.nickname .. '"') - end - return name -end - -local FORT_DEATH_NO_KILLER = { - '%s has tragically died', - '%s met an untimely end', - '%s perished in sorrow', - '%s breathed their last in the halls of the fort', - '%s was lost to misfortune', - '%s passed into the stone', - '%s left this world too soon', - '%s faded from dwarven memory' -} - -local FORT_DEATH_WITH_KILLER = { - '%s was murdered by %s', - '%s fell victim to %s', - '%s was slain by %s', - '%s was claimed by the hand of %s', - '%s met their doom at the hands of %s', - '%s could not withstand the wrath of %s', - '%s was bested in deadly combat by %s', - '%s was struck down by %s' -} - -local FORT_DEATH_STRANGE = { - '%s met their mysterious demise', - '%s blood was drained completely', - '%s fate was sealed by darkest evil', - '%s disappeared without a trace', - '%s succumbed to an unnatural end', - '%s was claimed by forces unknown', - '%s fell prey to a sinister fate' -} - -local ENEMY_DEATH_WITH_KILLER = { - '%s granted a glorious death to %s', - '%s dispatched the wretched %s', - '%s vanquished pitiful %s', - '%s sent %s to the afterlife', - '%s laid low the hated %s', - '%s showed no mercy to %s', - '%s brought an end to %s’s rampage', - '%s struck down the enemy %s' -} - -local ENEMY_DEATH_NO_KILLER = { - '%s met their demise', - '%s found their end', - '%s succumbed to death', - '%s was brought low by fate', - '%s faded from existence', - '%s’s threat was ended', - '%s was erased from this world' -} - -local DANGEROUS_ENEMY_DEATH_WITH_KILLER = { - '%s has claimed victory over the dreaded %s!', - '%s has felled the fearsome %s in battle!', - '%s delivered the final blow to the infamous %s!', - '%s stood triumphant over the legendary %s!', - '%s shattered the legend of %s!', - '%s put an end to the reign of terror of %s!', - '%s conquered the monstrous %s in epic battle!', - '%s broke the might of %s!' -} - -local DANGEROUS_ENEMY_DEATH_NO_KILLER = { - '%s has fallen, their legend ends here.', - '%s has perished, their menace is no more.', - '%s was undone, their terror brought to an end.', - '%s vanished into myth, never to threaten again.', - '%s was swept away by fate itself.', - '%s crumbled under their own power.', - '%s slipped into oblivion, their tale finished.', - '%s faded into legend, never to rise again.' -} - - -local function random_choice(tbl) - return tbl[math.random(#tbl)] -end - -local function format_death_text(unit) - local victim = describe_unit(unit) - local incident = df.incident.find(unit.counters.death_id) - local killer - if incident and incident.criminal then - killer = df.unit.find(incident.criminal) - end - - if dfhack.units.isFortControlled(unit) then - if dfhack.units.isBloodsucker(killer) then - return string.format(random_choice(FORT_DEATH_STRANGE), victim) - elseif killer then - local killer_name = describe_unit(killer) - return string.format(random_choice(FORT_DEATH_WITH_KILLER), victim, killer_name) - else - return string.format(random_choice(FORT_DEATH_NO_KILLER), victim) - end - elseif dfhack.units.isInvader(unit) then - if killer then - local killer_name = describe_unit(killer) - return string.format(random_choice(ENEMY_DEATH_WITH_KILLER), killer_name, victim) - else - return string.format(random_choice(ENEMY_DEATH_NO_KILLER), victim) - end - elseif dfhack.units.isGreatDanger(unit) then - if killer then - local killer_name = describe_unit(killer) - return string.format(random_choice(ENEMY_DEATH_WITH_KILLER), killer_name, victim) - else - return string.format(random_choice(ENEMY_DEATH_NO_KILLER), victim) - end - else - local str = (unit.name.has_name and '' or 'The ') .. victim - str = str .. ' ' .. death_string(unit.counters.death_cause) - if killer then - str = str .. (', killed by the %s'):format(get_race_name(killer.race)) - if killer.name.has_name then - str = str .. (' %s'):format(dfhack.translation.translateName(dfhack.units.getVisibleName(killer))) - end - end - return str - end -end - -local CATEGORY_MAP = { - -- food and food-related - DRINK='food', DRINK2='food', FOOD='food', MEAT='food', FISH='food', - FISH_RAW='food', PLANT='food', PLANT_GROWTH='food', SEEDS='food', - EGG='food', CHEESE='food', POWDER_MISC='food', LIQUID_MISC='food', - GLOB='food', - -- weapons and defense - WEAPON='weapons', TRAPCOMP='weapons', - AMMO='ammo', SIEGEAMMO='ammo', - ARMOR='armor', PANTS='armor', HELM='armor', GLOVES='armor', - SHOES='armor', SHIELD='armor', QUIVER='armor', - -- materials - WOOD='wood', BOULDER='stone', ROCK='stone', ROUGH='gems', SMALLGEM='gems', - BAR='bars_blocks', BLOCKS='bars_blocks', - -- misc - COIN='coins', - -- finished goods and furniture - FIGURINE='finished_goods', AMULET='finished_goods', SCEPTER='finished_goods', - CROWN='finished_goods', RING='finished_goods', EARRING='finished_goods', - BRACELET='finished_goods', CRAFTS='finished_goods', TOY='finished_goods', - TOOL='finished_goods', GOBLET='finished_goods', FLASK='finished_goods', - BOX='furniture', BARREL='furniture', BED='furniture', CHAIR='furniture', - TABLE='furniture', DOOR='furniture', WINDOW='furniture', BIN='furniture', -} - -local IGNORE_TYPES = { - CORPSE=true, CORPSEPIECE=true, REMAINS=true, -} - -local ANNOUNCEMENT_PATTERNS = { - 'the enemy have come', - 'a vile force of darkness has arrived', - 'an ambush', - 'snatcher', - 'thief', - ' has bestowed the name ', - ' has been found dead', - 'you have ', - ' has come', - ' upon you', - 'tt is ', - 'is visiting', -} - -local function get_category(item) - local t = df.item_type[item:getType()] - return CATEGORY_MAP[t] or 'other' -end - -local function on_unit_death(unit_id) - local unit = df.unit.find(unit_id) - if not unit then return end - if dfhack.units.isWildlife(unit) then return end - local date = format_date(df.global.cur_year, df.global.cur_year_tick) - add_entry(string.format('%s: %s', date, format_death_text(unit))) -end - -local function on_item_created(item_id) - local item = df.item.find(item_id) - if not item then return end - - local date = format_date(df.global.cur_year, df.global.cur_year_tick) - - if item.flags.artifact then - local gref = dfhack.items.getGeneralRef(item, df.general_ref_type.IS_ARTIFACT) - local rec = gref and df.artifact_record.find(gref.artifact_id) or nil - if rec and rec.id > state.last_artifact_id then - state.last_artifact_id = rec.id - end - -- artifact announcements are captured via REPORT events - return - end - - local type_name = df.item_type[item:getType()] - if IGNORE_TYPES[type_name] then return end - - local year = df.global.cur_year - local category = get_category(item) - state.item_counts[year] = state.item_counts[year] or {} - state.item_counts[year][category] = (state.item_counts[year][category] or 0) + 1 - persist_state() -end - -local function on_invasion(invasion_id) - if state.known_invasions[invasion_id] then return end - state.known_invasions[invasion_id] = true - local date = format_date(df.global.cur_year, df.global.cur_year_tick) - add_entry(string.format('%s: Invasion started', date)) -end - --- capture artifact announcements from reports -local function transform_notification(text) - -- "You have " >> "Dwarves have " - if text:sub(1, 9) == "You have " then - text = "Dwarves have " .. text:sub(10) - end - - -- "Now you will know why you fear the night." >> "Gods have mercy!" - text = text:gsub("Now you will know why you fear the night%.", "Gods have mercy!") - - return text -end - -local pending_artifact_report -local function on_report(report_id) - local rep = df.report.find(report_id) - if not rep then return end - local text = sanitize(rep.text) - if pending_artifact_report then - if text:find(' offers it to ') or text:find(' claims it ') then - local date = format_date(df.global.cur_year, df.global.cur_year_tick) - add_entry(string.format('%s: %s %s', date, pending_artifact_report, text)) - pending_artifact_report = nil - return - else - local date = format_date(df.global.cur_year, df.global.cur_year_tick) - add_entry(string.format('%s: %s', date, pending_artifact_report)) - pending_artifact_report = nil - end - end - if text:find(' has created ') then - pending_artifact_report = text - return - end - - if state.log_masterworks and text:lower():find('has created a master') then - local date = format_date(df.global.cur_year, df.global.cur_year_tick) - add_entry(string.format('%s: %s', date, text)) - return - end - - for _,pattern in ipairs(ANNOUNCEMENT_PATTERNS) do - if text:lower():find(pattern) then - local date = format_date(df.global.cur_year, df.global.cur_year_tick) - local msg = transform_notification(text) - add_entry(string.format('%s: %s', date, msg)) - break - end -end -end - -local function transform_notification(text) - -- "You have " >> "Dwarves have " - if text:sub(1, 9) == "You have " then - text = "Dwarves have " .. text:sub(10) - end - - -- "Now you will know why you fear the night." >> "Gods have mercy!" - text = text:gsub("Now you will know why you fear the night%.", "Gods have mercy!") - - return text -end - -local function do_enable() - state.enabled = true - eventful.enableEvent(eventful.eventType.ITEM_CREATED, 100) - eventful.enableEvent(eventful.eventType.INVASION, 100) - eventful.enableEvent(eventful.eventType.REPORT, 100) - eventful.enableEvent(eventful.eventType.UNIT_DEATH, 100) - eventful.onUnitDeath[GLOBAL_KEY] = on_unit_death - eventful.onItemCreated[GLOBAL_KEY] = on_item_created - eventful.onInvasion[GLOBAL_KEY] = on_invasion - eventful.onReport[GLOBAL_KEY] = on_report - persist_state() -end - -local function do_disable() - state.enabled = false - eventful.onUnitDeath[GLOBAL_KEY] = nil - eventful.onItemCreated[GLOBAL_KEY] = nil - eventful.onInvasion[GLOBAL_KEY] = nil - eventful.onReport[GLOBAL_KEY] = nil - persist_state() -end - -local function load_state() - state = get_default_state() - utils.assign(state, dfhack.persistent.getSiteData(GLOBAL_KEY, state)) -end - --- State change hook -dfhack.onStateChange[GLOBAL_KEY] = function(sc) - if sc == SC_MAP_UNLOADED then - eventful.onUnitDeath[GLOBAL_KEY] = nil - eventful.onItemCreated[GLOBAL_KEY] = nil - eventful.onInvasion[GLOBAL_KEY] = nil - eventful.onReport[GLOBAL_KEY] = nil - state.enabled = false - return - end - if sc ~= SC_MAP_LOADED or not dfhack.world.isFortressMode() then - return - end - - load_state() - if state.enabled then - do_enable() - end -end - -if dfhack.isMapLoaded() and dfhack.world.isFortressMode() then - load_state() - if state.enabled then - do_enable() - end -end - -local function main(args) - if not dfhack.world.isFortressMode() or not dfhack.isMapLoaded() then - qerror('chronicle requires a loaded fortress map') - end - - load_state() - local cmd = args[1] or 'print' - - if cmd == 'enable' then - do_enable() - elseif cmd == 'disable' then - do_disable() - elseif cmd == 'clear' then - state.entries = {} - persist_state() - elseif cmd == 'masterworks' then - local sub = args[2] - if sub == 'enable' then - state.log_masterworks = true - elseif sub == 'disable' then - state.log_masterworks = false - else - print(string.format('Masterwork logging is currently %s.', - state.log_masterworks and 'enabled' or 'disabled')) - return - end - persist_state() - elseif cmd == 'export' then - export_chronicle(args[2]) - elseif cmd == 'long' then - local entries = get_full_entries() - if #entries == 0 then - print('Chronicle is empty.') - else - for _,entry in ipairs(entries) do print(entry) end - end - elseif cmd == 'print' then - local count = tonumber(args[2]) or 25 - if #state.entries == 0 then - print('Chronicle is empty.') - else - local start_idx = math.max(1, #state.entries - count + 1) - for i = start_idx, #state.entries do - print(state.entries[i]) - end - end - elseif cmd == 'view' then - if #get_full_entries() == 0 then - print('Chronicle is empty.') - else - reqscript('gui/chronicle').show() - end - elseif cmd == 'summary' then - local years = {} - for year in pairs(state.item_counts) do table.insert(years, year) end - table.sort(years) - if #years == 0 then - print('No item creation records.') - return - end - for _,year in ipairs(years) do - local counts = state.item_counts[year] - local parts = {} - for cat,count in pairs(counts) do - table.insert(parts, string.format('%d %s', count, cat)) - end - table.sort(parts) - print(string.format('Year %d: %s', year, table.concat(parts, ', '))) - end - else - print(help) - end - - persist_state() -end - -if not dfhack_flags.module then - main({...}) -end From acaa6715a5e72676692c67926651029d67218196 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 17:27:02 +0200 Subject: [PATCH 63/93] Delete holy-war.lua --- holy-war.lua | 200 --------------------------------------------------- 1 file changed, 200 deletions(-) delete mode 100644 holy-war.lua diff --git a/holy-war.lua b/holy-war.lua deleted file mode 100644 index 71227dafe..000000000 --- a/holy-war.lua +++ /dev/null @@ -1,200 +0,0 @@ --- Trigger wars based on conflicts between deity spheres ---@ module = true - -local utils = require('utils') - -local help = [====[ -holy-war -======== - -Provoke wars with civilizations that do not share deity spheres with -your civilization or the temples in your fortress. Religious -persecution grudges in the historical record also trigger war. - -Usage: - holy-war [--dry-run] - -If ``--dry-run`` is specified, no diplomatic changes are made and a list of -potential targets is printed instead. -]====] - -local valid_args = utils.invert({ 'dry-run', 'help' }) - -local RELIGIOUS_PERSECUTION_GRUDGE = - df.vague_relationship_type.RELIGIOUS_PERSECUTION_GRUDGE or - df.vague_relationship_type.religious_persecution_grudge - -local function merge(dst, src) - for k in pairs(src) do dst[k] = true end -end - -local function spheres_to_str(spheres) - local names = {} - for sph in pairs(spheres) do - table.insert(names, df.sphere_type[sph]) - end - table.sort(names) - return table.concat(names, ', ') -end - -local function diff(a, b) - local d = {} - for k in pairs(a) do - if not b[k] then d[k] = true end - end - return d -end - -local function get_deity_spheres(hfid) - local spheres = {} - local hf = df.historical_figure.find(hfid) - if hf and hf.info and hf.info.metaphysical then - for _, sph in ipairs(hf.info.metaphysical.spheres) do - spheres[sph] = true - end - end - return spheres -end - -local function get_civ_spheres(civ) - local spheres = {} - for _, deity_id in ipairs(civ.relations.deities) do - merge(spheres, get_deity_spheres(deity_id)) - end - return spheres -end - -local function get_fort_spheres() - local spheres = {} - for _, bld in ipairs(df.global.world.buildings.all) do - if bld:getType() == df.building_type.Temple then - local dtype = bld.deity_type - if dtype == df.religious_practice_type.WORSHIP_HFID then - merge(spheres, get_deity_spheres(bld.deity_data.HFID)) - elseif dtype == df.religious_practice_type.RELIGION_ENID then - local rciv = df.historical_entity.find(bld.deity_data.Religion) - if rciv then merge(spheres, get_civ_spheres(rciv)) end - end - end - end - return spheres -end - -local function union(a, b) - local u = {} - merge(u, a) - merge(u, b) - return u -end - -local function share(p1, p2) - for k in pairs(p1) do - if p2[k] then return true end - end - return false -end - -local function get_civ_hists(civ) - local hfs = {} - for _, id in ipairs(civ.histfig_ids) do hfs[id] = true end - return hfs -end - -local function has_religious_grudge(p_hfs, t_hfs) - if not RELIGIOUS_PERSECUTION_GRUDGE then return false end - for _, set in ipairs(df.global.world.history.relationship_events) do - for i = 0, set.next_element-1 do - if set.relationship[i] == RELIGIOUS_PERSECUTION_GRUDGE then - local src = set.source_hf[i] - local tgt = set.target_hf[i] - if (p_hfs[src] and t_hfs[tgt]) or (p_hfs[tgt] and t_hfs[src]) then - return true - end - end - end - end - return false -end - -local function change_relation(target, relation) - local pciv = df.historical_entity.find(df.global.plotinfo.civ_id) - for _, state in ipairs(pciv.relations.diplomacy.state) do - if state.group_id == target.id then - state.relation = relation - end - end - for _, state in ipairs(target.relations.diplomacy.state) do - if state.group_id == pciv.id then - state.relation = relation - end - end -end - -local function main(...) - local args = utils.processArgs({...}, valid_args) - - if args.help then - print(help) - return - end - - local dry_run = args['dry-run'] - local pciv = df.historical_entity.find(df.global.plotinfo.civ_id) - local player_spheres = union(get_civ_spheres(pciv), get_fort_spheres()) - local player_hfs = get_civ_hists(pciv) - - for _, civ in ipairs(df.global.world.entities.all) do - if civ.type == 0 and civ.id ~= pciv.id then - local p_status - for _, state in ipairs(pciv.relations.diplomacy.state) do - if state.group_id == civ.id then - p_status = state.relation - break - end - end - local c_status - for _, state in ipairs(civ.relations.diplomacy.state) do - if state.group_id == pciv.id then - c_status = state.relation - break - end - end - if p_status ~= 1 or c_status ~= 1 then -- not already mutually at war - local civ_spheres = get_civ_spheres(civ) - local civ_hfs = get_civ_hists(civ) - local divine_conflict = next(player_spheres) and next(civ_spheres) - and not share(player_spheres, civ_spheres) - local persecution = has_religious_grudge(player_hfs, civ_hfs) - if (divine_conflict or persecution) and civ.name.has_name then - local name = dfhack.translation.translateName(civ.name, true) - local reason_parts = {} - if divine_conflict then - local p_diff = diff(player_spheres, civ_spheres) - local c_diff = diff(civ_spheres, player_spheres) - table.insert(reason_parts, - ('conflicting spheres (%s vs %s)'):format( - spheres_to_str(p_diff), - spheres_to_str(c_diff))) - end - if persecution then - table.insert(reason_parts, 'religious persecution') - end - local reason = table.concat(reason_parts, ' and ') - if dry_run then - print(('Would declare war on %s due to %s.'):format(name, reason)) - else - change_relation(civ, 1) -- war - dfhack.gui.showAnnouncement( - ('%s sparks war with %s!'):format( - reason:gsub('^.', string.upper), name), - COLOR_RED, true) - end - end - end - end - end -end - -if not dfhack_flags.module then - main(...) -end From a9b48d153b696c4ab3706ae4773c119a75e4b7bb Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 17:29:39 +0200 Subject: [PATCH 64/93] Delete docs/need-acquire.rst --- docs/need-acquire.rst | 34 ---------------------------------- 1 file changed, 34 deletions(-) delete mode 100644 docs/need-acquire.rst diff --git a/docs/need-acquire.rst b/docs/need-acquire.rst deleted file mode 100644 index eeaaae0ef..000000000 --- a/docs/need-acquire.rst +++ /dev/null @@ -1,34 +0,0 @@ -need-acquire -============ - -.. dfhack-tool:: - :summary: Give trinkets to citizens to satisfy the Acquire Object need. - :tags: fort gameplay happiness - -Assigns free jewelry items to dwarves who have a strong ``Acquire Object`` need. -The script searches for unowned earrings, rings, amulets, and bracelets and -assigns them to dwarves whose focus level for the need falls below a configurable -threshold. - -Usage ------ - -``need-acquire [-t ]`` - Give trinkets to all dwarves whose focus level is below ``-``. - The default threshold is ``-3000``. - -Examples --------- - -``need-acquire`` - Use the default focus threshold of ``-3000``. -``need-acquire -t 2000`` - Fulfill the need for dwarves whose focus drops below ``-2000``. - -Options -------- - -``-t`` ```` - Focus level below which the need is considered unmet. -``-help`` - Show the help text. From 2e45d27073dfe0de01b7282ae555eb9e8c602c92 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 17:29:46 +0200 Subject: [PATCH 65/93] Delete docs/wheelbarrow-multi.rst --- docs/wheelbarrow-multi.rst | 34 ---------------------------------- 1 file changed, 34 deletions(-) delete mode 100644 docs/wheelbarrow-multi.rst diff --git a/docs/wheelbarrow-multi.rst b/docs/wheelbarrow-multi.rst deleted file mode 100644 index afeb9c952..000000000 --- a/docs/wheelbarrow-multi.rst +++ /dev/null @@ -1,34 +0,0 @@ -wheelbarrow-multi -================= - -.. dfhack-tool:: - :summary: Load multiple items into wheelbarrows with one job. - :tags: fort productivity items - -This tool allows dwarves to gather several adjacent items at once when -loading wheelbarrows. When enabled, new ``StoreItemInVehicle`` jobs will -automatically attach nearby items so they can be hauled in a single trip. -By default, up to four additional items within one tile of the original -item are collected. - -Usage ------ - -:: - - wheelbarrow-multi enable [] - wheelbarrow-multi disable - wheelbarrow-multi status - wheelbarrow-multi config [] - -Options -------- - -``--radius `` - Search this many tiles around the target item for additional items. Default - is ``1``. -``--max-items `` - Attach at most this many additional items to each job. Default is ``4``. -``--debug`` - Show debug messages via ``dfhack.gui.showAnnouncement`` when items are - attached. Use ``--no-debug`` to disable. From 830de7a6011e8b0eb94848b941d73fb984c3ea26 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 17:29:54 +0200 Subject: [PATCH 66/93] Delete docs/chronicle.rst --- docs/chronicle.rst | 69 ---------------------------------------------- 1 file changed, 69 deletions(-) delete mode 100644 docs/chronicle.rst diff --git a/docs/chronicle.rst b/docs/chronicle.rst deleted file mode 100644 index a12799e8e..000000000 --- a/docs/chronicle.rst +++ /dev/null @@ -1,69 +0,0 @@ -chronicle -========= - -.. dfhack-tool:: - :summary: Record fortress events like deaths, item creation, and invasions. - :tags: fort gameplay - -This tool automatically records notable events in a chronicle that is stored -with your save. Unit deaths now include the cause of death as well as any -titles, nicknames, or noble positions held by the fallen. Death entries also -differentiate fortress citizens, invading enemies, and other visitors. Fortress -citizens are memorialized with tragic or murderous phrasing, while invading -enemies receive a mocking epitaph. Artifact creation events, invasions, mission -reports, and yearly totals of -crafted items are also recorded. Announcements for masterwork creations can be -toggled on or off and are enabled by default. Artifact entries include the full -announcement text from the game, and output text is sanitized so that any -special characters are replaced with simple Latin equivalents. - -The chronicle file is trimmed to about 32KB of the most recent text so that it -doesn't exceed DFHack's persistence limits. Trimmed entries are automatically -appended to ``chronicle_full.txt`` in your save folder so nothing is lost. - -Usage ------ - -:: - - chronicle enable - chronicle disable - chronicle print [count] - chronicle long - chronicle summary - chronicle clear - chronicle masterworks - chronicle export [filename] - chronicle view - -``chronicle enable`` - Start recording events in the current fortress. -``chronicle disable`` - Stop recording events. -``chronicle print`` - Print the most recent recorded events. Takes an optional ``count`` - argument (default ``25``) that specifies how many events to show. Prints - a notice if the chronicle is empty. -``chronicle summary`` - Show yearly totals of created items by category (non-artifact items only). -``chronicle clear`` - Delete the chronicle. -``chronicle masterworks`` - Enable or disable logging of masterwork creation announcements. When run - with no argument, displays the current setting. -``chronicle export`` - Write all recorded events to a text file. If ``filename`` is omitted, the - output is saved as ``chronicle.txt`` in your save folder. -``chronicle long`` - Print the entire chronicle, including older entries stored in - ``chronicle_full.txt``. -``chronicle view`` - Display the full chronicle in a scrollable window. - -Examples --------- - -``chronicle print 10`` - Show the 10 most recent chronicle entries. -``chronicle summary`` - Display yearly summaries of items created in the fort. From e1e3c6517585c39abadbc2e89c1ad696bb17c197 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 17:30:02 +0200 Subject: [PATCH 67/93] Delete docs/holy-war.rst --- docs/holy-war.rst | 46 ---------------------------------------------- 1 file changed, 46 deletions(-) delete mode 100644 docs/holy-war.rst diff --git a/docs/holy-war.rst b/docs/holy-war.rst deleted file mode 100644 index 5c8fa3c70..000000000 --- a/docs/holy-war.rst +++ /dev/null @@ -1,46 +0,0 @@ -holy-war -======== - -.. dfhack-tool:: - :summary: Start wars when religions clash. - :tags: fort gameplay diplomacy - -This tool compares the spheres of influence represented by the gods of -nearby civilizations with those worshipped by your civilization and -represented in fortress temples. If no spheres overlap, or if the -historical record shows a ``religious_persecution_grudge`` between the -two peoples, the civilization is set to war. Both your stance toward -the other civilization and their stance toward you are set to war, -ensuring a mutual declaration. - -Civilizations without proper names are ignored, and the reported sphere -lists contain only the spheres unique to each civilization. - -Usage ------ - -:: - - holy-war [--dry-run] - -When run without options, wars are declared immediately on all -qualifying civilizations and an announcement is displayed. With -``--dry-run``, the tool only reports which civilizations would be -affected without actually changing diplomacy. Each message also notes -whether the conflict arises from disjoint spheres of influence or a -religious persecution grudge and lists the conflicting spheres when -appropriate. - -Examples --------- - -``holy-war`` - Immediately declare war on all qualifying civilizations. -``holy-war --dry-run`` - Show which civilizations would be targeted without changing diplomacy. - -Options -------- - -``--dry-run`` - List potential targets without declaring war. From b96a5bf76ef1c56811794d6d479bbe76117f99c1 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 17:32:03 +0200 Subject: [PATCH 68/93] Add files via upload --- changelog.txt | 1022 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1022 insertions(+) create mode 100644 changelog.txt diff --git a/changelog.txt b/changelog.txt new file mode 100644 index 000000000..a937e2fb2 --- /dev/null +++ b/changelog.txt @@ -0,0 +1,1022 @@ +===[[[ +This file contains changes specific to the scripts repo. See docs/changelog.txt +in the dfhack repo for a full description, or +https://docs.dfhack.org/en/latest/docs/dev/Documentation.html#building-the-changelogs + +NOTE: currently, gen_changelog.py expects a "Future" section to exist at the +top of this file (even if no changes are listed under it), or you will get a +"Entry without section" error. Also, to maintain proper sorting in the generated +changelogs when making a new release, docs/changelog.txt in the dfhack repo must +have the new release listed in the right place, even if no changes were made in +that repo. + +Template for new versions: + +## New Tools + +## New Features + +## Fixes +- `gui/gm-unit`: remove reference to ``think_counter``, removed in v51.12 +- fixed references to removed ``unit.curse`` compound + +## Misc Improvements + +## Removed + +]]] + +# Future + +## New Tools + +## New Features + +## Fixes +- `gui/journal`: fix typo which caused the table of contents to always be regenerated even when not needed +- `gui/mod-manager`: gracefully handle mods with missing or broken ``info.txt`` files +- `uniform-unstick`: resolve overlap with new buttons in 51.13 + +## Misc Improvements + +## Removed + +# 51.12-r1 + +## New Tools +- `deteriorate`: (reinstated) allow corpses, body parts, food, and/or damaged clothes to rot away +- `modtools/moddable-gods`: (reinstated) create new deities from scratch + +## New Features +- `gui/spectate`: added "Prefer nicknamed" to the list of options +- `gui/mod-manager`: when run in a loaded world, shows a list of active mods -- click to export the list to the clipboard for easy sharing or posting +- `gui/blueprint`: now records zone designations +- `gui/design`: add option to draw N-point stars, hollow or filled or inverted, and change the main axis to orient in any direction + +## Fixes +- `starvingdead`: properly restore to correct enabled state when loading a new game that is different from the first game loaded in this session +- `starvingdead`: ensure undead decay does not happen faster than the declared decay rate when saving and loading the game +- `gui/design`: prevent line thickness from extending outside the map boundary + +## Misc Improvements +- `remove-stress`: also applied to long-term stress, immediately removing stressed and haggard statuses + +# 51.11-r1 + +## Fixes +- `list-agreements`: fix date math when determining petition age +- `gui/petitions`: fix date math when determining petition age +- `gui/rename`: fix commandline processing when manually specifying target ids +- `gui/sandbox`: restore metal equipment options when spawning units + +## Misc Improvements +- `fix/loyaltycascade`: now also breaks up brawls and other intra-fort conflicts that *look* like loyalty cascades +- `makeown`: remove selected unit from any current conflicts so they don't just start attacking other citizens when you make them a citizen of your fort + +# 51.09-r1 + +## New Features +- `gui/mass-remove`: add a button to the bottom toolbar when eraser mode is active for launching `gui/mass-remove` +- `idle-crafting`: default to only considering happy and ecstatic units for the highest need threshold +- `gui/sitemap`: add a button to the toolbar at the bottom left corner of the screen for launching `gui/sitemap` + +## Fixes +- `idle-crafting`: check that units still have crafting needs before creating a job for them +- `gui/journal`: prevent pause/unpause events from leaking through the UI when keys are mashed + +# 51.07-r1 + +## New Tools +- `devel/export-map`: export map tile data to a JSON file +- `autocheese`: automatically make cheese using barrels that have accumulated sufficient milk +- `gui/spectate`: interactive UI for configuring `spectate` +- `gui/notes`: UI for adding and managing notes attached to tiles on the map +- `launch`: (reinstated) new adventurer fighting move: thrash your enemies with a flying suplex +- `putontable`: (reinstated) make an item appear on a table + +## New Features +- `advtools`: ``advtools.fastcombat`` overlay (enabled by default) allows you to skip combat animations and the announcement "More" button by mashing the movement keys +- `gui/journal`: now working in adventure mode -- journal is per-adventurer, so if you unretire an adventurer, you get the same journal +- `emigration`: ``nobles`` command for sending freeloader barons back to the sites that they rule over +- `toggle-kbd-cursor`: support adventure mode (Alt-k keybinding now toggles Look mode) + +## Fixes +- `hfs-pit`: use correct wall types when making pits with walls +- `gui/liquids`: don't add liquids to wall tiles +- `gui/liquids`: using the remove tool with magma selected will no longer create unexpected unpathable tiles +- `idle-crafting`: do not assign crafting jobs to nobles holding meetings (avoids dangling jobs) +- `rejuvenate`: update unit portrait and sprite when aging up babies and children +- `rejuvenate`: recalculate labor assignments for unit when aging up babies and children (so they can start accepting jobs) + +## Misc Improvements +- `hide-tutorials`: handle tutorial popups for adventure mode +- `hide-tutorials`: new ``reset`` command that will re-enable popups in the current game (in case you hid them all and now want them back) +- `gui/notify`: moody dwarf notification turns red when they can't reach workshop or items +- `gui/notify`: save reminder now appears in adventure mode +- `gui/notify`: save reminder changes color to yellow at 30 minutes and to orange at 60 minutes +- `gui/confirm`: in the delete manager order confirmation dialog, show a description of which order you have selected to delete +- `gui/create-item`: now accepts a ``pos`` argument of where to spawn items +- `modtools/create-item`: exported ``hackWish`` function now supports ``opts.pos`` for determining spawn location +- `hfs-pit`: improve placement of stairs w/r/t eerie pits and ramp tops +- `position`: add adventurer tile position +- `position`: add global site position +- `position`: when a tile is selected, display relevant map block and intra-block offset +- `gui/sitemap`: shift click to start following the selected unit or artifact +- `prioritize`: when prioritizing jobs of a specified type, also output how many of those jobs were already prioritized before you ran the command +- `prioritize`: don't include already-prioritized jobs in the output of ``prioritize -j`` +- `gui/design`: only display vanilla dimensions tooltip if the DFHack dimensions tooltip is disabled +- `devel/query`: support adventure mode +- `devel/tree-info`: support adventure mode +- `hfs-pit`: support adventure mode +- `colonies`: support adventure mode +- `position`: report position of the adventure mode look cursor, if active + +# 51.04-r1.1 + +## Fixes +- `advtools`: fix dfhack-added conversation options not appearing in the ask whereabouts conversation tree +- `gui/rename`: fix error when changing the language of a unit's name + +## Misc Improvements +- `assign-preferences`: new ``--show`` option to display the preferences of the selected unit +- `pref-adjust`: new ``show`` command to display the preferences of the selected unit + +## Removed +- `gui/control-panel`: removed ``craft-age-wear`` tweak for Windows users; the tweak doesn't currently load on Windows + +# 51.02-r1 + +## Fixes +- `deathcause`: fix error when retrieving the name of a historical figure + +# 50.15-r2 + +## New Tools +- `fix/stuck-squad`: allow squads and messengers returning from missions to rescue squads that have gotten stuck on the world map +- `gui/rename`: (reinstated) give new in-game language-based names to anything that can be named (units, governments, fortresses, the world, etc.) + +## New Features +- `gui/settings-manager`: new overlay on the Labor -> Standing Orders tab for configuring the number of barrels to reserve for job use (so you can brew alcohol and not have all your barrels claimed by stockpiles for container storage) +- `gui/settings-manager`: standing orders save/load now includes the reserved barrels setting +- `gui/rename`: add overlay to worldgen screen allowing you to rename the world before the new world is saved +- `gui/rename`: add overlay to the "Prepare carefully" embark screen that transparently fixes a DF bug where you can't give units nicknames or custom professions +- `gui/notify`: new notification type: save reminder; appears if you have gone more than 15 minutes without saving; click to autosave + +## Fixes +- `fix/dry-buckets`: don't empty buckets for wells that are actively in use +- `gui/unit-info-viewer`: skill progress bars now show correct XP thresholds for skills past Legendary+5 +- `caravan`: no longer incorrectly identify wood-based plant items and plant-based soaps as being ethically unsuitable for trading with the elves +- `gui/design`: don't require an extra right click on the first cancel of building area designations +- `gui/gm-unit`: refresh unit sprite when profession is changed + +## Misc Improvements +- `immortal-cravings`: goblins and other naturally non-eating/non-drinking races will now also satisfy their needs for eating and drinking +- `caravan`: add filter for written works in display furniture assignment dialog +- `fix/wildlife`: don't vaporize stuck wildlife that is onscreen -- kill them instead (as if they died from old age) +- `gui/sitemap`: show primary group affiliation for visitors and invaders (e.g. civilization name or performance troupe) + +# 50.14-r2 + +## New Tools +- `fix/wildlife`: prevent wildlife from getting stuck when trying to exit the map. This fix needs to be enabled manually in `gui/control-panel` on the Bug Fixes tab since not all players want this bug to be fixed (you can intentionally stall wildlife incursions by trapping wildlife in an enclosed area so they are not caged but still cannot escape). +- `immortal-cravings`: allow immortals to satisfy their cravings for food and drink +- `justice`: pardon a criminal's prison sentence + +## New Features +- `force`: add support for a ``Wildlife`` event to allow additional wildlife to enter the map + +## Fixes +- `gui/quickfort`: only print a help blueprint's text once even if the repeat setting is enabled +- `makeown`: quell any active enemy or conflict relationships with converted creatures +- `makeown`: halt any hostile jobs the unit may be engaged in, like kidnapping +- `fix/loyaltycascade`: allow the fix to work on non-dwarven citizens +- `control-panel`: fix error when setting numeric preferences from the commandline +- `gui/quickfort`: fix build mode evaluation rules to allow placement of furniture and constructions on tiles with stair shapes or without orthagonal floors +- `emigration`: save-and-reload no longer resets the emigration cycle timeout +- `geld`, `ungeld`: save-and-reload no longer loses changes done by `geld` and `ungeld` for units who are historical figures +- `rejuvenate`: fix error when specifying ``--age`` parameter +- `gui/notify`: don't classify (peacefully) visiting night creatures as hostile +- `exportlegends`: ensure historical figure race filter is usable after re-entering legends mode with a different loaded world + +## Misc Improvements +- `idle-crafting`: also support making shell crafts for workshops with linked input stockpiles +- `gui/gm-editor`: automatically resolve and display names for ``language_name`` fields +- `fix/stuck-worship`: reduced console output by default. Added ``--verbose`` and ``--quiet`` options. +- `gui/design`: add dimensions tooltip to vanilla zone painting interface +- `necronomicon`: new ``--world`` option to list all secret-containing items in the entire world +- `gui/design`: new ``gui/design.rightclick`` overlay that allows you to cancel out of partially drawn box and minecart designations without canceling completely out of drawing mode + +## Removed +- `modtools/force`: merged into `force` + +# 50.13-r5 + +## New Tools +- `embark-anyone`: allows you to embark as any civilization, including dead and non-dwarven civs +- `idle-crafting`: allow dwarves to independently satisfy their need to craft objects +- `gui/family-affairs`: (reinstated) inspect or meddle with pregnancies, marriages, or lover relationships +- `notes`: attach notes to locations on a fort map + +## New Features +- `caravan`: DFHack dialogs for trade screens (both ``Bring goods to depot`` and the ``Trade`` barter screen) can now filter by item origins (foreign vs. fort-made) and can filter bins by whether they have a mix of ethically acceptable and unacceptable items in them +- `caravan`: If you have managed to select an item that is ethically unacceptable to the merchant, an "Ethics warning" badge will now appear next to the "Trade" button. Clicking on the badge will show you which items that you have selected are problematic. The dialog has a button that you can click to deselect the problematic items in the trade list. +- `confirm`: If you have ethically unacceptable items selected for trade, the "Are you sure you want to trade" confirmation will warn you about them +- `quickfort`: ``#zone`` blueprints now integrated with `preserve-rooms` so you can create a zone and automatically assign it to a noble or administrative role +- `exportlegends`: option to filter by race on historical figures page + +## Fixes +- `timestream`: ensure child growth events (that is, a child's transition to adulthood) are not skipped; existing "overage" children will be automatically fixed within a year +- `empty-bin`: ``--liquids`` option now correctly empties containers filled with LIQUID_MISC (like lye) +- `gui/design`: don't overcount "affected tiles" for Line & Freeform drawing tools +- `deep-embark`: fix error when embarking where there is no land to stand on (e.g. when embarking in the ocean with `gui/embark-anywhere`) +- `deep-embark`: fix failure to transport units and items when embarking where there is no room to spawn the starting wagon +- `gui/create-item`, `modtools/create-item`: items of type "VERMIN", "PET", "REMANS", "FISH", "RAW FISH", and "EGG" no longer spawn creature item "nothing" and will now stack correctly +- `rejuvenate`: don't set a lifespan limit for creatures that are immortal (e.g. elves, goblins) +- `rejuvenate`: properly disconnect babies from mothers when aging babies up to adults + +## Misc Improvements +- `gui/sitemap`: show whether a unit is friendly, hostile, or wild +- `gui/sitemap`: show whether a unit is caged +- `gui/control-panel`: include option for turning off dumping of old clothes for `tailor`, for players who have magma pit dumps and want to save old clothes from being dumped into the magma +- `position`: report current historical era (e.g., "Age of Myth"), site/adventurer world coords, and mouse map tile coords +- `position`: option to copy keyboard cursor position to the clipboard +- `assign-minecarts`: reassign vehicles to routes where the vehicle has been destroyed (or has otherwise gone missing) +- `fix/dry-buckets`: prompt DF to recheck requests for aid (e.g. "bring water" jobs) when a bucket is unclogged and becomes available for use +- `exterminate`: show descriptive names for the listed races in addition to their IDs +- `exterminate`: show actual names for unique creatures such as forgotten beasts and titans +- `fix/ownership`: now also checks and fixes room ownership links + +## Documentation +- `gui/embark-anywhere`: add information about how the game determines world tile pathability and instructions for bridging two landmasses + +# 50.13-r4 + +## New Features +- `gui/journal`: new automatic table of contents. add lines that start with "# ", like "# Entry for 502-04-02", to add hyperlinked headers to the table of contents + +## Fixes +- `full-heal`: fix ``-r --all_citizens`` option combination not resurrecting citizens +- `open-legends`: don't intercept text bound for vanilla legends mode search widgets +- `gui/unit-info-viewer`: correctly display skill levels when rust is involved +- `timestream`: fix dwarves spending too long eating and drinking +- `timestream`: fix jobs not being created at a sufficient rate, leading to dwarves standing around doing nothing +- `locate-ore`: fix sometimes selecting an incorrect tile when there are multiple mineral veins in a single map block +- `gui/settings-manager`: fix position of "settings restored" message on embark when the player has no saved embark profiles +- `build-now`: fix error when building buildings that (in previous DF versions) required the architecture labor +- `prioritize`: fix incorrect restoring of saved settings on Windows +- `list-waves`: no longer gets confused by units that leave the map and then return (e.g. squads who go out on raids) +- `fix/dead-units`: fix error when removing dead units from burrows and the unit with the greatest ID was dead +- `makeown`: ensure names given to adopted units (or units created with `gui/sandbox`) are respected later in legends mode +- `gui/autodump`: prevent dumping into walls or invalid map areas +- `gui/autodump`: properly turn items into projectiles when they are teleported into mid-air + +## Misc Improvements +- `build-now`: if `suspendmanager` is running, run an unsuspend cycle immediately before scanning for buildings to build +- `list-waves`: now outputs the names of the dwarves in each migration wave +- `list-waves`: can now display information about specific migration waves (e.g. ``list-waves 0`` to identify your starting 7 dwarves) +- `allneeds`: display distribution of needs by how severely they are affecting the dwarf + +# 50.13-r3 + +## New Tools +- `advtools`: collection of useful commands and overlays for adventure mode +- `advtools`: added an overlay that automatically fixes corrupt throwing/shooting state, preventing save/load crashes +- `advtools`: advtools party - promotes one of your companions to become a controllable adventurer +- `advtools`: advtools pets - fixes pets you gift in adventure mode. +- `pop-control`: (reinstated) limit the maximum size of migrant waves +- `bodyswap`: (reinstated) take control of another unit in adventure mode +- `gui/sitemap`: list and zoom to people, locations, and artifacts +- `devel/tree-info`: print a technical visualization of tree data +- `gui/tiletypes`: interface for modifying map tiles and tile properties +- `fix/population-cap`: fixes the situation where you continue to get migrant waves even when you are above your configured population cap +- `fix/occupancy`: fixes issues where you can't build somewhere because the game tells you an item/unit/building is in the way but there's nothing there +- `fix/sleepers`: (reinstated) fixes sleeping units belonging to a camp that never wake up. +- `timestream`: (reinstated) keep the game running quickly even when there are large numbers of units on the map +- `gui/journal`: fort journal with a multi-line text editor +- `devel/luacov`: (reinstated) add Lua script coverage reporting for use in testing and performance analysis + +## New Features +- `buildingplan`: dimension tooltip is now displayed for constructions and buildings that are designated over an area, like bridges and farm plots +- `gui/notify`: new notification type: injured citizens; click to zoom to injured units; also displays a warning if your hospital is not functional (or if you have no hospital) +- `gui/notify`: new notification type: drowning and suffocation progress bars for adventure mode +- `prioritize`: new info panel on under-construction buildings showing if the construction job has been taken and by whom. click to zoom to builder; toggle high priority status for job if it's not yet taken and you need it to be built ASAP +- `gui/unit-info-viewer`: new overlay for displaying progress bars for skills on the unit info sheet +- `gui/pathable`: new "Depot" mode that shows whether wagons can path to your trade depot +- `advtools`: automatically add a conversation option to "ask whereabouts of" for all your relationships (before, you could only ask whereabouts of people involved in rumors) +- `gui/design`: all-new visually-driven UI for much improved usability + +## Fixes +- `assign-profile`: fix handling of ``unit`` option for setting target unit id +- `gui/gm-unit`: correctly display skill levels above Legendary+5 +- `gui/gm-unit`: fix errors when editing/randomizing colors and body appearance +- `quickfort`: fix incorrect handling of stockpiles that are split into multiple separate areas but are given the same label (indicating that they should be part of the same stockpile) +- `makeown`: set animals to tame and domesticated +- `gui/sandbox`: spawned citizens can now be useful military squad members +- `gui/sandbox`: spawned undead now have a purple shade (only after save and reload, though) +- `caravan`: fix errors in trade dialog if all fort items are traded away while the trade dialog is showing fort items and the `confirm` trade confirmation is shown +- `control-panel`: restore non-default values of per-save enabled/disabled settings for repeat-based commands +- `confirm`: fix confirmation prompt behavior when overwriting a hotkey zoom location +- `quickfort`: allow farm plots to be built on muddy stone (as per vanilla behavior) +- `suspend`: remove broken ``--onlyblocking`` option; restore functionality to ``suspend all`` +- `gui/create-item`: allow creation of adamantine thread, wool, and yarn +- `gui/notify`: the notification panel no longer responds to the Enter key so Enter key is passed through to the vanilla UI +- `clear-smoke`: properly tag smoke flows for garbage collection to avoid memory leak +- `warn-stranded`: don't warn for babies carried by mothers who happen to be gathering fruit from trees +- `prioritize`: also boost priority of already-claimed jobs when boosting priority of a job type so those jobs are not interrupted +- `ban-cooking`: ban all seed producing items from being cooked when 'seeds' is chosen instead of just brewable seed producing items + +## Misc Improvements +- `item`: option for ignoring uncollected spider webs when you search for "silk" +- `gui/launcher`: "space space to toggle pause" behavior is skipped if the game was paused when `gui/launcher` came up to prevent accidental unpausing +- `gui/unit-info-viewer`: add precise unit size in cc (cubic centimeters) for comparison against the wiki values. you can set your preferred number format for large numbers like this in the preferences of `control-panel` or `gui/control-panel` +- `gui/unit-info-viewer`: now displays a unit's weight relative to a similarly-sized well-known creature (dwarves, elephants, or cats) +- `gui/unit-info-viewer`: shows a unit's size compared to the average for the unit's race +- `caravan`: optional overlay to hide vanilla "bring trade goods to depot" button (if you prefer to always use the DFHack version and don't want to accidentally click on the vanilla button). enable ``caravan.movegoods_hider`` in `gui/control-panel` UI Overlays tab to use. +- `caravan`: bring goods to depot screen now shows (approximate) distance from item to depot +- `gui/design`: circles are more circular (now matches more pleasing shape generated by ``digcircle``) +- `gui/quickfort`: you can now delete your blueprints from the blueprint load dialog +- `caravan`: remember filter settings for pedestal item assignment dialog +- `quickfort`: new ``delete`` command for deleting player-owned blueprints (library and mod-added blueprints cannot be deleted) +- `quickfort`: support enabling `logistics` features for autoforbid and autoclaim on stockpiles +- `gui/quickfort`: allow farm plots, dirt roads, and paved roads to be designated around partial obstructions without calling it an error, matching vanilla behavior +- `gui/launcher`: refresh default tag filter when mortal mode is toggled in `gui/control-panel` so changes to which tools autocomplete take effect immediately +- `gui/civ-alert`: you can now register multiple burrows as civilian alert safe spaces +- `exterminate`: add ``all`` target for convenient scorched earth tactics +- `empty-bin`: select a stockpile, tile, or building to empty all containers in the stockpile, tile, or building +- `exterminate`: add ``--limit`` option to limit number of exterminated creatures +- `exterminate`: add ``knockout`` and ``traumatize`` method for non-lethal incapacitation +- `caravan`: add shortcut to the trade request screen for selecting item types by value (e.g. so you can quickly select expensive gems or cheap leather) +- `gui/notify`: notification panel extended to apply to adventure mode +- `gui/control-panel`: highlight preferences that have been changed from the defaults +- `gui/quickfort`: buildings can now be constructed in a "high priority" state, giving them first dibs on `buildingplan` materials and setting their construction jobs to the highest priority +- `prioritize`: add ``ButcherAnimal`` to the default prioritization list (``SlaughterAnimal`` was already there, but ``ButcherAnimal`` -- which is different -- was missing) +- `prioritize`: list both unclaimed and total counts for current jobs when the --jobs option is specified +- `prioritize`: boost performance of script by not tracking number of times a job type was prioritized +- `gui/unit-syndromes`: make werecreature syndromes easier to search for + +## Removed +- `max-wave`: merged into `pop-control` +- `devel/find-offsets`, `devel/find-twbt`, `devel/prepare-save`: remove development scripts that are no longer useful +- `fix/item-occupancy`, `fix/tile-occupancy`: merged into `fix/occupancy` +- `adv-fix-sleepers`: renamed to `fix/sleepers` +- `adv-rumors`: merged into `advtools` + +# 50.13-r2 + +## New Tools +- Updated for adventure mode: `gui/sandbox`, `gui/create-item`, `gui/reveal` +- `ghostly`: (reinstated) allow your adventurer to phase through walls +- `markdown`: (reinstated) export description of selected unit or item to a text file +- `adaptation`: (reinstated) inspect or set unit cave adaptation levels +- `fix/engravings`: fix corrupt engraving tiles +- `unretire-anyone`: (reinstated) choose anybody in the world as an adventurer +- `reveal-adv-map`: (reinstated) reveal (or hide) the adventure map +- `resurrect-adv`: (reinstated) allow your adventurer to recover from death +- `flashstep`: (reinstated) teleport your adventurer to the mouse cursor + +## New Features +- `instruments`: new subcommand ``instruments order`` for creating instrument work orders + +## Fixes +- `modtools/create-item`: now functions properly when the ``reaction-gloves`` tweak is active +- `quickfort`: don't designate multiple tiles of the same tree for chopping when applying a tree chopping blueprint to a multi-tile tree +- `gui/quantum`: fix processing when creating a quantum dump instead of a quantum stockpile +- `caravan`: don't include undiscovered divine artifacts in the goods list +- `quickfort`: fix detection of valid tiles for wells +- `combine`: respect container volume limits + +## Misc Improvements +- `gui/autobutcher`: add shortcuts for butchering/unbutchering all animals +- `combine`: reduce combined drink sizes to 25 +- `gui/launcher`: add button for copying output to the system clipboard +- `deathcause`: automatically find and choose a corpse when a pile of mixed items is selected +- `gui/quantum`: add option for whether a minecart automatically gets ordered and/or attached +- `gui/quantum`: when attaching a minecart, show which minecart was attached +- `gui/quantum`: allow multiple feeder stockpiles to be linked to the minecart route +- `prioritize`: add PutItemOnDisplay jobs to the default prioritization list -- when these kinds of jobs are requested by the player, they generally want them done ASAP + +# 50.13-r1.1 + +## Fixes +- `gui/quantum`: accept all item types in the output stockpile as intended +- `deathcause`: fix error on run + +# 50.13-r1 + +## New Tools +- `gui/unit-info-viewer`: (reinstated) give detailed information on a unit, such as egg laying behavior, body size, birth date, age, and information about their afterlife behavior (if a ghost) +- `gui/quantum`: (reinstated) point and click interface for creating quantum stockpiles or quantum dumps + +## Fixes +- `open-legends`: don't interfere with the dragging of vanilla list scrollbars +- `gui/create-item`: properly restrict bags to bag materials by default +- `gui/create-item`: allow gloves and shoes to be made out of textiles by default +- `exterminate`: don't classify dangerous non-invader units as friendly (e.g. snatchers) + +## Misc Improvements +- `open-legends`: allow player to cancel the "DF will now exit" dialog and continue browsing +- `gui/gm-unit`: changes to unit appearance will now immediately be reflected in the unit portrait + +# 50.12-r3 + +## New Tools +- `gui/aquifer`: interactive aquifer visualization and editing +- `open-legends`: (reinstated) open legends mode directly from a loaded fort + +## New Features +- `quickfort`: add options for setting warm/damp dig markers when applying blueprints +- `gui/quickfort`: add options for setting warm/damp dig markers when applying blueprints +- `gui/reveal`: new "aquifer only" mode to only see hidden aquifers but not reveal any tiles +- `gui/notify`: optional notification for general wildlife (not on by default) + +## Fixes +- `fix/loyaltycascade`: fix edge case where loyalties of renegade units were not being fixed +- `quickfort`: reject tiles for building that contain magma or deep water +- `armoks-blessing`: fix error when making "Normal" attributes legendary +- `emigration`: remove units from burrows when they emigrate +- `agitation-rebalance`: fix calculated percent chance of cavern invasion +- `gui/launcher`: don't pop up a result dialog if a command run from minimal mode has no output + +## Misc Improvements +- `gui/reveal`: show aquifers even when not in mining mode +- `gui/control-panel`: add alternate "nodump" version for `cleanowned` that does not cause citizens to toss their old clothes in the dump. this is useful for players who would rather sell old clothes than incinerate them +- `agitation-rebalance`: when more than the maximum allowed cavern invaders are trying to enter the map, prefer keeping the animal people invaders instead of their war animals + +## Removed +- `drain-aquifer`: replaced by ``aquifer drain --all``; an alias now exists so ``drain-aquifer`` will automatically run the new command + +# 50.12-r2.1 + +## Fixes +- `fix/noexert-exhaustion`: fix typo in control panel registry entry which prevented the fix from being run when enabled +- `gui/suspendmanager`: fix script startup errors +- `control-panel`: properly auto-enable newly added bugfixes + +## Misc Improvements +- `gui/unit-syndromes`: make syndromes searchable by their display names (e.g. "necromancer") + +# 50.12-r2 + +## New Tools +- `agitation-rebalance`: alter mechanics of irritation-related attacks so they are less constant and are more responsive to recent player behavior +- `fix/ownership`: fix instances of multiple citizens claiming the same items, resulting in "Store owned item" job loops +- `fix/stuck-worship`: fix prayer so units don't get stuck in uninterruptible "Worship!" states +- `instruments`: provides information on how to craft the instruments used by the player civilization +- `modtools/item-trigger`: (reinstated) modder's resource for triggering scripted content when specific items are used +- `modtools/if-entity`: (reinstated) modder's resource for triggering scripted content depending on the race of the loaded fort +- `devel/block-borders`: (reinstated) highlights boundaries of map blocks or embark tile blocks +- `fix/noexert-exhaustion`: fix "Tired" NOEXERT units. Enabling via `gui/control-panel` prevents NOEXERT units from getting stuck in a "Tired" state + +## New Features +- `gui/settings-manager`: add import, export, and autoload for work details +- `exterminate`: new "disintegrate" kill method that additionally destroys carried items +- `quickfort`: allow setting of workshop profile properties (e.g. labor, skill restrictions) from build blueprints + +## Fixes +- `gui/launcher`: fix history scanning (Up/Down arrow keys) being slow to respond when in minimal mode +- `control-panel`: fix filtering not filtering when running the ``list`` command +- `gui/notify`: don't zoom to forbidden depots for merchants ready to trade notification +- `catsplosion`: only cause pregnancies in adults + +## Misc Improvements +- `gui/launcher`: add interface for browsing and filtering commands by tags +- `gui/launcher`: add support for history search (Alt-s hotkey) when in minimal mode +- `gui/launcher`: add support for the ``clear`` command and clearing the scrollback buffer +- `control-panel`: enable tweaks quietly on fort load so we don't spam the console +- `devel/tile-browser`: simplify interface now that SDL automatically normalizes texture scale +- `exterminate`: make race name matching case and space insensitive +- `gui/gm-editor`: support opening engraved art for inspection +- `gui/notify`: Shift click or Shift Enter on a zoomable notification to zoom to previous target +- `allneeds`: select a dwarf in the UI to see a summary of needs for just that dwarf +- `allneeds`: provide options for sorting the cumulative needs by different criteria +- `prioritize`: print out custom reaction and hauling jobs in the same format that is used for ``prioritize`` command arguments so the player can just copy and paste + +# 50.12-r1 + +## Fixes +- `gui/notify`: persist notification settings when toggled in the UI + +## Misc Improvements +- `gui/launcher`: developer mode hotkey restored to Ctrl-D + +# 50.11-r7 + +## New Tools +- `undump-buildings`: (reinstated) remove dump designation from in-use building materials +- `gui/petitions`: (reinstated) show outstanding (or all historical) petition agreements for guildhalls and temples +- `gui/notify`: display important notifications that vanilla doesn't support yet and provide quick zoom links to notification targets. +- `list-waves`: (reinstated) show migration wave information +- `make-legendary`: (reinstated) make a dwarf legendary in specified skills +- `combat-harden`: (reinstated) set a dwarf's resistance to being affected by visible corpses +- `add-thought`: (reinstated) add custom thoughts to a dwarf +- `devel/input-monitor`: interactive UI for debugging input issues + +## Fixes +- `gui/design`: clicking the center point when there is a design mark behind it will no longer simultaneously enter both mark dragging and center dragging modes. Now you can click once to move the shape, and click twice to move only the mark behind the center point. +- `fix/retrieve-units`: prevent pulling in duplicate units from offscreen +- `warn-stranded`: when there was at least one truly stuck unit and miners were actively mining, the miners were also confusingly shown in the stuck units list +- `source`: fix issue where removing sources would make some other sources inactive +- `caravan`: display book and scroll titles in the goods and trade dialogs instead of generic scroll descriptions +- `item`: avoid error when scanning items that have no quality rating (like bars and other construction materials) +- `gui/blueprint`: changed hotkey for setting blueprint origin tile so it doesn't conflict with default map movement keys +- `gui/control-panel`: fix error when toggling autostart settings + +## Misc Improvements +- `exportlegends`: make progress increase smoothly over the entire export and increase precision of progress percentage +- `gui/autobutcher`: ask for confirmation before zeroing out targets for all races +- `caravan`: move goods to trade depot dialog now allocates more space for the display of the value of very expensive items +- `extinguish`: allow selecting units/items/buildings in the UI to target them for extinguishing; keyboard cursor is only required for extinguishing map tiles that cannot be selected any other way +- `item`: change syntax so descriptions can be searched for without indicating the ``--description`` option. e.g. it's now ``item count royal`` instead of ``item count --description royal`` +- `item`: add ``--verbose`` option to print each item as it is matched +- `gui/mod-manager`: will automatically unmark the default mod profile from being the default if it fails to load (due to missing or incompatible mods) +- `gui/quickfort`: can now dynamically adjust the dig priority of tiles designated by dig blueprints +- `gui/quickfort`: can now opt to apply dig blueprints in marker mode + +## Removed +- `gui/manager-quantity`: the vanilla UI can now modify manager order quantities after creation +- `gui/create-tree`: replaced by `gui/sandbox` +- `warn-starving`: combined into `gui/notify` +- `warn-stealers`: combined into `gui/notify` + +# 50.11-r6 + +## Fixes +- `makeown`: fix error when adopting units that need a historical figure to be created +- `item`: fix missing item categories when using ``--by-type`` + +# 50.11-r5 + +## New Tools +- `control-panel`: new commandline interface for control panel functions +- `uniform-unstick`: (reinstated) force squad members to drop items that they picked up in the wrong order so they can get everything equipped properly +- `gui/reveal`: temporarily unhide terrain and then automatically hide it again when you're ready to unpause +- `gui/teleport`: mouse-driven interface for selecting and teleporting units +- `gui/biomes`: visualize and inspect biome regions on the map +- `gui/embark-anywhere`: bypass those pesky warnings and embark anywhere you want to +- `item`: perform bulk operations on groups of items. + +## New Features +- `uniform-unstick`: add overlay to the squad equipment screen to show a equipment conflict report and give you a one-click button to (attempt to) fix +- `gui/settings-manager`: save and load embark difficulty settings and standing orders; options for auto-load on new embark + +## Fixes +- `source`: water and magma sources and sinks now persist with fort across saves and loads +- `gui/design`: fix incorrect dimensions being shown when you're placing a stockpile, but a start coordinate hasn't been selected yet +- `warn-stranded`: don't warn for citizens who are only transiently stranded, like those on stepladders gathering plants or digging themselves out of a hole +- `ban-cooking`: fix banning creature alcohols resulting in error +- `confirm`: properly detect clicks on the remove zone button even when the unit selection screen is also open (e.g. the vanilla assign animal to pasture panel) +- `caravan`: ensure items are marked for trade when the move trade goods dialog is closed even when they were selected and then the list filters were changed such that the items were no longer actively shown +- `quickfort`: if a blueprint specifies an up/down stair, but the tile the blueprint is applied to cannot make an up stair (e.g. it has already been dug out), still designate a down stair if possible +- `suspendmanager`: correctly handle building collisions with smoothing designations when the building is on the edge of the map +- `empty-bin`: now correctly sends ammunition in carried quivers to the tile underneath the unit instead of teleporting them to an invalid (or possibly just far away) location + +## Misc Improvements +- `warn-stranded`: center the screen on the unit when you select one in the list +- `gui/control-panel`: reduce frequency for `warn-stranded` check to once every 2 days +- `gui/control-panel`: tools are now organized by type: automation, bugfix, and gameplay +- `confirm`: updated confirmation dialogs to use clickable widgets and draggable windows +- `confirm`: added confirmation prompt for right clicking out of the trade agreement screen (so your trade agreement selections aren't lost) +- `confirm`: added confirmation prompts for irreversible actions on the trade screen +- `confirm`: added confirmation prompt for deleting a uniform +- `confirm`: added confirmation prompt for convicting a criminal +- `confirm`: added confirmation prompt for re-running the embark site finder +- `confirm`: added confirmation prompt for reassigning or clearing zoom hotkeys +- `confirm`: added confirmation prompt for exiting the uniform customization page without saving +- `gui/autobutcher`: interface redesigned to better support mouse control +- `gui/launcher`: now persists the most recent 32KB of command output even if you close it and bring it back up +- `gui/quickcmd`: clickable buttons for command add/remove/edit operations +- `uniform-unstick`: warn if a unit belongs to a squad from a different site (can happen with migrants from previous forts) +- `gui/mass-remove`: can now differentiate planned constructions, stockpiles, and regular buildings +- `gui/mass-remove`: can now remove zones +- `gui/mass-remove`: can now cancel removal for buildings and constructions +- `fix/stuck-instruments`: now handles instruments that are left in the "in job" state but that don't have any actual jobs associated with them +- `gui/launcher`: make autocomplete case insensitive + +# 50.11-r4 + +## New Tools +- `build-now`: (reinstated) instantly complete unsuspended buildings that are ready to be built + +## Fixes +- `combine`: prevent stack sizes from growing beyond quantities that you would normally see in vanilla gameplay +- `gui/design`: Center dragging shapes now track the mouse correctly + +## Misc Improvements +- `caravan`: enable searching within containers in trade screen when in "trade bin with contents" mode + +# 50.11-r3 + +## New Tools +- `sync-windmills`: synchronize or randomize movement of active windmills +- `trackstop`: (reimplemented) integrated overlay for changing track stop and roller settings after construction + +## New Features +- `gui/design`: show selected dimensions next to the mouse cursor when designating with vanilla tools, for example when painting a burrow or designating digging +- `quickfort`: new ``burrow`` blueprint mode for designating or manipulating burrows +- `unforbid`: now ignores worn and tattered items by default (X/XX), use -X to bypass +- `fix/dead-units`: gained ability to scrub dead units from burrow membership lists + +## Fixes +- `gui/unit-syndromes`: show the syndrome names properly in the UI +- `emigration`: fix clearing of work details assigned to units that leave the fort + +## Misc Improvements +- `warn-stranded`: don't warn for units that are temporarily on unwalkable tiles (e.g. as they pass under a waterfall) + +## Removed +- `gui/control-panel`: removed always-on system services from the ``System`` tab: `buildingplan`, `confirm`, `logistics`, and `overlay`. The base services should not be turned off by the player. Individual confirmation prompts can be managed via `gui/confirm`, and overlays (including those for `buildingplan` and `logistics`) are managed on the control panel ``Overlays`` tab. +- `gui/control-panel`: removed `autolabor` from the ``Fort`` and ``Autostart`` tabs. The tool does not function correctly with the new labor types, and is causing confusion. You can still enable `autolabor` from the commandline with ``enable autolabor`` if you understand and accept its limitations. + +# 50.11-r2 + +## New Tools +- `add-recipe`: (reinstated) add reactions to your civ (e.g. for high boots if your civ didn't start with the ability to make high boots) +- `fix/corrupt-jobs`: prevents crashes by automatically removing corrupted jobs +- `burial`: (reinstated) create tomb zones for unzoned coffins + +## New Features +- `burial`: new options to configure automatic burial and limit scope to the current z-level +- `drain-aquifer`: gained ability to drain just above or below a certain z-level +- `drain-aquifer`: new option to drain all layers except for the first N aquifer layers, in case you want some aquifer layers but not too many +- `gui/control-panel`: ``drain-aquifer --top 2`` added as an autostart option + +## New Scripts +- `warn-stranded`: new repeatable maintenance script to check for stranded units, similar to `warn-starving` + +## Fixes +- `suspendmanager`: fix errors when constructing near the map edge +- `gui/sandbox`: fix scrollbar moving double distance on click +- `hide-tutorials`: fix the embark tutorial prompt sometimes not being skipped +- `full-heal`: fix removal of corpse after resurrection +- `toggle-kbd-cursor`: clear the cursor position when disabling, preventing the game from sometimes jumping the viewport around when cursor keys are hit + +## Misc Improvements +- `prioritize`: refuse to automatically prioritize dig and smooth/carve job types since it can break the DF job scheduler; instead, print a suggestion that the player use specialized units and vanilla designation priorities +- `gui/overlay`: filter overlays by current context so there are fewer on the screen at once and you can more easily click on the one you want to reposition +- `quickfort`: now allows constructions to be built on top of constructed floors and ramps, just like vanilla. however, to allow blueprints to be safely reapplied to the same area, for example to fill in buildings whose constructions were canceled due to lost items, floors will not be rebuilt on top of floors and ramps will not be rebuilt on top of ramps +- `gui/gm-editor`: for fields with primitive types, change from click to edit to click to select, double-click to edit. this should help prevent accidental modifications to the data and make hotkeys easier to use (since you have to click on a data item to use a hotkey on it) + +# 50.11-r1 + +## New Tools +- `startdwarf`: (reinstated) set number of starting dwarves + +## New Features +- `startdwarf`: overlay scrollbar so you can scroll through your starting dwarves if they don't all fit on the screen +- A new searchable, sortable, filterable dialog for selecting items for display on pedestals and display cases + +## Fixes +- `suspendmanager`: fixed a bug where floor grates, bars, bridges etc. wouldn't be recognised as walkable, leading to unnecessary suspensions in certain cases. + +## Misc Improvements +- `devel/inspect-screen`: display total grid size for UI and map layers +- `suspendmanager`: now suspends constructions that would cave-in immediately on completion + +# 50.10-r1 + +## Fixes +- 'fix/general-strike: fix issue where too many seeds were getting planted in farm plots + +# 50.09-r4 + +## Misc Improvements +- `autofish`: changed ``--raw`` argument format to allow explicit setting to on or off +- `caravan`: move goods to depot screen can now see/search/trade items inside of barrels and pots +- `gui/launcher`: show tagged tools in the autocomplete list when a tag name is typed + +# 50.09-r3 + +## New Tools +- `devel/scan-vtables`: scan and dump likely vtable addresses (for memory research) +- `hide-interface`: hide the vanilla UI elements for clean screenshots or laid-back fortress observing +- `hide-tutorials`: hide the DF tutorial popups; enable in the System tab of `gui/control-panel` +- `set-orientation`: tinker with romantic inclinations (reinstated from back catalog of tools) + +## New Features +- `exportlegends`: new overlay that integrates with the vanilla "Export XML" button. Now you can generate both the vanilla export and the extended data export with a single click! + +## Fixes +- `suspendmanager`: Fix the overlay enabling/disabling `suspendmanager` unexpectedly +- `caravan`: correct price adjustment values in trade agreement details screen +- `caravan`: apply both import and export trade agreement price adjustments to items being both bought or sold to align with how vanilla DF calculates prices +- `caravan`: cancel any active TradeAtDepot jobs if all caravans are instructed to leave +- `emigration`: fix errors loading forts after dwarves assigned to work details or workshops have emigrated +- `emigration`: fix citizens sometimes "emigrating" to the fortress site +- `suspendmanager`: improve the detection on "T" and "+" shaped high walls +- `starvingdead`: ensure sieges end properly when undead siegers starve +- `fix/retrieve-units`: fix retrieved units sometimes becoming duplicated on the map +- `quickfort`: cancel old dig jobs that point to a tile when a new designation is applied to the tile +- `gui/launcher`, `gui/gm-editor`: recover gracefully when the saved frame position is now offscreen +- `gui/sandbox`: correctly load equipment materials in modded games that categorize non-wood plants as wood + +## Misc Improvements +- `devel/lsmem`: added support for filtering by memory addresses and filenames +- `gui/gm-editor`: hold down shift and right click to exit, regardless of how many substructures deep you are +- `quickfort`: linked stockpiles and workshops can now be specified by ID instead of only by name. this is mostly useful when dynamically generating blueprints and applying them via the `quickfort` API +- `gui/quickfort`: blueprint details screen can now be closed with Ctrl-D (the same hotkey used to open the details) +- `suspendmanager`: display a different color for jobs suspended by suspendmanager +- `caravan`: optionally display items within bins in bring goods to depot screen +- `gui/gm-editor`: display in the title bar whether the editor window is scanning for live updates +- `gui/design`: change "auto commit" hotkey from ``c`` to ``Alt-c`` to avoid conflict with the default keybinding for z-level down +- `gui/liquids`: support removing river sources by converting them into stone floors + +# 50.09-r2 + +## New Scripts +- `caravan`: new trade screen UI replacements for bringing goods to trade depot and trading +- `fix/empty-wheelbarrows`: new script to empty stuck rocks from all wheelbarrows on the map + +## Fixes +- `gui/autodump`: when "include items claimed by jobs" is on, actually cancel the job so the item can be teleported +- `gui/gm-unit`: fix commandline processing when a unit id is specified +- `suspendmanager`: take in account already built blocking buildings +- `suspendmanager`: don't consider tree branches as a suitable access path to a building + +## Misc Improvements +- `gui/unit-syndromes`: make lists searchable +- `suspendmanager`: display the suspension reason when viewing a suspended building +- `quickfort`: blueprint libraries are now moddable -- add a ``blueprints/`` directory to your mod and they'll show up in `quickfort` and `gui/quickfort`! + +# 50.09-r1 + +## Misc Improvements +- `caravan`: new overlay for selecting all/none on trade request screen +- `suspendmanager`: don't suspend constructions that are built over open space + +# 50.08-r4 + +## Fixes +- `gui/create-item`: allow blocks to be made out of wood when using the restrictive filters +- `emigration`: reassign home site for emigrating units so they don't just come right back to the fort +- `gui/sandbox`: allow creatures that have separate caste-based graphics to be spawned (like ewes/rams) +- `gui/liquids`: ensure tile temperature is set correctly when painting water or magma +- `workorder`: prevent ``autoMilkCreature`` from over-counting milkable animals, which was leading to cancellation spam for the MilkCreature job +- `gui/quickfort`: allow traffic designations to be applied over buildings +- `gui/quickfort`: protect against meta blueprints recursing infinitely if they include themselves + +## Misc Improvements +- `gui/control-panel`: add some popular startup configuration commands for `autobutcher` and `autofarm` +- `gui/control-panel`: add option for running `fix/blood-del` on new forts (enabled by default) +- `gui/sandbox`: when creating citizens, give them names appropriate for their races +- `gui/autodump`: add option to clear the ``trader`` flag from teleported items, allowing you to reclaim items dropped by merchants +- `quickfort`: significant rewrite for DF v50! now handles zones, locations, stockpile configuration, hauling routes, and more +- `suspendmanager`: now suspends construction jobs on top of floor designations, protecting the designations from being erased +- `prioritize`: add wild animal management tasks and lever pulling to the default list of prioritized job types +- `suspendmanager`: suspend blocking jobs when building high walls or filling corridors +- `workorder`: reduce existing orders for automatic shearing and milking jobs when animals are no longer available +- `gui/quickfort`: adapt "cursor lock" to mouse controls so it's easier to see the full preview for multi-level blueprints before you apply them +- `gui/quickfort`: only display post-blueprint messages once when repeating the blueprint up or down z-levels +- `combine`: reduce max different stacks in containers to 30 to prevent containers from getting overfull + +## Removed +- `gui/automelt`: replaced by an overlay panel that appears when you click on a stockpile + +# 50.08-r2 + +## New Scripts +- `diplomacy`: view or alter diplomatic relationships +- `exportlegends`: (reinstated) export extended legends information for external browsing +- `modtools/create-item`: (reinstated) commandline and API interface for creating items +- `light-aquifers-only`: (reinstated) convert heavy aquifers to light +- `necronomicon`: search fort for items containing the secrets of life and death +- `fix/stuck-instruments`: fix instruments that are attached to invalid jobs, making them unusable. turn on automatic fixing in `gui/control-panel` in the ``Maintenance`` tab. +- `gui/mod-manager`: automatically restore your list of active mods when generating new worlds +- `gui/autodump`: point and click item teleportation and destruction interface (available only if ``armok`` tools are shown) +- `gui/sandbox`: creation interface for units, trees, and items (available only if ``armok`` tools are shown) +- `assign-minecarts`: (reinstated) quickly assign minecarts to hauling routes + +## Fixes +- `quickfort`: properly allow dwarves to smooth, engrave, and carve beneath walkable tiles of buildings +- `deathcause`: fix incorrect weapon sometimes being reported +- `gui/create-item`: allow armor to be made out of leather when using the restrictive filters +- `gui/design`: Fix building and stairs designation +- `quickfort`: fixed detection of tiles where machines are allowed (e.g. water wheels *can* be built on stairs if there is a machine support nearby) +- `quickfort`: fixed rotation of blueprints with carved track tiles + +## Misc Improvements +- `gui/quickfort`: blueprints that designate items for dumping/forbidding/etc. no longer show an error highlight for tiles that have no items on them +- `gui/quickfort`: place (stockpile layout) mode is now supported. note that detailed stockpile configurations were part of query mode and are not yet supported +- `gui/quickfort`: you can now generate manager orders for items required to complete blueprints +- `gui/create-item`: ask for number of items to spawn by default +- `light-aquifers-only`: now available as a fort Autostart option in `gui/control-panel`. note that it will only appear if "armok" tools are configured to be shown on the Preferences tab. +- `gui/gm-editor`: when passing the ``--freeze`` option, further ensure that the game is frozen by halting all rendering (other than for DFHack tool windows) +- `gui/gm-editor`: Alt-A now enables auto-update mode, where you can watch values change live when the game is unpaused + +# 50.08-r1 + +## Fixes +- `deteriorate`: ensure remains of enemy dwarves are properly deteriorated +- `suspendmanager`: Fix over-aggressive suspension of jobs that could still possibly be done (e.g. jobs that are partially submerged in water) + +## Misc Improvements +- `combine`: Now supports ammo, parts, powders, and seeds, and combines into containers +- `deteriorate`: add option to exclude useable parts from deterioration +- `gui/gm-editor`: press ``g`` to move the map to the currently selected item/unit/building +- `gui/gm-editor`: press ``Ctrl-D`` to toggle read-only mode to protect from accidental changes; this state persists across sessions +- `gui/gm-editor`: new ``--freeze`` option for ensuring the game doesn't change while you're inspecting it +- `gui/launcher`: DFHack version now shown in the default help text +- `gui/prerelease-warning`: widgets are now clickable + +# 50.07-r1 + +## Fixes +-@ `caravan`: fix trade good list sometimes disappearing when you collapse a bin +-@ `gui/gm-editor`: no longer nudges last open window when opening a new one +- `warn-starving`: no longer warns for dead units +-@ `gui/control-panel`: the config UI for `automelt` is no longer offered when not in fortress mode + +## Misc Improvements +- `gui/gm-editor`: can now jump to material info objects from a mat_type reference with a mat_index using ``i`` +- `gui/gm-editor`: the key column now auto-fits to the widest key +- `prioritize`: revise and simplify the default list of prioritized jobs -- be sure to tell us if your forts are running noticeably better (or worse!) +-@ `gui/control-panel`: add `faststart` to the system services + +# 50.07-beta2 + +## New Scripts +- `fix/general-strike`: fix known causes of the general strike bug (contributed by Putnam) +- `gui/seedwatch`: GUI config and status panel interface for `seedwatch` +- `gui/civ-alert`: configure and trigger civilian alerts + +## Fixes +-@ `caravan`: item list length now correct when expanding and collapsing containers +-@ `prioritize`: fixed all watched job type names showing as ``nil`` after a game load +-@ `suspendmanager`: does not suspend non-blocking jobs such as floor bars or bridges anymore +-@ `suspendmanager`: fix occasional bad identification of buildingplan jobs +- `warn-starving`: no longer warns for enemy and neutral units + +## Misc Improvements +- `gui/control-panel`: Now detects overlays from scripts named with capital letters +- `gui/cp437-table`: now has larger key buttons and clickable backspace/submit/cancel buttons, making it fully usable on the Steam Deck and other systems that don't have an accessible keyboard +-@ `gui/design`: Now supports placing constructions using 'Building' mode. Inner and Outer tile constructions are configurable. Uses buildingplan filters set up with the regular buildingplan interface. +- `exterminate`: add support for ``vaporize`` kill method for when you don't want to leave a corpse +- `combine`: you can select a target stockpile in the UI instead of having to use the keyboard cursor +- `combine`: added ``--quiet`` option for no output when there are no changes +- `stripcaged`: added ``--skip-forbidden`` option for greater control over which items are marked for dumping +- `stripcaged`: items that are marked for dumping are now automatically unforbidden (unless ``--skip-forbidden`` is set) +-@ `gui/control-panel`: added ``combine all`` maintenance option for automatic combining of partial stacks in stockpiles +-@ `gui/control-panel`: added ``general-strike`` maintenance option for automatic fixing of (at least one cause of) the general strike bug +- `gui/cp437-table`: dialog is now fully controllable with the mouse, including highlighting which key you are hovering over and adding a clickable backspace button + +## Removed +- `autounsuspend`: replaced by `suspendmanager` +-@ `gui/dig`: renamed to `gui/design` + +# 50.07-beta1 + +## New Scripts +- `suspendmanager`: automatic job suspension management (replaces `autounsuspend`) +- `gui/suspendmanager`: graphical configuration interface for `suspendmanager` +- `suspend`: suspends building construction jobs + +## Fixes +-@ `quicksave`: now reliably triggers an autosave, even if one has been performed recently +- `gui/launcher`: tab characters in command output now appear as a space instead of a code page 437 "blob" + +## Misc Improvements +- `quickfort`: now reads player-created blueprints from ``dfhack-config/blueprints/`` instead of the old ``blueprints/`` directory. Be sure to move over your personal blueprints to the new directory! +- `gui/gm-editor`: can now open the selected stockpile if run without parameters + +# 50.07-alpha3 + +## Fixes +-@ `gui/create-item`: fix generic corpsepiece spawning + +## Misc Improvements +- `gui/create-item`: added ability to spawn 'whole' corpsepieces (every layer of a part) +-@ `gui/dig`: Allow placing an extra point (curve) while still placing the second main point +-@ `gui/dig`: Allow placing n-point shapes, shape rotation/mirroring +-@ `gui/dig`: Allow second bezier point, mirror-mode for freeform shapes, symmetry mode + +# 50.07-alpha2 + +## New Scripts +- `combine`: combines stacks of food and plant items. + +## Fixes +-@ `troubleshoot-item`: fix printing of job details for chosen item +-@ `makeown`: fixes errors caused by using makeown on an invader +-@ `gui/blueprint`: correctly use setting presets passed on the commandline +-@ `gui/quickfort`: correctly use settings presets passed on the commandline +- `devel/query`: can now properly index vectors in the --table argument +-@ `forbid`: fix detection of unreachable items for items in containers +-@ `unforbid`: fix detection of unreachable items for items in containers + +## Misc Improvements +- `troubleshoot-item`: output as bullet point list with indenting, with item description and ID at top +- `troubleshoot-item`: reports on items that are hidden, artifacts, in containers, and held by a unit +- `troubleshoot-item`: reports on the contents of containers with counts for each contained item type +- `devel/visualize-structure`: now automatically inspects the contents of most pointer fields, rather than inspecting the pointers themselves +- `devel/query`: will now search for jobs at the map coordinate highlighted, if no explicit job is highlighted and there is a map tile highlighted +- `caravan`: add trade screen overlay that assists with selecting groups of items and collapsing groups in the UI +- `gui/gm-editor`: will now inspect a selected building itself if the building has no current jobs + +## Removed +- `combine-drinks`: replaced by `combine` +- `combine-plants`: replaced by `combine` + +# 50.07-alpha1 + +## New Scripts +- `gui/design`: digging and construction designation tool with shapes and patterns +- `makeown`: makes the selected unit a citizen of your fortress + +## Fixes +-@ `gui/unit-syndromes`: allow the window widgets to be interacted with +-@ `fix/protect-nicks`: now works by setting the historical figure nickname +-@ `gui/liquids`: fixed issues with unit pathing after adding/removing liquids +-@ `gui/dig`: Fix for 'continuing' auto-stair designation. Avoid nil index issue for tile_type + +## Misc Improvements +- `gui/gm-editor`: now supports multiple independent data inspection windows +- `gui/gm-editor`: now prints out contents of coordinate vars instead of just the type +- `rejuvenate`: now takes an --age parameter to choose a desired age. +-@ `gui/dig` : Added 'Line' shape that also can draw curves, added draggable center handle + +# 50.05-alpha3.1 + +## Fixes +-@ `gui/launcher`: no longer resets to the Help tab on every keystroke + +# 50.05-alpha3 + +## New Scripts +- `autofish`: auto-manage fishing labors to control your stock of fish +- `gui/autofish`: GUI config and status panel interface for autofish +- `gui/automelt`: GUI config and status panel interface for automelt +- `gui/control-panel`: quick access to DFHack configuration +- `fix/civil-war`: removes negative relations with own government +- `fix/protect-nicks`: restore nicknames when DF loses them +- `forbid`: forbid and list forbidden items on the map +- `gui/unit-syndromes`: browser for syndrome information + +## Fixes +- `build-now`: now correctly avoids adjusting non-empty tiles above constructions that it builds +- `catsplosion`: now only affects live, active units +- `quickfort`: allow floor bars, floor grates, and hatches to be placed over all stair types like vanilla allows + +## Misc Improvements +- `ban-cooking`: ban announcements are now hidden by default; use new option ``--verbose`` to show them. +- `ban-cooking`: report number of items banned. +- `build-now`: now handles dirt roads and initializes farm plots properly +- `devel/click-monitor`: report on middle mouse button actions +-@ `gui/autochop`: hide uninteresting burrows by default +-@ `gui/blueprint`: allow map movement with the keyboard while the UI is open +- `gui/create-item`: support spawning corpse pieces (e.g. shells) under "body part" +- `gui/create-item`: added search and filter capabilities to the selection lists +- `gui/launcher`: make command output scrollback separate from the help text so players can continue to see the output of the previous command as they type the next one +- `gui/launcher`: allow double spacebar to pause/unpause the game, even while typing a command +- `gui/launcher`: clarify what is being shown in the autocomplete list (all commands, autocompletion of partially typed command, or commands related to typed command) +- `gui/launcher`: support running commands directly from the autocomplete list via double-clicking +- `gui/liquids`: interface overhaul, also now allows spawning river sources, setting/adding/removing liquid levels, and cleaning water from being salty or stagnant +- `gui/overlay`: now focuses on repositioning overlay widgets; enabling, disabling, and getting help for overlay widgets has moved to the new `gui/control-panel` +-@ `gui/quickcmd`: now acts like a regular window instead of a modal dialog +- `gui/quickfort`: don't close the window when applying a blueprint so players can apply the same blueprint multiple times more easily +- `locate-ore`: now only searches revealed tiles by default +- `modtools/spawn-liquid`: sets tile temperature to stable levels when spawning water or magma +-@ `prioritize`: pushing minecarts is now included in the default prioritization list +- `prioritize`: now automatically starts boosting the default list of job types when enabled +- `unforbid`: avoids unforbidding unreachable and underwater items by default +- `gui/create-item`: added whole corpse spawning alongside corpsepieces. (under "corpse") + +## Removed +- `show-unit-syndromes`: replaced by `gui/unit-syndromes`; html export is no longer supported + +# 50.05-alpha2 + +## Fixes +-@ `gui/gm-editor`: fix errors displayed while viewing help screen +- `build-now`: don't error on constructions that do not have an item attached + +## Removed +- `create-items`: replaced by `gui/create-item` ``--multi`` + +# 50.05-alpha1 + +## New Scripts +- `gui/autochop`: configuration frontend and status monitor for the `autochop` plugin +- `devel/tile-browser`: page through available textures and see their texture ids +- `allneeds`: list all unmet needs sorted by how many dwarves suffer from them. + +## Fixes +- `make-legendary`: "MilitaryUnarmed" option now functional + +## Misc Improvements +- `autounsuspend`: now saves its state with your fort +- `emigration`: now saves its state with your fort +- `prioritize`: now saves its state with your fort +- `unsuspend`: overlay now displays different letters for different suspend states so they can be differentiated in graphics mode (P=planning, x=suspended, X=repeatedly suspended) +- `unsuspend`: overlay now shows a marker all the time when in graphics mode. ascii mode still only shows when paused so that you can see what's underneath. +- `gui/gm-editor`: converted to a movable, resizable, mouse-enabled window +- `gui/launcher`: now supports a smaller, minimal mode. click the toggle in the launcher UI or start in minimal mode via the ``Ctrl-Shift-P`` keybinding +- `gui/launcher`: can now be dragged from anywhere on the window body +- `gui/launcher`: now remembers its size and position between invocations +- `gui/gm-unit`: converted to a movable, resizable, mouse-enabled window +- `gui/cp437-table`: converted to a movable, mouse-enabled window +- `gui/quickcmd`: converted to a movable, resizable, mouse-enabled window +- `gui/quickcmd`: commands are now stored globally so you don't have to recreate commands for every fort +- `devel/inspect-screen`: updated for new rendering semantics and can now also inspect map textures +- `exterminate`: added drown method. magma and drown methods will now clean up liquids automatically. + +## Documentation +- `devel/hello-world`: updated to be a better example from which to start new gui scripts From a4845fdfcbb45e38cad733f6c66fe0c6023b92f0 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 17:36:19 +0200 Subject: [PATCH 69/93] Update changelog.txt --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index a937e2fb2..e0c5ba707 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,6 +31,7 @@ Template for new versions: ## New Tools ## New Features +- `multihaul`: add multiple items to a single haul job with wheelbarrow ## Fixes - `gui/journal`: fix typo which caused the table of contents to always be regenerated even when not needed From e963afe6e44af7d367182a12dc2d4b284fcca353 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 17:37:39 +0200 Subject: [PATCH 70/93] Delete need-acquire.lua --- need-acquire.lua | 105 ----------------------------------------------- 1 file changed, 105 deletions(-) delete mode 100644 need-acquire.lua diff --git a/need-acquire.lua b/need-acquire.lua deleted file mode 100644 index ce81818f6..000000000 --- a/need-acquire.lua +++ /dev/null @@ -1,105 +0,0 @@ --- Assign trinkets to citizens so they can satisfy the "Acquire Object" need. --- Derived from an old Bay12 forums script and updated for modern DFHack. ---@module = true - -local help = [=[ -need-acquire -============ -Assign trinkets to citizens who have a strong "Acquire Object" need. - -Usage: - need-acquire [-t ] - -Options: - -t Focus level below which the need is considered unmet - (default: -3000). - -help Show this help text. -]=] - -local utils = require('utils') - -local valid_args = utils.invert{'help', 't'} - -local ACQUIRE_NEED_ID = df.need_type.AcquireObject -local acquire_threshold = -3000 - -local function get_citizens() - local result = {} - for _, unit in ipairs(dfhack.units.getCitizens(true)) do - if unit.profession ~= df.profession.BABY and - unit.profession ~= df.profession.CHILD then - table.insert(result, unit) - end - end - return result -end - -local function find_need(unit, need_id) - if not unit.status.current_soul then return nil end - local needs = unit.status.current_soul.personality.needs - for idx = #needs - 1, 0, -1 do - if needs[idx].id == need_id then - return needs[idx] - end - end -end - -local function get_free_trinkets() - local trinkets = {} - local function add(list) for _, i in ipairs(list) do table.insert(trinkets, i) end end - add(df.global.world.items.other.EARRING) - add(df.global.world.items.other.RING) - add(df.global.world.items.other.AMULET) - add(df.global.world.items.other.BRACELET) - local free = {} - for _, item in ipairs(trinkets) do - if not (item.flags.trader or item.flags.in_job or item.flags.construction or - item.flags.removed or item.flags.forbid or item.flags.dump or - item.flags.owned) then - table.insert(free, item) - end - end - return free -end - -local function give_items() - local trinkets = get_free_trinkets() - local needs, fulfilled = 0, 0 - local idx = 1 - for _, unit in ipairs(get_citizens()) do - local need = find_need(unit, ACQUIRE_NEED_ID) - if need and need.focus_level < acquire_threshold then - needs = needs + 1 - local item = trinkets[idx] - if item then - dfhack.items.setOwner(item, unit) - need.focus_level = 200 - need.need_level = 1 - fulfilled = fulfilled + 1 - idx = idx + 1 - end - end - end - local missing = needs - fulfilled - dfhack.println(('need-acquire | Need: %d Done: %d TODO: %d'):format(needs, fulfilled, missing)) - if missing > 0 then - dfhack.printerr('Need ' .. missing .. ' more trinkets to fulfill needs!') - end -end - -local function main(args) - args = utils.processArgs(args, valid_args) - if args.help then - print(help) - return - end - if args.t then - acquire_threshold = -tonumber(args.t) - end - give_items() -end - -if not dfhack_flags.module then - main({...}) -end - From 4b0f1871def5dd2a5263ac4c25eb793ada2c78d8 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 17:45:47 +0200 Subject: [PATCH 71/93] Update multihaul.lua From bb0d0452007f309ba74c7340efb005e38b8c967f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:47:39 +0000 Subject: [PATCH 72/93] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/multihaul.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/multihaul.rst b/docs/multihaul.rst index 35ef2355a..2e446ca9e 100644 --- a/docs/multihaul.rst +++ b/docs/multihaul.rst @@ -13,7 +13,7 @@ definitively attached to the job. By default, up to ten additional items within 10 tiles of the original item are collected. Warning: Destination stockpile filters are currently ignored by the job (because of DF logic). Which items qualify can be controlled with the ``--mode`` option. -Basic usage of wheelbarrows remains the same: dwarfs would use them only if hauling item is heavier than 75 +Basic usage of wheelbarrows remains the same: dwarfs would use them only if hauling item is heavier than 75 Usage ----- From 40cd43331cb30d4a0f57bc591e02dbe9f60d02cc Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 21:09:02 +0200 Subject: [PATCH 73/93] multihaul.lua: protection for already in stockpile items and autobreak for bugged jobs --- multihaul.lua | 98 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 74 insertions(+), 24 deletions(-) diff --git a/multihaul.lua b/multihaul.lua index d93898c68..7ef03cf38 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -4,6 +4,7 @@ local eventful = require('plugins.eventful') local utils = require('utils') +local itemtools = reqscript('item') local GLOBAL_KEY = 'multihaul' @@ -14,6 +15,7 @@ local function get_default_state() radius=10, max_items=10, mode='sametype', + autocancel=true } end @@ -60,6 +62,10 @@ local function add_nearby_items(job) local x,y,z = dfhack.items.getPosition(target) if not x then return end + local cond = {} + itemtools.condition_stockpiled(cond) + local is_stockpiled = cond[1] + local function matches(it) if state.mode == 'identical' then return items_identical(it, target) @@ -76,7 +82,9 @@ local function add_nearby_items(job) for _,it in ipairs(df.global.world.items.other.IN_PLAY) do if it ~= target and not it.flags.in_job and it.flags.on_ground and it.pos.z == z and math.abs(it.pos.x - x) <= state.radius and + not it:isWheelbarrow() and math.abs(it.pos.y - y) <= state.radius and + not is_stockpiled(it) and matches(it) then dfhack.job.attachJobItem(job, it, df.job_role_type.Hauled, -1, -1) count = count + 1 @@ -91,6 +99,34 @@ local function add_nearby_items(job) end end +local function find_attached_wheelbarrow(job) + for _, jitem in ipairs(job.items) do + local item = jitem.item + if item and df.item_toolst:is_instance(item) and item:isWheelbarrow() then + if jitem.role ~= df.job_role_type.PushHaulVehicle then + return nil + end + local ref = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) + if ref and ref.data.job == job then + return item + end + end + end +end + +local function finish_jobs_without_wheelbarrow() + local count = 0 + for _, job in utils.listpairs(df.global.world.jobs.list) do + if job.job_type == df.job_type.StoreItemInStockpile and + #job.items > 1 and not find_attached_wheelbarrow(job) then + clear_job_items(job) + job.completion_timer = 0 + count = count + 1 + end + end + return count +end + local function emptyContainedItems(wheelbarrow) local items = dfhack.items.getContainedItems(wheelbarrow) if #items == 0 then return end @@ -107,6 +143,15 @@ local function emptyContainedItems(wheelbarrow) end end dfhack.items.moveToGround(item, wheelbarrow.pos) + if state.autocancel then + local count = finish_jobs_without_wheelbarrow() + if count > 0 then + dfhack.gui.showAnnouncement( + string.format('multihaul: finished %d StoreItemInStockpile job', count), + COLOR_CYAN + ) + end + end end end @@ -117,21 +162,6 @@ local function clear_job_items(job) job.items:resize(0) end -local function find_attached_wheelbarrow(job) - for _, jitem in ipairs(job.items) do - local item = jitem.item - if item and df.item_toolst:is_instance(item) and item:isWheelbarrow() then - if jitem.role ~= df.job_role_type.PushHaulVehicle then - return nil - end - local ref = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) - if ref and ref.data.job == job then - return item - end - end - end -end - local function on_new_job(job) if job.job_type ~= df.job_type.StoreItemInStockpile then return end @@ -153,7 +183,9 @@ local function enable(val) end if dfhack.internal.IN_TEST then - unit_test_hooks = {on_new_job=on_new_job, enable=enable, load_state=load_state} + unit_test_hooks = {on_new_job=on_new_job, enable=enable, + load_state=load_state, + finish_jobs_without_wheelbarrow=finish_jobs_without_wheelbarrow} end -- state change handler @@ -188,9 +220,24 @@ local function parse_options(start_idx) while i <= #args do local a = args[i] if a == '--debug' then - state.debug_enabled = true - elseif a == '--no-debug' then - state.debug_enabled = false + local m = args[i + 1] + if m == 'off' or m == 'disable' then + state.debug_enabled = false + i = i + 1 + else + state.debug_enabled = true + end + elseif a == '--autocancel' then + local m = args[i + 1] + if m == 'on' or m == 'enable' then + state.autocancel = true + i = i + 1 + elseif m == 'off' or m == 'disable' then + state.autocancel = false + i = i + 1 + else + qerror('invalid autocancel option: ' .. tostring(m)) + end elseif a == '--radius' then i = i + 1 state.radius = tonumber(args[i]) or state.radius @@ -218,13 +265,16 @@ elseif cmd == 'disable' then enable(false) elseif cmd == 'status' or not cmd then print((state.enabled and 'multihaul is enabled' or 'multihaul is disabled')) - print(('radius=%d max-items=%d mode=%s debug=%s') - :format(state.radius, state.max_items, state.mode, state.debug_enabled and 'on' or 'off')) + print(('radius=%d max-items=%d mode=%s autocancel=%s debug=%s') + :format(state.radius, state.max_items, state.mode, state.autocancel and 'on' or 'off', state.debug_enabled and 'on' or 'off')) elseif cmd == 'config' then parse_options(2) persist_state() - print(('multihaul config: radius=%d max-items=%d mode=%s debug=%s') - :format(state.radius, state.max_items, state.mode, state.debug_enabled and 'on' or 'off')) + print(('multihaul config: radius=%d max-items=%d mode=%s autocancel=%s debug=%s') + :format(state.radius, state.max_items, state.mode, state.autocancel and 'on' or 'off', state.debug_enabled and 'on' or 'off')) +elseif cmd == 'finishjobs' then + local count = finish_jobs_without_wheelbarrow() + print(('finished %d StoreItemInStockpile job%s'):format(count, count == 1 and '' or 's')) else - qerror('Usage: multihaul [enable|disable|status|config] [--radius N] [--max-items N] [--mode MODE] [--debug|--no-debug]') + qerror('Usage: multihaul [enable|disable|status|config|finishjobs] [--radius N] [--max-items N] [--mode MODE] [--autocancel on|off] [--debug on|off]') end From 64ac19e37e0e948fe5791e81041dc060cfa2f94b Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 21:10:15 +0200 Subject: [PATCH 74/93] Update multihaul.lua From d31b5361c2b69fcfada26065aef966fbe0781cde Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Mon, 21 Jul 2025 21:13:59 +0200 Subject: [PATCH 75/93] Update multihaul.rst --- docs/multihaul.rst | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/multihaul.rst b/docs/multihaul.rst index 2e446ca9e..b7fef58ac 100644 --- a/docs/multihaul.rst +++ b/docs/multihaul.rst @@ -8,7 +8,9 @@ multihaul This tool allows dwarves to collect several adjacent items at once when performing hauling jobs with a wheelbarrow. When enabled, new ``StoreItemInStockpile`` jobs will automatically attach nearby items so -they can be hauled in a single trip. Items claimed by another jobs would be ignored. The script only triggers when a wheelbarrow is +they can be hauled in a single trip. Items claimed by another jobs would be ignored. +Items that are already stored in stockpiles are ignored. +The script only triggers when a wheelbarrow is definitively attached to the job. By default, up to ten additional items within 10 tiles of the original item are collected. Warning: Destination stockpile filters are currently ignored by the job (because of DF logic). Which items qualify can be controlled @@ -24,8 +26,10 @@ Usage multihaul disable multihaul status multihaul config [] + multihaul finishjobs The script can also be enabled persistently with ``enable multihaul``. +finishjobs is an additional command to find and cancel all broken jobs, related to multihaul Options ------- @@ -41,6 +45,8 @@ Options ``sametype`` only matches the item type (like STONE or WOOD), ``samesubtype`` requires type and subtype to match, and ``identical`` additionally matches material. The default is ``sametype``. -``--debug`` +``--autocancel `` + Auto run finishjobs from time to time. +``--debug `` Show debug messages via ``dfhack.gui.showAnnouncement`` when items are - attached. Use ``--no-debug`` to disable. + attached. From 8ae55725ce83310a8592715643e48e6bca36822a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 21 Jul 2025 19:14:24 +0000 Subject: [PATCH 76/93] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/multihaul.rst | 4 ++-- multihaul.lua | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/multihaul.rst b/docs/multihaul.rst index b7fef58ac..efa09f796 100644 --- a/docs/multihaul.rst +++ b/docs/multihaul.rst @@ -8,7 +8,7 @@ multihaul This tool allows dwarves to collect several adjacent items at once when performing hauling jobs with a wheelbarrow. When enabled, new ``StoreItemInStockpile`` jobs will automatically attach nearby items so -they can be hauled in a single trip. Items claimed by another jobs would be ignored. +they can be hauled in a single trip. Items claimed by another jobs would be ignored. Items that are already stored in stockpiles are ignored. The script only triggers when a wheelbarrow is definitively attached to the job. By default, up to ten additional items within @@ -46,7 +46,7 @@ Options subtype to match, and ``identical`` additionally matches material. The default is ``sametype``. ``--autocancel `` - Auto run finishjobs from time to time. + Auto run finishjobs from time to time. ``--debug `` Show debug messages via ``dfhack.gui.showAnnouncement`` when items are attached. diff --git a/multihaul.lua b/multihaul.lua index 7ef03cf38..8b2d62b7e 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -15,7 +15,7 @@ local function get_default_state() radius=10, max_items=10, mode='sametype', - autocancel=true + autocancel=true } end @@ -82,7 +82,7 @@ local function add_nearby_items(job) for _,it in ipairs(df.global.world.items.other.IN_PLAY) do if it ~= target and not it.flags.in_job and it.flags.on_ground and it.pos.z == z and math.abs(it.pos.x - x) <= state.radius and - not it:isWheelbarrow() and + not it:isWheelbarrow() and math.abs(it.pos.y - y) <= state.radius and not is_stockpiled(it) and matches(it) then @@ -143,7 +143,7 @@ local function emptyContainedItems(wheelbarrow) end end dfhack.items.moveToGround(item, wheelbarrow.pos) - if state.autocancel then + if state.autocancel then local count = finish_jobs_without_wheelbarrow() if count > 0 then dfhack.gui.showAnnouncement( From e94232cfd29cd41febdf00384c8cf00f6ec2c5ae Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Tue, 22 Jul 2025 23:55:05 +0200 Subject: [PATCH 77/93] multihaul: assign wheelbarrows to any job! --- multihaul.lua | 105 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 69 insertions(+), 36 deletions(-) diff --git a/multihaul.lua b/multihaul.lua index 8b2d62b7e..5b58fd75a 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -15,7 +15,7 @@ local function get_default_state() radius=10, max_items=10, mode='sametype', - autocancel=true + autocancel=true } end @@ -52,6 +52,25 @@ local function items_samesubtype(a, b) return a:getType() == b:getType() and a:getSubtype() == b:getSubtype() end +local function emptyContainedItems(wheelbarrow) + local items = dfhack.items.getContainedItems(wheelbarrow) + if #items == 0 then return end + + if state.debug_enabled then + dfhack.gui.showAnnouncement('multihaul: emptying wheelbarrow', COLOR_CYAN) + end + + for _, item in ipairs(items) do + if item.flags.in_job then + local job_ref = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) + if job_ref then + dfhack.job.removeJob(job_ref.data.job) + end + end + dfhack.items.moveToGround(item, wheelbarrow.pos) + end +end + local function add_nearby_items(job) if #job.items == 0 then return end @@ -82,7 +101,8 @@ local function add_nearby_items(job) for _,it in ipairs(df.global.world.items.other.IN_PLAY) do if it ~= target and not it.flags.in_job and it.flags.on_ground and it.pos.z == z and math.abs(it.pos.x - x) <= state.radius and - not it:isWheelbarrow() and + not df.item_toolst:is_instance(item) and + --not it._type == df.vehicle_minecartst and math.abs(it.pos.y - y) <= state.radius and not is_stockpiled(it) and matches(it) then @@ -102,7 +122,7 @@ end local function find_attached_wheelbarrow(job) for _, jitem in ipairs(job.items) do local item = jitem.item - if item and df.item_toolst:is_instance(item) and item:isWheelbarrow() then + if item and item:isWheelbarrow() then if jitem.role ~= df.job_role_type.PushHaulVehicle then return nil end @@ -114,44 +134,41 @@ local function find_attached_wheelbarrow(job) end end -local function finish_jobs_without_wheelbarrow() - local count = 0 - for _, job in utils.listpairs(df.global.world.jobs.list) do - if job.job_type == df.job_type.StoreItemInStockpile and - #job.items > 1 and not find_attached_wheelbarrow(job) then - clear_job_items(job) - job.completion_timer = 0 - count = count + 1 +local function find_free_wheelbarrow(stockpile) + if not df.building_stockpilest:is_instance(stockpile) then return nil end + local abs = math.abs + local items = df.global.world.items.other.TOOL + local sx, sy, sz = stockpile.centerx, stockpile.centery, stockpile.z + local max_radius = state.radius or 10 + + for _, item in ipairs(items) do + if item and item:isWheelbarrow() and not item.flags.in_job then + local pos = item.pos + local ix, iy, iz = pos.x, pos.y, pos.z + if ix and iy and iz and iz == sz then + local dx = abs(ix - sx) + local dy = abs(iy - sy) + if dx <= max_radius and dy <= max_radius then + return item + end + end end end - return count + return nil end -local function emptyContainedItems(wheelbarrow) - local items = dfhack.items.getContainedItems(wheelbarrow) - if #items == 0 then return end - - if state.debug_enabled then - dfhack.gui.showAnnouncement('multihaul: emptying wheelbarrow', COLOR_CYAN) - end - for _, item in ipairs(items) do - if item.flags.in_job then - local job_ref = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) - if job_ref then - dfhack.job.removeJob(job_ref.data.job) - end - end - dfhack.items.moveToGround(item, wheelbarrow.pos) - if state.autocancel then - local count = finish_jobs_without_wheelbarrow() - if count > 0 then - dfhack.gui.showAnnouncement( - string.format('multihaul: finished %d StoreItemInStockpile job', count), - COLOR_CYAN - ) - end - end +local function attach_free_wheelbarrow(job) + local stockpile = get_job_stockpile(job) + if not stockpile then return nil end + local wheelbarrow = find_free_wheelbarrow(stockpile) + if not wheelbarrow then return nil end + if dfhack.job.attachJobItem(job, wheelbarrow, + df.job_role_type.PushHaulVehicle, -1, -1) then + if state.debug_enabled then + dfhack.gui.showAnnouncement('multihaul: adding wheelbarrow to a job', COLOR_CYAN) + end + return wheelbarrow end end @@ -162,10 +179,26 @@ local function clear_job_items(job) job.items:resize(0) end +local function finish_jobs_without_wheelbarrow() + local count = 0 + local wheelbarrow + for _, job in utils.listpairs(df.global.world.jobs.list) do + if job.job_type == df.job_type.StoreItemInStockpile and + #job.items > 1 and not find_attached_wheelbarrow(job) then + wheelbarrow = attach_free_wheelbarrow(job) + on_new_job(job) + end + end + return count +end + local function on_new_job(job) if job.job_type ~= df.job_type.StoreItemInStockpile then return end local wheelbarrow = find_attached_wheelbarrow(job) + if not wheelbarrow then + wheelbarrow = attach_free_wheelbarrow(job) + end if not wheelbarrow then return end add_nearby_items(job) From 3ccf8d0a5b7e042f89fa835d9be5f6956586270e Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Wed, 23 Jul 2025 00:13:39 +0200 Subject: [PATCH 78/93] multihaul: optimize wheelbarrow assignment --- docs/multihaul.rst | 4 ++-- multihaul.lua | 54 ++++++++++++++++++++++++++++------------------ 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/docs/multihaul.rst b/docs/multihaul.rst index efa09f796..c42bb944d 100644 --- a/docs/multihaul.rst +++ b/docs/multihaul.rst @@ -45,8 +45,8 @@ Options ``sametype`` only matches the item type (like STONE or WOOD), ``samesubtype`` requires type and subtype to match, and ``identical`` additionally matches material. The default is ``sametype``. -``--autocancel `` - Auto run finishjobs from time to time. +``--autowheelbarrows `` + Automatically assign wheelbarrows to jobs that lack them. ``--debug `` Show debug messages via ``dfhack.gui.showAnnouncement`` when items are attached. diff --git a/multihaul.lua b/multihaul.lua index 5b58fd75a..32feef4d6 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -15,7 +15,7 @@ local function get_default_state() radius=10, max_items=10, mode='sametype', - autocancel=true + autowheelbarrows=true } end @@ -69,6 +69,15 @@ local function emptyContainedItems(wheelbarrow) end dfhack.items.moveToGround(item, wheelbarrow.pos) end + + if state.autowheelbarrows then + local count = finish_jobs_without_wheelbarrow() + if count > 0 and state.debug_enabled then + dfhack.gui.showAnnouncement( + string.format('multihaul: assigned wheelbarrows to %d job%s', count, count == 1 and '' or 's'), + COLOR_CYAN) + end + end end local function add_nearby_items(job) @@ -98,12 +107,13 @@ local function add_nearby_items(job) end local count = 0 + local abs = math.abs for _,it in ipairs(df.global.world.items.other.IN_PLAY) do if it ~= target and not it.flags.in_job and it.flags.on_ground and - it.pos.z == z and math.abs(it.pos.x - x) <= state.radius and - not df.item_toolst:is_instance(item) and - --not it._type == df.vehicle_minecartst and - math.abs(it.pos.y - y) <= state.radius and + it.pos.z == z and abs(it.pos.x - x) <= state.radius and + not it:isWheelbarrow() and + --not it._type == df.vehicle_minecartst and + abs(it.pos.y - y) <= state.radius and not is_stockpiled(it) and matches(it) then dfhack.job.attachJobItem(job, it, df.job_role_type.Hauled, -1, -1) @@ -165,9 +175,9 @@ local function attach_free_wheelbarrow(job) if not wheelbarrow then return nil end if dfhack.job.attachJobItem(job, wheelbarrow, df.job_role_type.PushHaulVehicle, -1, -1) then - if state.debug_enabled then - dfhack.gui.showAnnouncement('multihaul: adding wheelbarrow to a job', COLOR_CYAN) - end + if state.debug_enabled then + dfhack.gui.showAnnouncement('multihaul: adding wheelbarrow to a job', COLOR_CYAN) + end return wheelbarrow end end @@ -181,12 +191,14 @@ end local function finish_jobs_without_wheelbarrow() local count = 0 - local wheelbarrow for _, job in utils.listpairs(df.global.world.jobs.list) do if job.job_type == df.job_type.StoreItemInStockpile and #job.items > 1 and not find_attached_wheelbarrow(job) then - wheelbarrow = attach_free_wheelbarrow(job) - on_new_job(job) + local wheelbarrow = attach_free_wheelbarrow(job) + if wheelbarrow then + on_new_job(job) + count = count + 1 + end end end return count @@ -196,7 +208,7 @@ local function on_new_job(job) if job.job_type ~= df.job_type.StoreItemInStockpile then return end local wheelbarrow = find_attached_wheelbarrow(job) - if not wheelbarrow then + if not wheelbarrow then wheelbarrow = attach_free_wheelbarrow(job) end if not wheelbarrow then return end @@ -260,16 +272,16 @@ local function parse_options(start_idx) else state.debug_enabled = true end - elseif a == '--autocancel' then + elseif a == '--autowheelbarrows' then local m = args[i + 1] if m == 'on' or m == 'enable' then - state.autocancel = true + state.autowheelbarrows = true i = i + 1 elseif m == 'off' or m == 'disable' then - state.autocancel = false + state.autowheelbarrows = false i = i + 1 else - qerror('invalid autocancel option: ' .. tostring(m)) + qerror('invalid autowheelbarrows option: ' .. tostring(m)) end elseif a == '--radius' then i = i + 1 @@ -298,16 +310,16 @@ elseif cmd == 'disable' then enable(false) elseif cmd == 'status' or not cmd then print((state.enabled and 'multihaul is enabled' or 'multihaul is disabled')) - print(('radius=%d max-items=%d mode=%s autocancel=%s debug=%s') - :format(state.radius, state.max_items, state.mode, state.autocancel and 'on' or 'off', state.debug_enabled and 'on' or 'off')) + print(('radius=%d max-items=%d mode=%s autowheelbarrows=%s debug=%s') + :format(state.radius, state.max_items, state.mode, state.autowheelbarrows and 'on' or 'off', state.debug_enabled and 'on' or 'off')) elseif cmd == 'config' then parse_options(2) persist_state() - print(('multihaul config: radius=%d max-items=%d mode=%s autocancel=%s debug=%s') - :format(state.radius, state.max_items, state.mode, state.autocancel and 'on' or 'off', state.debug_enabled and 'on' or 'off')) + print(('multihaul config: radius=%d max-items=%d mode=%s autowheelbarrows=%s debug=%s') + :format(state.radius, state.max_items, state.mode, state.autowheelbarrows and 'on' or 'off', state.debug_enabled and 'on' or 'off')) elseif cmd == 'finishjobs' then local count = finish_jobs_without_wheelbarrow() print(('finished %d StoreItemInStockpile job%s'):format(count, count == 1 and '' or 's')) else - qerror('Usage: multihaul [enable|disable|status|config|finishjobs] [--radius N] [--max-items N] [--mode MODE] [--autocancel on|off] [--debug on|off]') + qerror('Usage: multihaul [enable|disable|status|config|finishjobs] [--radius N] [--max-items N] [--mode MODE] [--autowheelbarrows on|off] [--debug on|off]') end From 80501ddb9d6e4a3ac1dfb8ab6744a213f51c7cff Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Wed, 23 Jul 2025 00:20:18 +0200 Subject: [PATCH 79/93] multihaul: optimization --- multihaul.lua | 39 ++++----------------------------------- 1 file changed, 4 insertions(+), 35 deletions(-) diff --git a/multihaul.lua b/multihaul.lua index 32feef4d6..1157abbc4 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -69,15 +69,6 @@ local function emptyContainedItems(wheelbarrow) end dfhack.items.moveToGround(item, wheelbarrow.pos) end - - if state.autowheelbarrows then - local count = finish_jobs_without_wheelbarrow() - if count > 0 and state.debug_enabled then - dfhack.gui.showAnnouncement( - string.format('multihaul: assigned wheelbarrows to %d job%s', count, count == 1 and '' or 's'), - COLOR_CYAN) - end - end end local function add_nearby_items(job) @@ -105,19 +96,16 @@ local function add_nearby_items(job) return true end end - - local count = 0 + local abs = math.abs for _,it in ipairs(df.global.world.items.other.IN_PLAY) do if it ~= target and not it.flags.in_job and it.flags.on_ground and it.pos.z == z and abs(it.pos.x - x) <= state.radius and - not it:isWheelbarrow() and - --not it._type == df.vehicle_minecartst and + not df.item_toolst:is_instance(item) and abs(it.pos.y - y) <= state.radius and not is_stockpiled(it) and matches(it) then dfhack.job.attachJobItem(job, it, df.job_role_type.Hauled, -1, -1) - count = count + 1 if state.debug_enabled then dfhack.gui.showAnnouncement( ('multihaul: added %s to hauling job of %s'):format( @@ -189,21 +177,6 @@ local function clear_job_items(job) job.items:resize(0) end -local function finish_jobs_without_wheelbarrow() - local count = 0 - for _, job in utils.listpairs(df.global.world.jobs.list) do - if job.job_type == df.job_type.StoreItemInStockpile and - #job.items > 1 and not find_attached_wheelbarrow(job) then - local wheelbarrow = attach_free_wheelbarrow(job) - if wheelbarrow then - on_new_job(job) - count = count + 1 - end - end - end - return count -end - local function on_new_job(job) if job.job_type ~= df.job_type.StoreItemInStockpile then return end @@ -229,8 +202,7 @@ end if dfhack.internal.IN_TEST then unit_test_hooks = {on_new_job=on_new_job, enable=enable, - load_state=load_state, - finish_jobs_without_wheelbarrow=finish_jobs_without_wheelbarrow} + load_state=load_state} end -- state change handler @@ -317,9 +289,6 @@ elseif cmd == 'config' then persist_state() print(('multihaul config: radius=%d max-items=%d mode=%s autowheelbarrows=%s debug=%s') :format(state.radius, state.max_items, state.mode, state.autowheelbarrows and 'on' or 'off', state.debug_enabled and 'on' or 'off')) -elseif cmd == 'finishjobs' then - local count = finish_jobs_without_wheelbarrow() - print(('finished %d StoreItemInStockpile job%s'):format(count, count == 1 and '' or 's')) else - qerror('Usage: multihaul [enable|disable|status|config|finishjobs] [--radius N] [--max-items N] [--mode MODE] [--autowheelbarrows on|off] [--debug on|off]') + qerror('Usage: multihaul [enable|disable|status|config] [--radius N] [--max-items N] [--mode MODE] [--autowheelbarrows on|off] [--debug on|off]') end From 8f4c3e7feb2647362bdee4cdf9416071b3a3ba54 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Wed, 23 Jul 2025 00:22:43 +0200 Subject: [PATCH 80/93] Update multihaul.rst --- docs/multihaul.rst | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/docs/multihaul.rst b/docs/multihaul.rst index c42bb944d..b3d3095f3 100644 --- a/docs/multihaul.rst +++ b/docs/multihaul.rst @@ -7,12 +7,10 @@ multihaul This tool allows dwarves to collect several adjacent items at once when performing hauling jobs with a wheelbarrow. When enabled, new -``StoreItemInStockpile`` jobs will automatically attach nearby items so -they can be hauled in a single trip. Items claimed by another jobs would be ignored. -Items that are already stored in stockpiles are ignored. -The script only triggers when a wheelbarrow is -definitively attached to the job. By default, up to ten additional items within -10 tiles of the original item are collected. +``StoreItemInStockpile`` jobs with wheelbarrows will automatically attach nearby items so +they can be hauled in a single trip. Jobs without wheelbarrows would try to attach one if autowheelbarrows option is on. +Items claimed by another jobs or already stored in stockpiles would be ignored. +By default, up to ten additional items within 10 tiles of the original item are collected. Warning: Destination stockpile filters are currently ignored by the job (because of DF logic). Which items qualify can be controlled with the ``--mode`` option. Basic usage of wheelbarrows remains the same: dwarfs would use them only if hauling item is heavier than 75 @@ -26,10 +24,8 @@ Usage multihaul disable multihaul status multihaul config [] - multihaul finishjobs The script can also be enabled persistently with ``enable multihaul``. -finishjobs is an additional command to find and cancel all broken jobs, related to multihaul Options ------- From 759332cd85a19b7cda15334d2273e3c458057dcc Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Wed, 23 Jul 2025 00:29:53 +0200 Subject: [PATCH 81/93] Update multihaul.lua --- multihaul.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/multihaul.lua b/multihaul.lua index 1157abbc4..f65b390df 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -98,6 +98,7 @@ local function add_nearby_items(job) end local abs = math.abs + local count = 0 for _,it in ipairs(df.global.world.items.other.IN_PLAY) do if it ~= target and not it.flags.in_job and it.flags.on_ground and it.pos.z == z and abs(it.pos.x - x) <= state.radius and @@ -106,6 +107,7 @@ local function add_nearby_items(job) not is_stockpiled(it) and matches(it) then dfhack.job.attachJobItem(job, it, df.job_role_type.Hauled, -1, -1) + count = count + 1 if state.debug_enabled then dfhack.gui.showAnnouncement( ('multihaul: added %s to hauling job of %s'):format( From f2886aed514badc9512f0fa5707685008e7282c3 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Wed, 23 Jul 2025 00:48:13 +0200 Subject: [PATCH 82/93] multihaul: fix grabbing wheelbarrows and minecart logic --- multihaul.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/multihaul.lua b/multihaul.lua index f65b390df..b87a7cfe5 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -97,17 +97,17 @@ local function add_nearby_items(job) end end - local abs = math.abs local count = 0 for _,it in ipairs(df.global.world.items.other.IN_PLAY) do if it ~= target and not it.flags.in_job and it.flags.on_ground and - it.pos.z == z and abs(it.pos.x - x) <= state.radius and - not df.item_toolst:is_instance(item) and - abs(it.pos.y - y) <= state.radius and + it.pos.z == z and math.abs(it.pos.x - x) <= state.radius and + not it:isWheelbarrow() and + not dfhack.items.isRouteVehicle(it) and + math.abs(it.pos.y - y) <= state.radius and not is_stockpiled(it) and matches(it) then dfhack.job.attachJobItem(job, it, df.job_role_type.Hauled, -1, -1) - count = count + 1 + count = count + 1 if state.debug_enabled then dfhack.gui.showAnnouncement( ('multihaul: added %s to hauling job of %s'):format( From f21982fd7d70d77bb5ee202d9233e806f5d8bca7 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Wed, 23 Jul 2025 01:31:45 +0200 Subject: [PATCH 83/93] multihaul: optimizing item search --- multihaul.lua | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/multihaul.lua b/multihaul.lua index b87a7cfe5..286a63b6a 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -34,6 +34,26 @@ local function load_state() utils.assign(state, dfhack.persistent.getSiteData(GLOBAL_KEY, state)) end +local function for_each_item_in_radius(x, y, z, radius, fn) + local xmin = math.max(0, x - radius) + local xmax = math.min(df.global.world.map.x_count - 1, x + radius) + local ymin = math.max(0, y - radius) + local ymax = math.min(df.global.world.map.y_count - 1, y + radius) + local bxmin, bxmax = math.floor(xmin/16), math.floor(xmax/16) + local bymin, bymax = math.floor(ymin/16), math.floor(ymax/16) + for by = bymin, bymax do + for bx = bxmin, bxmax do + local block = dfhack.maps.getTileBlock(bx*16, by*16, z) + if block then + for _, id in ipairs(block.items) do + local item = df.item.find(id) + if item and fn(item) then return end + end + end + end + end +end + local function get_job_stockpile(job) local ref = dfhack.job.getGeneralRef(job, df.general_ref_type.BUILDING_HOLDER) return ref and df.building.find(ref.building_id) or nil @@ -97,26 +117,22 @@ local function add_nearby_items(job) end end - local count = 0 - for _,it in ipairs(df.global.world.items.other.IN_PLAY) do + local count = 0 + for_each_item_in_radius(x, y, z, state.radius, function(it) if it ~= target and not it.flags.in_job and it.flags.on_ground and - it.pos.z == z and math.abs(it.pos.x - x) <= state.radius and - not it:isWheelbarrow() and - not dfhack.items.isRouteVehicle(it) and - math.abs(it.pos.y - y) <= state.radius and - not is_stockpiled(it) and - matches(it) then + not it:isWheelbarrow() and not dfhack.items.isRouteVehicle(it) and + not is_stockpiled(it) and matches(it) then dfhack.job.attachJobItem(job, it, df.job_role_type.Hauled, -1, -1) - count = count + 1 + count = count + 1 if state.debug_enabled then dfhack.gui.showAnnouncement( ('multihaul: added %s to hauling job of %s'):format( dfhack.items.getDescription(it, 0), dfhack.items.getDescription(target, 0)), COLOR_CYAN) end - if count >= state.max_items then break end + if count >= state.max_items then return true end end - end + end) end local function find_attached_wheelbarrow(job) From d8eded8b4e1489d3abf6bab0c94350597aee9821 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Wed, 23 Jul 2025 01:47:14 +0200 Subject: [PATCH 84/93] refactor multihaul item search --- multihaul.lua | 48 +++++++++++++++++++++--------------------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/multihaul.lua b/multihaul.lua index 286a63b6a..80e2454dd 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -72,6 +72,18 @@ local function items_samesubtype(a, b) return a:getType() == b:getType() and a:getSubtype() == b:getSubtype() end +local match_fns = { + any = function() return true end, + identical = items_identical, + sametype = items_sametype, + samesubtype = items_samesubtype, +} + +local function items_match(a, b) + local fn = match_fns[state.mode] or match_fns.sametype + return fn(a, b) +end + local function emptyContainedItems(wheelbarrow) local items = dfhack.items.getContainedItems(wheelbarrow) if #items == 0 then return end @@ -106,17 +118,9 @@ local function add_nearby_items(job) local is_stockpiled = cond[1] local function matches(it) - if state.mode == 'identical' then - return items_identical(it, target) - elseif state.mode == 'sametype' then - return items_sametype(it, target) - elseif state.mode == 'samesubtype' then - return items_samesubtype(it, target) - else - return true - end + return items_match(it, target) end - + local count = 0 for_each_item_in_radius(x, y, z, state.radius, function(it) if it ~= target and not it.flags.in_job and it.flags.on_ground and @@ -152,25 +156,15 @@ end local function find_free_wheelbarrow(stockpile) if not df.building_stockpilest:is_instance(stockpile) then return nil end - local abs = math.abs - local items = df.global.world.items.other.TOOL local sx, sy, sz = stockpile.centerx, stockpile.centery, stockpile.z - local max_radius = state.radius or 10 - - for _, item in ipairs(items) do - if item and item:isWheelbarrow() and not item.flags.in_job then - local pos = item.pos - local ix, iy, iz = pos.x, pos.y, pos.z - if ix and iy and iz and iz == sz then - local dx = abs(ix - sx) - local dy = abs(iy - sy) - if dx <= max_radius and dy <= max_radius then - return item - end - end + local found + for_each_item_in_radius(sx, sy, sz, state.radius or 10, function(it) + if it:isWheelbarrow() and not it.flags.in_job then + found = it + return true end - end - return nil + end) + return found end From 2b4f7b985756cff65536a21b494b0630ee17fb20 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Wed, 23 Jul 2025 02:07:35 +0200 Subject: [PATCH 85/93] multihaul: added protection for returning wheelbarrow jobs And limited wheelbarrow search radius for the sake of performance --- multihaul.lua | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/multihaul.lua b/multihaul.lua index 80e2454dd..41e374a36 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -144,7 +144,7 @@ local function find_attached_wheelbarrow(job) local item = jitem.item if item and item:isWheelbarrow() then if jitem.role ~= df.job_role_type.PushHaulVehicle then - return nil + return 'badrole' end local ref = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) if ref and ref.data.job == job then @@ -152,13 +152,14 @@ local function find_attached_wheelbarrow(job) end end end + return nil end local function find_free_wheelbarrow(stockpile) if not df.building_stockpilest:is_instance(stockpile) then return nil end local sx, sy, sz = stockpile.centerx, stockpile.centery, stockpile.z local found - for_each_item_in_radius(sx, sy, sz, state.radius or 10, function(it) + for_each_item_in_radius(sx, sy, sz, state.radius*10 or 100, function(it) if it:isWheelbarrow() and not it.flags.in_job then found = it return true @@ -191,13 +192,13 @@ end local function on_new_job(job) if job.job_type ~= df.job_type.StoreItemInStockpile then return end - local wheelbarrow = find_attached_wheelbarrow(job) + if wheelbarrow == 'badrole' then return + end if not wheelbarrow then wheelbarrow = attach_free_wheelbarrow(job) end if not wheelbarrow then return end - add_nearby_items(job) emptyContainedItems(wheelbarrow) end From a76460f7729f2368c6b11c1adeff114d5ff4b9f1 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Wed, 23 Jul 2025 02:08:57 +0200 Subject: [PATCH 86/93] multihaul: removed magic number on wheelbarrow_search_radius_k --- multihaul.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/multihaul.lua b/multihaul.lua index 41e374a36..995b52865 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -13,6 +13,7 @@ local function get_default_state() enabled=false, debug_enabled=false, radius=10, + wheelbarrow_search_radius_k=5, max_items=10, mode='sametype', autowheelbarrows=true @@ -159,7 +160,7 @@ local function find_free_wheelbarrow(stockpile) if not df.building_stockpilest:is_instance(stockpile) then return nil end local sx, sy, sz = stockpile.centerx, stockpile.centery, stockpile.z local found - for_each_item_in_radius(sx, sy, sz, state.radius*10 or 100, function(it) + for_each_item_in_radius(sx, sy, sz, state.radius*wheelbarrow_search_radius_k or 10*wheelbarrow_search_radius_k, function(it) if it:isWheelbarrow() and not it.flags.in_job then found = it return true From bbead1186a049d5345448b49aa222fd85e569376 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sun, 27 Jul 2025 00:30:03 +0200 Subject: [PATCH 87/93] wheelbarrow_search_radius_k now is called correctly --- multihaul.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multihaul.lua b/multihaul.lua index 995b52865..019c8c2f7 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -160,7 +160,7 @@ local function find_free_wheelbarrow(stockpile) if not df.building_stockpilest:is_instance(stockpile) then return nil end local sx, sy, sz = stockpile.centerx, stockpile.centery, stockpile.z local found - for_each_item_in_radius(sx, sy, sz, state.radius*wheelbarrow_search_radius_k or 10*wheelbarrow_search_radius_k, function(it) + for_each_item_in_radius(sx, sy, sz, state.radius*state.wheelbarrow_search_radius_k or 10*state.wheelbarrow_search_radius_k, function(it) if it:isWheelbarrow() and not it.flags.in_job then found = it return true From 94100455857c6450ea60a34d81606ab4d32bbd32 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sun, 27 Jul 2025 00:50:08 +0200 Subject: [PATCH 88/93] multihaul: clear lost wheelbarrow jobs when emptying --- docs/multihaul.rst | 4 ++++ multihaul.lua | 14 +++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/multihaul.rst b/docs/multihaul.rst index b3d3095f3..1f1475486 100644 --- a/docs/multihaul.rst +++ b/docs/multihaul.rst @@ -24,9 +24,13 @@ Usage multihaul disable multihaul status multihaul config [] + multihaul finish The script can also be enabled persistently with ``enable multihaul``. +``multihaul finish`` cancels any ``StoreItemInStockpile`` jobs that have lost +their wheelbarrows, freeing attached items. + Options ------- diff --git a/multihaul.lua b/multihaul.lua index 019c8c2f7..82c5bec08 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -102,6 +102,7 @@ local function emptyContainedItems(wheelbarrow) end dfhack.items.moveToGround(item, wheelbarrow.pos) end + finish_jobs_without_wheelbarrow() end local function add_nearby_items(job) @@ -191,6 +192,15 @@ local function clear_job_items(job) job.items:resize(0) end +local function finish_jobs_without_wheelbarrow() + for _, job in utils.listpairs(df.global.world.jobs.list) do + if job.job_type == df.job_type.StoreItemInStockpile and + not find_attached_wheelbarrow(job) then + dfhack.job.removeJob(job) + end + end +end + local function on_new_job(job) if job.job_type ~= df.job_type.StoreItemInStockpile then return end local wheelbarrow = find_attached_wheelbarrow(job) @@ -303,6 +313,8 @@ elseif cmd == 'config' then persist_state() print(('multihaul config: radius=%d max-items=%d mode=%s autowheelbarrows=%s debug=%s') :format(state.radius, state.max_items, state.mode, state.autowheelbarrows and 'on' or 'off', state.debug_enabled and 'on' or 'off')) +elseif cmd == 'finish' then + finish_jobs_without_wheelbarrow() else - qerror('Usage: multihaul [enable|disable|status|config] [--radius N] [--max-items N] [--mode MODE] [--autowheelbarrows on|off] [--debug on|off]') + qerror('Usage: multihaul [enable|disable|status|config|finish] [--radius N] [--max-items N] [--mode MODE] [--autowheelbarrows on|off] [--debug on|off]') end From d910921a53177e4e76d2ef0cb441094ed71f2d69 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sun, 27 Jul 2025 01:00:32 +0200 Subject: [PATCH 89/93] fix multihaul forward declaration --- multihaul.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/multihaul.lua b/multihaul.lua index 82c5bec08..e9e0da5b6 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -8,6 +8,8 @@ local itemtools = reqscript('item') local GLOBAL_KEY = 'multihaul' +local finish_jobs_without_wheelbarrow -- forward declaration + local function get_default_state() return { enabled=false, @@ -192,7 +194,7 @@ local function clear_job_items(job) job.items:resize(0) end -local function finish_jobs_without_wheelbarrow() +function finish_jobs_without_wheelbarrow() for _, job in utils.listpairs(df.global.world.jobs.list) do if job.job_type == df.job_type.StoreItemInStockpile and not find_attached_wheelbarrow(job) then From 6049eae122df591b2d3861e1233bbd18eb86df77 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sun, 27 Jul 2025 01:09:38 +0200 Subject: [PATCH 90/93] autowheelbarrow option now works correctly --- multihaul.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/multihaul.lua b/multihaul.lua index e9e0da5b6..4d32b0dc3 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -8,7 +8,7 @@ local itemtools = reqscript('item') local GLOBAL_KEY = 'multihaul' -local finish_jobs_without_wheelbarrow -- forward declaration +local finish_jobs_without_wheelbarrow local function get_default_state() return { @@ -208,10 +208,10 @@ local function on_new_job(job) local wheelbarrow = find_attached_wheelbarrow(job) if wheelbarrow == 'badrole' then return end - if not wheelbarrow then + if not wheelbarrow and state.autowheelbarrows then wheelbarrow = attach_free_wheelbarrow(job) end - if not wheelbarrow then return end + if not wheelbarrow or wheelbarrow.flags.in_job then return end add_nearby_items(job) emptyContainedItems(wheelbarrow) end @@ -315,8 +315,8 @@ elseif cmd == 'config' then persist_state() print(('multihaul config: radius=%d max-items=%d mode=%s autowheelbarrows=%s debug=%s') :format(state.radius, state.max_items, state.mode, state.autowheelbarrows and 'on' or 'off', state.debug_enabled and 'on' or 'off')) -elseif cmd == 'finish' then +elseif cmd == 'unstuckjobs' then finish_jobs_without_wheelbarrow() else - qerror('Usage: multihaul [enable|disable|status|config|finish] [--radius N] [--max-items N] [--mode MODE] [--autowheelbarrows on|off] [--debug on|off]') + qerror('Usage: multihaul [enable|disable|status|config|unstuckjobs] [--radius N] [--max-items N] [--mode MODE] [--autowheelbarrows on|off] [--debug on|off]') end From a334119e73bda1bdb2821f0c32c2e93e42727f28 Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sun, 27 Jul 2025 01:15:02 +0200 Subject: [PATCH 91/93] Removed breaking wheelbarrow.flags.in_job --- multihaul.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multihaul.lua b/multihaul.lua index 4d32b0dc3..fc4dadcb1 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -211,7 +211,7 @@ local function on_new_job(job) if not wheelbarrow and state.autowheelbarrows then wheelbarrow = attach_free_wheelbarrow(job) end - if not wheelbarrow or wheelbarrow.flags.in_job then return end + if not wheelbarrow then return end add_nearby_items(job) emptyContainedItems(wheelbarrow) end From b976bdf845325eb2f97bd4ce3ba00d37ef7260fe Mon Sep 17 00:00:00 2001 From: LOIRELAB <66545056+LoireLab@users.noreply.github.com> Date: Sun, 27 Jul 2025 01:26:18 +0200 Subject: [PATCH 92/93] finish_jobs_without_wheelbarrow function expanded --- multihaul.lua | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/multihaul.lua b/multihaul.lua index fc4dadcb1..f77bf7654 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -187,20 +187,27 @@ local function attach_free_wheelbarrow(job) end end -local function clear_job_items(job) - if state.debug_enabled then - dfhack.gui.showAnnouncement('multihaul: clearing stuck hauling job', COLOR_CYAN) - end - job.items:resize(0) -end - function finish_jobs_without_wheelbarrow() + local count = 0 for _, job in utils.listpairs(df.global.world.jobs.list) do - if job.job_type == df.job_type.StoreItemInStockpile and - not find_attached_wheelbarrow(job) then - dfhack.job.removeJob(job) + if job.job_type == df.job_type.StoreItemInStockpile and #job.items > 1 and not find_attached_wheelbarrow(job) then + for _, jobitem in ipairs(job.items) do + local item = jobitem.item + if item and item.flags.in_job then + local ref = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) + if ref and ref.data.job == job then + dfhack.job.removeJob(job) + end + end + end + job.items:resize(0) + job.completion_timer = 0 + count = count + 1 end end + if count > 0 then + dfhack.gui.showAnnouncement('multihaul: clearing stuck hauling job', COLOR_CYAN) + end end local function on_new_job(job) From f30d4b274277f5cd00b8a8a4e0556363c3c6eed5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 30 Jul 2025 17:19:21 +0000 Subject: [PATCH 93/93] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- multihaul.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/multihaul.lua b/multihaul.lua index f77bf7654..512784447 100644 --- a/multihaul.lua +++ b/multihaul.lua @@ -205,9 +205,9 @@ function finish_jobs_without_wheelbarrow() count = count + 1 end end - if count > 0 then - dfhack.gui.showAnnouncement('multihaul: clearing stuck hauling job', COLOR_CYAN) - end + if count > 0 then + dfhack.gui.showAnnouncement('multihaul: clearing stuck hauling job', COLOR_CYAN) + end end local function on_new_job(job)