Skip to content

Commit a02935d

Browse files
New Feature: autotraining (#1411)
* New Feature: `gym` Code for dwarves to hit the gym when they yearn for the gains. Assigns Dwarves to a military squad until they have fulfilled their need for Martial Training * Fix whitespace * missed some * MORE whitespace (and some other cleanup) * Update gym.lua * Create gym.rst * Fix EOF * Update gym.rst * fix key error * more key errors * Update the documentation * Use the enable/disable stuff not args to start or stop * Do the documentation in one place * Various fixes - Clean up documentation - Add option to change squad name. - persist the enabled state, the threshold, and the squad name. - fixed findNeed function - renamed script to `autotraining` - made the ignore flag more clear and more changable - fixed 1 sided military link in `addTraining` * More cleanup Also tell the user when data was persisted (mostly for debugging) * rename the script itself and update the docs to account. * fix docs * Add credit where credit is due * add to control panel alert the user if the squad cant be found (since we cant reliably make a squad ourselves... yet) * Check the squad's entity_id to make sure we get *our* Gym * Update autotraining.lua remove the `.` because it could lead to confusion * Fix the ignore count never being reset * Fix units that need training but are already doing so being reported as queued * fix the ignore count (it should be global) * Apply suggestions from code review * fix typo * fix to actually check the unit's squad * Update for gui usage * clean up * initial gui and update from code review * show alias in gui too * clean up * Create gui docs * update the docs * remove non-existant name args in docs * fix typo in message * fix trainees being labeled as queued * add ignore nobles * Remove more debug code * Gui cleanup * Update to use the Military Module * use the squad position * Remove all training dwarves when you disable * disable autotraining on map unload * Apply suggestions from code review Co-authored-by: Christian Doczkal <20443222+chdoc@users.noreply.github.com> * fix erroneous training numbers * Update autotraining.rst * remove outdated debug logging * remove outdated comment * fix up silly code in `removeTraining` * Update autotraining.lua * Update autotraining.lua * clean and de-nest training candidates * remove units who don't need training * use our precomputed good squads list * forgot a nil check * only count as ignored if they are ignored * Apply suggestions from code review Co-authored-by: Christian Doczkal <20443222+chdoc@users.noreply.github.com> * code review changes * Fix up from testing improvements to `autotraining`: - fix the argument error - avoid the double execution of the loop when enabling - consistently only count ignored units when they would otherwise qualify for training - allow enabling the tool from within `gui/autotraining` - sort the list of training candidates, so that the most needed candidates are preferred for training - move the argument handling out of the `start` function Co-Authored-By: Christian Doczkal <20443222+chdoc@users.noreply.github.com> * fix whitespace error * Update and fix changelog * only process cli args if we are running in the cli * skip units in squads (dont mark as ignored tho) --------- Co-authored-by: Christian Doczkal <20443222+chdoc@users.noreply.github.com>
1 parent b36c739 commit a02935d

File tree

7 files changed

+638
-0
lines changed

7 files changed

+638
-0
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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ 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
3032

3133
## New Features
3234

docs/autotraining.rst

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
autotraining
2+
============
3+
4+
.. dfhack-tool::
5+
:summary: Assigns citizens to a military squad until they have fulfilled their need for Martial Training
6+
:tags: fort auto bugfix units
7+
8+
This script automatically assigns citizens with the need for military training to designated training squads.
9+
10+
You need to have at least one squad that is set up for training. The squad should be set to "Constant Training" in the military screen. The squad doesn't need months off. The members leave the squad once they have satisfied their need for military training.
11+
12+
The configured uniform determines the skills that are acquired by the training dwarves. Providing "No Uniform" is a perfectly valid choice and will make your militarily inclined civilians become wrestlers over time. However, you can also provide weapons and armor to pre-train civilians for future drafts.
13+
14+
Once you have made squads for training use `gui/autotraining` to select the squads and ignored units, as well as the needs threshhold.
15+
16+
Usage
17+
-----
18+
19+
``autotraining [<options>]``
20+
21+
Examples
22+
--------
23+
24+
``autotraining``
25+
Current status of script
26+
27+
``enable autotraining``
28+
Checks to see if you have fullfilled the creation of a training squad.
29+
If there is no squad marked for training use, a clickable notification will appear letting you know to set one up/
30+
Searches your fort for dwarves with a need for military training, and begins assigning them to a training squad.
31+
Once they have fulfilled their need they will be removed from their squad to be replaced by the next dwarf in the list.
32+
33+
``disable autotraining``
34+
Stops adding new units to the squad.
35+
36+
Options
37+
-------
38+
``-t``
39+
Use integer values. (Default 5000)
40+
The negative need threshhold to trigger for each citizen
41+
The greater the number the longer before a dwarf is added to the waiting list.

docs/gui/autotraining.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
gui/autotraining
2+
================
3+
4+
.. dfhack-tool::
5+
:summary: GUI interface for ``autotraining``
6+
:tags: fort auto interface
7+
8+
This is an in-game configuration interface for `autotraining`. You can pick squads for training, select ignored units, and set the needs threshold.
9+
10+
Usage
11+
-----
12+
13+
::
14+
15+
gui/autotraining

0 commit comments

Comments
 (0)