Skip to content

Commit 5b4072e

Browse files
authored
Merge branch 'master' into uniform-unstick-fix
2 parents 0831e6e + 22c9058 commit 5b4072e

File tree

12 files changed

+1330
-45
lines changed

12 files changed

+1330
-45
lines changed

autotraining.lua

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
-- Based on the original code by RNGStrategist (who also got some help from Uncle Danny)
2+
--@ enable = true
3+
--@ module = true
4+
5+
local repeatUtil = require('repeat-util')
6+
local utils=require('utils')
7+
8+
local GLOBAL_KEY = "autotraining"
9+
local MartialTraining = df.need_type['MartialTraining']
10+
local ignore_count = 0
11+
12+
local function get_default_state()
13+
return {
14+
enabled=false,
15+
threshold=-5000,
16+
ignored={},
17+
ignored_nobles={},
18+
training_squads = {},
19+
}
20+
end
21+
22+
state = state or get_default_state()
23+
24+
function isEnabled()
25+
return state.enabled
26+
end
27+
28+
-- persisting a table with numeric keys results in a json array with a huge number of null entries
29+
-- therefore, we convert the keys to strings for persistence
30+
local function to_persist(persistable)
31+
local persistable_ignored = {}
32+
for k, v in pairs(persistable) do
33+
persistable_ignored[tostring(k)] = v
34+
end
35+
return persistable_ignored
36+
end
37+
38+
-- loads both from the older array format and the new string table format
39+
local function from_persist(persistable)
40+
if not persistable then
41+
return
42+
end
43+
local ret = {}
44+
for k, v in pairs(persistable) do
45+
ret[tonumber(k)] = v
46+
end
47+
return ret
48+
end
49+
50+
function persist_state()
51+
dfhack.persistent.saveSiteData(GLOBAL_KEY, {
52+
enabled=state.enabled,
53+
threshold=state.threshold,
54+
ignored=to_persist(state.ignored),
55+
ignored_nobles=state.ignored_nobles,
56+
training_squads=to_persist(state.training_squads)
57+
})
58+
end
59+
60+
--- Load the saved state of the script
61+
local function load_state()
62+
-- load persistent data
63+
local persisted_data = dfhack.persistent.getSiteData(GLOBAL_KEY, {})
64+
state.enabled = persisted_data.enabled or state.enabled
65+
state.threshold = persisted_data.threshold or state.threshold
66+
state.ignored = from_persist(persisted_data.ignored) or state.ignored
67+
state.ignored_nobles = persisted_data.ignored_nobles or state.ignored_nobles
68+
state.training_squads = from_persist(persisted_data.training_squads) or state.training_squads
69+
return state
70+
end
71+
72+
dfhack.onStateChange[GLOBAL_KEY] = function(sc)
73+
if sc == SC_MAP_UNLOADED then
74+
state.enabled = false
75+
return
76+
end
77+
-- the state changed, is a map loaded and is that map in fort mode?
78+
if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then
79+
-- no its isnt, so bail
80+
return
81+
end
82+
-- yes it was, so:
83+
84+
-- retrieve state saved in game. merge with default state so config
85+
-- saved from previous versions can pick up newer defaults.
86+
load_state()
87+
if state.enabled then
88+
start()
89+
end
90+
persist_state()
91+
end
92+
93+
94+
--######
95+
--Functions
96+
--######
97+
local function isIgnoredNoble(unit)
98+
local noblePos = dfhack.units.getNoblePositions(unit)
99+
if noblePos ~= nil then
100+
for _, position in ipairs(noblePos) do
101+
if state.ignored_nobles[position.position.code] then
102+
return true
103+
end
104+
end
105+
end
106+
return false
107+
end
108+
109+
---@return table<integer, { ['unit']: df.unit, ['need']: integer }>
110+
function getTrainingCandidates()
111+
local ret = {}
112+
ignore_count = 0
113+
for _, unit in ipairs(dfhack.units.getCitizens(true)) do
114+
if not dfhack.units.isAdult(unit) then
115+
goto next_unit
116+
end
117+
local need = getTrainingNeed(unit)
118+
if not need or need.focus_level >= state.threshold then
119+
goto next_unit
120+
end
121+
-- ignored units are those that would like to train but are forbidden from doing so
122+
if state.ignored[unit.id] then
123+
ignore_count = ignore_count + 1
124+
goto next_unit
125+
end
126+
if isIgnoredNoble(unit) then
127+
ignore_count = ignore_count + 1
128+
goto next_unit
129+
end
130+
if unit.military.squad_id ~= -1 then
131+
goto next_unit
132+
end
133+
table.insert(ret, { unit = unit, need = need.focus_level })
134+
::next_unit::
135+
end
136+
table.sort(ret, function (a, b) return a.need < b.need end)
137+
return ret
138+
end
139+
140+
function getTrainingSquads()
141+
local squads = {}
142+
for squad_id, _ in pairs(state.training_squads) do
143+
local squad = df.squad.find(squad_id)
144+
if squad then
145+
table.insert(squads, squad)
146+
else
147+
-- setting to nil during iteration is permitted by lua
148+
state.training_squads[squad_id] = nil
149+
end
150+
end
151+
return squads
152+
end
153+
154+
function getTrainingNeed(unit)
155+
if unit == nil then return nil end
156+
local needs = unit.status.current_soul.personality.needs
157+
for _, need in ipairs(needs) do
158+
if need.id == MartialTraining then
159+
return need
160+
end
161+
end
162+
return nil
163+
end
164+
165+
--######
166+
--Main
167+
--######
168+
169+
-- Find all training squads
170+
-- Abort if no squads found
171+
function checkSquads()
172+
local squads = {}
173+
for _, squad in ipairs(getTrainingSquads()) do
174+
if squad.entity_id == df.global.plotinfo.group_id then
175+
local leader = squad.positions[0].occupant
176+
if leader ~= -1 then
177+
table.insert(squads,squad)
178+
end
179+
end
180+
end
181+
182+
if #squads == 0 then
183+
return nil
184+
end
185+
186+
return squads
187+
end
188+
189+
function addTraining(unit,good_squads)
190+
if unit.military.squad_id ~= -1 then
191+
for _, squad in ipairs(good_squads) do
192+
if unit.military.squad_id == squad.id then
193+
return true
194+
end
195+
end
196+
return false
197+
end
198+
for _, squad in ipairs(good_squads) do
199+
for i=1,9,1 do
200+
if squad.positions[i].occupant == -1 then
201+
return dfhack.military.addToSquad(unit.id,squad.id,i)
202+
end
203+
end
204+
end
205+
206+
return false
207+
end
208+
209+
function removeAll()
210+
if state.training_squads == nil then return end
211+
for _, squad in ipairs(getTrainingSquads()) do
212+
for i=1,9,1 do
213+
local hf = df.historical_figure.find(squad.positions[i].occupant)
214+
if hf ~= nil then
215+
dfhack.military.removeFromSquad(hf.unit_id)
216+
end
217+
end
218+
end
219+
end
220+
221+
222+
function check()
223+
local squads = checkSquads()
224+
local intraining_count = 0
225+
local inque_count = 0
226+
if squads == nil then return end
227+
for _,squad in ipairs(squads) do
228+
for i=1,9,1 do
229+
if squad.positions[i].occupant ~= -1 then
230+
local hf = df.historical_figure.find(squad.positions[i].occupant)
231+
if hf ~= nil then
232+
local unit = df.unit.find(hf.unit_id)
233+
local training_need = getTrainingNeed(unit)
234+
if not training_need or training_need.focus_level >= state.threshold then
235+
dfhack.military.removeFromSquad(unit.id)
236+
end
237+
end
238+
end
239+
end
240+
end
241+
for _, p in ipairs(getTrainingCandidates()) do
242+
local added = addTraining(p.unit, squads)
243+
if added then
244+
intraining_count = intraining_count +1
245+
else
246+
inque_count = inque_count +1
247+
end
248+
end
249+
print(("%s: %d training, %d waiting, and %d excluded units with training needs"):
250+
format(GLOBAL_KEY, intraining_count, inque_count, ignore_count))
251+
end
252+
253+
function start()
254+
repeatUtil.scheduleEvery(GLOBAL_KEY, 1, 'days', check)
255+
end
256+
257+
function stop()
258+
repeatUtil.cancel(GLOBAL_KEY)
259+
end
260+
261+
function enable()
262+
state.enabled = true
263+
persist_state()
264+
start()
265+
end
266+
267+
function disable()
268+
state.enabled = false
269+
persist_state()
270+
stop()
271+
removeAll()
272+
end
273+
274+
if dfhack_flags.module then
275+
return
276+
end
277+
278+
validArgs = utils.invert({
279+
't'
280+
})
281+
282+
local args = utils.processArgs({...}, validArgs)
283+
284+
if dfhack_flags.enable then
285+
if dfhack_flags.enable_state then
286+
enable()
287+
else
288+
disable()
289+
end
290+
else
291+
-- called on the command-line
292+
if args.t then
293+
state.threshold = 0-tonumber(args.t)
294+
end
295+
print(("autotraining is %s"):format(state.enabled and "enabled" or "disabled"))
296+
end

changelog.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,15 @@ Template for new versions:
2727
# Future
2828

2929
## New Tools
30+
- `autotraining`: new tool to assign citizens to a military squad when they need Martial Training
31+
- `gui/autotraining`: configuration tool for autotraining
32+
- `entomb`: allow any unit that has a corpse or body parts to be assigned a tomb zone
3033

3134
## New Features
35+
- `deathcause`: added functionality to this script to fetch cause of death programatically
3236

3337
## Fixes
38+
- `immortal-cravings`: prioritize high-value meals, properly split of portions, and don't go eating or drinking on a full stomach
3439
- `uniform-unstick`: no longer causes units to equip multiples of assigned items
3540

3641
## Misc Improvements

0 commit comments

Comments
 (0)