Skip to content

Commit

Permalink
Merge pull request #1351 from myk002/myk_stuck_squad
Browse files Browse the repository at this point in the history
new tool: fix/stuck-squad
  • Loading branch information
myk002 authored Dec 25, 2024
2 parents cb8fc90 + cc2dc94 commit 68ee5bb
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 1 deletion.
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
24 changes: 24 additions & 0 deletions docs/fix/stuck-squad.rst
Original file line number Diff line number Diff line change
@@ -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 <gui/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
100 changes: 100 additions & 0 deletions fix/stuck-squad.lua
Original file line number Diff line number Diff line change
@@ -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()
3 changes: 2 additions & 1 deletion internal/control-panel/registry.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
31 changes: 31 additions & 0 deletions internal/notify/notifications.lua
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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.',
Expand Down

0 comments on commit 68ee5bb

Please sign in to comment.