Skip to content

Commit 68ee5bb

Browse files
authored
Merge pull request #1351 from myk002/myk_stuck_squad
new tool: fix/stuck-squad
2 parents cb8fc90 + cc2dc94 commit 68ee5bb

File tree

5 files changed

+158
-1
lines changed

5 files changed

+158
-1
lines changed

changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Template for new versions:
2727
# Future
2828

2929
## New Tools
30+
- `fix/stuck-squad`: allow squads returning from missions to rescue other squads that have gotten stuck on the world map
3031

3132
## New Features
3233

docs/fix/stuck-squad.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
fix/stuck-squad
2+
===============
3+
4+
.. dfhack-tool::
5+
:summary: Allow squads returning from missions to rescue lost squads.
6+
:tags: fort bugfix military
7+
8+
Occasionally, squads that you send out on a mission get stuck on the world map.
9+
They lose their ability to navigate and are unable to return to your fortress.
10+
This tool finds another of your squads that is returning from a mission and
11+
assigns them to rescue the lost squad.
12+
13+
This fix is enabled by default in the DFHack
14+
`control panel <gui/control-panel>`, or you can run it as needed. However, it
15+
is still up to you to send out another squad that can be tasked with the rescue
16+
mission. You can send the rescue squad out on an innocuous "Demand tribute"
17+
mission to minimize risk to the squad.
18+
19+
Usage
20+
-----
21+
22+
::
23+
24+
fix/stuck-squad

fix/stuck-squad.lua

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
--@ module=true
2+
3+
local utils = require('utils')
4+
5+
-- from observing bugged saves, this condition appears to be unique to stuck armies
6+
local function is_army_stuck(army)
7+
return army.controller_id ~= 0 and not army.controller
8+
end
9+
10+
-- if army is currently camping, we'll need to go up the chain
11+
local function get_top_controller(controller)
12+
if not controller then return end
13+
if controller.master_id == controller.id then return controller end
14+
return df.army_controller.find(controller.master_id)
15+
end
16+
17+
local function is_army_valid_and_returning(army)
18+
local controller = get_top_controller(army.controller)
19+
if not controller or controller.goal ~= df.army_controller_goal_type.SITE_INVASION then
20+
return false, false
21+
end
22+
return true, controller.data.goal_site_invasion.flag.RETURNING_HOME
23+
end
24+
25+
-- need to check all squad positions since some members may have died
26+
local function get_squad_army(squad)
27+
if not squad then return end
28+
for _,sp in ipairs(squad.positions) do
29+
local hf = df.historical_figure.find(sp.occupant)
30+
if not hf then goto continue end
31+
local army = df.army.find(hf.info and hf.info.whereabouts and hf.info.whereabouts.army_id or -1)
32+
if army then return army end
33+
::continue::
34+
end
35+
end
36+
37+
-- called by gui/notify notification
38+
function scan_fort_armies()
39+
local stuck_armies, outbound_army, returning_army = {}, nil, nil
40+
local govt = df.historical_entity.find(df.global.plotinfo.group_id)
41+
if not govt then return stuck_armies, outbound_army, returning_army end
42+
43+
for _,squad_id in ipairs(govt.squads) do
44+
local squad = df.squad.find(squad_id)
45+
local army = get_squad_army(squad)
46+
if not army then goto continue end
47+
if is_army_stuck(army) then
48+
table.insert(stuck_armies, {squad=squad, army=army})
49+
elseif not returning_army then
50+
local valid, returning = is_army_valid_and_returning(army)
51+
if valid then
52+
if returning then
53+
returning_army = {squad=squad, army=army}
54+
else
55+
outbound_army = {squad=squad, army=army}
56+
end
57+
end
58+
end
59+
::continue::
60+
end
61+
return stuck_armies, outbound_army, returning_army
62+
end
63+
64+
local function unstick_armies()
65+
local stuck_armies, outbound_army, returning_army = scan_fort_armies()
66+
if #stuck_armies == 0 then return end
67+
if not returning_army then
68+
local instructions = outbound_army
69+
and ('Please wait for %s to complete their objective and run this command again when they are on their way home.'):format(
70+
dfhack.df2console(dfhack.military.getSquadName(outbound_army.squad.id)))
71+
or 'Please send a squad out on a mission that will return to the fort, and'..
72+
' run this command again when they are on the way home.'
73+
qerror(('%d stuck arm%s found, but no returning armies found to rescue them!\n%s'):format(
74+
#stuck_armies, #stuck_armies == 1 and 'y' or 'ies', instructions))
75+
return
76+
end
77+
local returning_squad_name = dfhack.df2console(dfhack.military.getSquadName(returning_army.squad.id))
78+
for _,stuck in ipairs(stuck_armies) do
79+
print(('fix/stuck-squad: Squad rescue operation underway! %s is rescuing %s'):format(
80+
returning_squad_name, dfhack.military.getSquadName(stuck.squad.id)))
81+
for _,member in ipairs(stuck.army.members) do
82+
local nemesis = df.nemesis_record.find(member.nemesis_id)
83+
if not nemesis or not nemesis.figure then goto continue end
84+
local hf = nemesis.figure
85+
if hf.info and hf.info.whereabouts then
86+
hf.info.whereabouts.army_id = returning_army.army.id
87+
end
88+
utils.insert_sorted(returning_army.army.members, member, 'nemesis_id')
89+
::continue::
90+
end
91+
stuck.army.members:resize(0)
92+
utils.insert_sorted(get_top_controller(returning_army.army.controller).assigned_squads, stuck.squad.id)
93+
end
94+
end
95+
96+
if dfhack_flags.module then
97+
return
98+
end
99+
100+
unstick_armies()

internal/control-panel/registry.lua

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,9 @@ COMMANDS_BY_IDX = {
8888
params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/ownership', ']'}},
8989
{command='fix/protect-nicks', group='bugfix', mode='enable', default=true},
9090
{command='fix/stuck-instruments', group='bugfix', mode='repeat', default=true,
91-
desc='Fix activity references on stuck instruments to make them usable again.',
9291
params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/stuck-instruments', ']'}},
92+
{command='fix/stuck-squad', group='bugfix', mode='repeat', default=true,
93+
params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/stuck-squad', ']'}},
9394
{command='fix/stuck-worship', group='bugfix', mode='repeat', default=true,
9495
params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/stuck-worship', '-q', ']'}},
9596
{command='fix/noexert-exhaustion', group='bugfix', mode='repeat', default=true,

internal/notify/notifications.lua

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
--@module = true
22

3+
local dlg = require('gui.dialogs')
34
local gui = require('gui')
45
local json = require('json')
56
local list_agreements = reqscript('list-agreements')
7+
local repeat_util = require('repeat-util')
8+
local stuck_squad = reqscript('fix/stuck-squad')
69
local warn_stranded = reqscript('warn-stranded')
710

811
local CONFIG_FILE = 'dfhack-config/notify.json'
@@ -302,6 +305,34 @@ end
302305

303306
-- the order of this list controls the order the notifications will appear in the overlay
304307
NOTIFICATIONS_BY_IDX = {
308+
{
309+
name='stuck_squad',
310+
desc='Notifies when a squad is stuck on the world map.',
311+
default=true,
312+
dwarf_fn=function()
313+
local stuck_armies, outbound_army, returning_army = stuck_squad.scan_fort_armies()
314+
if #stuck_armies == 0 then return end
315+
if repeat_util.isScheduled('control-panel/fix/stuck-squad') and (outbound_army or returning_army) then
316+
return
317+
end
318+
return ('%d squad%s need%s rescue'):format(
319+
#stuck_armies,
320+
#stuck_armies == 1 and '' or 's',
321+
#stuck_armies == 1 and 's' or ''
322+
)
323+
end,
324+
on_click=function()
325+
local message = 'A squad is lost on the world map and needs rescue!\n\n' ..
326+
'Please send a squad out on a mission that will return to the fort.\n' ..
327+
'They will rescue the stuck squad on their way home.'
328+
if not repeat_util.isScheduled('control-panel/fix/stuck-squad') then
329+
message = message .. '\n\n' ..
330+
'Please enable fix/stuck-squad in the DFHack control panel to allow\n'..
331+
'the rescue mission to happen.'
332+
end
333+
dlg.showMessage('Rescue stuck squads', message, COLOR_WHITE)
334+
end,
335+
},
305336
{
306337
name='traders_ready',
307338
desc='Notifies when traders are ready to trade at the depot.',

0 commit comments

Comments
 (0)