From a98250b6bd6f34b4874784a9171dc7ed205f8f9d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 24 Dec 2024 19:55:43 -0800 Subject: [PATCH 1/3] initial implementation of fix/stuck-squad --- changelog.txt | 1 + docs/fix/stuck-squad.rst | 24 ++++++++++ fix/stuck-squad.lua | 100 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 docs/fix/stuck-squad.rst create mode 100644 fix/stuck-squad.lua diff --git a/changelog.txt b/changelog.txt index acae15c38..796d6c63b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,6 +27,7 @@ Template for new versions: # Future ## New Tools +- `fix/stuck-squad`: allow squads returning from missions to rescue other squads that have gotten stuck on the world map ## New Features diff --git a/docs/fix/stuck-squad.rst b/docs/fix/stuck-squad.rst new file mode 100644 index 000000000..13bfc0bca --- /dev/null +++ b/docs/fix/stuck-squad.rst @@ -0,0 +1,24 @@ +fix/stuck-squad +=============== + +.. dfhack-tool:: + :summary: Allow squads returning from missions to rescue lost squads. + :tags: fort bugfix military + +Occasionally, squads that you send out on a mission get stuck on the world map. +They lose their ability to navigate and are unable to return to your fortress. +This tool finds another of your squads that is returning from a mission and +assigns them to rescue the lost squad. + +This fix is enabled by default in the DFHack +`control panel `, or you can run it as needed. However, it +is still up to you to send out another squad that can be tasked with the rescue +mission. You can send the rescue squad out on an innocuous "Demand tribute" +mission to minimize risk to the squad. + +Usage +----- + +:: + + fix/stuck-squad diff --git a/fix/stuck-squad.lua b/fix/stuck-squad.lua new file mode 100644 index 000000000..987fabbdc --- /dev/null +++ b/fix/stuck-squad.lua @@ -0,0 +1,100 @@ +--@ module=true + +local utils = require('utils') + +-- from observing bugged saves, this condition appears to be unique to stuck armies +local function is_army_stuck(army) + return army.controller_id ~= 0 and not army.controller +end + +-- if army is currently camping, we'll need to go up the chain +local function get_top_controller(controller) + if not controller then return end + if controller.master_id == controller.id then return controller end + return df.army_controller.find(controller.master_id) +end + +local function is_army_valid_and_returning(army) + local controller = get_top_controller(army.controller) + if not controller or controller.goal ~= df.army_controller_goal_type.SITE_INVASION then + return false, false + end + return true, controller.data.goal_site_invasion.flag.RETURNING_HOME +end + +-- need to check all squad positions since some members may have died +local function get_squad_army(squad) + if not squad then return end + for _,sp in ipairs(squad.positions) do + local hf = df.historical_figure.find(sp.occupant) + if not hf then goto continue end + local army = df.army.find(hf.info and hf.info.whereabouts and hf.info.whereabouts.army_id or -1) + if army then return army end + ::continue:: + end +end + +-- called by gui/notify notification +function scan_fort_armies() + local stuck_armies, outbound_army, returning_army = {}, nil, nil + local govt = df.historical_entity.find(df.global.plotinfo.group_id) + if not govt then return stuck_armies, outbound_army, returning_army end + + for _,squad_id in ipairs(govt.squads) do + local squad = df.squad.find(squad_id) + local army = get_squad_army(squad) + if not army then goto continue end + if is_army_stuck(army) then + table.insert(stuck_armies, {squad=squad, army=army}) + elseif not returning_army then + local valid, returning = is_army_valid_and_returning(army) + if valid then + if returning then + returning_army = {squad=squad, army=army} + else + outbound_army = {squad=squad, army=army} + end + end + end + ::continue:: + end + return stuck_armies, outbound_army, returning_army +end + +local function unstick_armies() + local stuck_armies, outbound_army, returning_army = scan_fort_armies() + if #stuck_armies == 0 then return end + if not returning_army then + local instructions = outbound_army + and ('Please wait for %s to complete their objective and run this command again when they are on their way home.'):format( + dfhack.df2console(dfhack.military.getSquadName(outbound_army.squad.id))) + or 'Please send a squad out on a mission that will return to the fort, and'.. + ' run this command again when they are on the way home.' + qerror(('%d stuck arm%s found, but no returning armies found to rescue them!\n%s'):format( + #stuck_armies, #stuck_armies == 1 and 'y' or 'ies', instructions)) + return + end + local returning_squad_name = dfhack.df2console(dfhack.military.getSquadName(returning_army.squad.id)) + for _,stuck in ipairs(stuck_armies) do + print(('fix/stuck-squad: Squad rescue operation underway! %s is rescuing %s'):format( + returning_squad_name, dfhack.military.getSquadName(stuck.squad.id))) + for _,member in ipairs(stuck.army.members) do + local nemesis = df.nemesis_record.find(member.nemesis_id) + if not nemesis or not nemesis.figure then goto continue end + local hf = nemesis.figure + if hf.info and hf.info.whereabouts then + hf.info.whereabouts.army_id = returning_army.army.id + end + utils.insert_sorted(returning_army.army.members, member, 'nemesis_id') + ::continue:: + end + stuck.army.members:resize(0) + utils.insert_sorted(get_top_controller(returning_army.army.controller).assigned_squads, stuck.squad.id) + end +end + +if dfhack_flags.module then + return +end + +unstick_armies() From e64a88e2e99c8813a11b7942eb59c8c744698fff Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 24 Dec 2024 19:55:57 -0800 Subject: [PATCH 2/3] add control panel registry entry for fix/stuck-squad --- internal/control-panel/registry.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index 04c374df7..04b28b3ff 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -88,8 +88,9 @@ COMMANDS_BY_IDX = { params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/ownership', ']'}}, {command='fix/protect-nicks', group='bugfix', mode='enable', default=true}, {command='fix/stuck-instruments', group='bugfix', mode='repeat', default=true, - desc='Fix activity references on stuck instruments to make them usable again.', params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/stuck-instruments', ']'}}, + {command='fix/stuck-squad', group='bugfix', mode='repeat', default=true, + params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/stuck-squad', ']'}}, {command='fix/stuck-worship', group='bugfix', mode='repeat', default=true, params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/stuck-worship', '-q', ']'}}, {command='fix/noexert-exhaustion', group='bugfix', mode='repeat', default=true, From cc2dc94599511fc76c8f23aae55c8aef474c4b8b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 24 Dec 2024 19:56:08 -0800 Subject: [PATCH 3/3] add notification for stuck squad and a player needs to take action --- internal/notify/notifications.lua | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/internal/notify/notifications.lua b/internal/notify/notifications.lua index cd3341af6..160f9f813 100644 --- a/internal/notify/notifications.lua +++ b/internal/notify/notifications.lua @@ -1,8 +1,11 @@ --@module = true +local dlg = require('gui.dialogs') local gui = require('gui') local json = require('json') local list_agreements = reqscript('list-agreements') +local repeat_util = require('repeat-util') +local stuck_squad = reqscript('fix/stuck-squad') local warn_stranded = reqscript('warn-stranded') local CONFIG_FILE = 'dfhack-config/notify.json' @@ -302,6 +305,34 @@ end -- the order of this list controls the order the notifications will appear in the overlay NOTIFICATIONS_BY_IDX = { + { + name='stuck_squad', + desc='Notifies when a squad is stuck on the world map.', + default=true, + dwarf_fn=function() + local stuck_armies, outbound_army, returning_army = stuck_squad.scan_fort_armies() + if #stuck_armies == 0 then return end + if repeat_util.isScheduled('control-panel/fix/stuck-squad') and (outbound_army or returning_army) then + return + end + return ('%d squad%s need%s rescue'):format( + #stuck_armies, + #stuck_armies == 1 and '' or 's', + #stuck_armies == 1 and 's' or '' + ) + end, + on_click=function() + local message = 'A squad is lost on the world map and needs rescue!\n\n' .. + 'Please send a squad out on a mission that will return to the fort.\n' .. + 'They will rescue the stuck squad on their way home.' + if not repeat_util.isScheduled('control-panel/fix/stuck-squad') then + message = message .. '\n\n' .. + 'Please enable fix/stuck-squad in the DFHack control panel to allow\n'.. + 'the rescue mission to happen.' + end + dlg.showMessage('Rescue stuck squads', message, COLOR_WHITE) + end, + }, { name='traders_ready', desc='Notifies when traders are ready to trade at the depot.',