Skip to content
Navigation Menu
{{ message }}
forked from DFHack/dfhack
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathautonestbox.cpp
More file actions
411 lines (355 loc) · 14.1 KB
/
Copy pathautonestbox.cpp
File metadata and controls
411 lines (355 loc) · 14.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
// - full automation of handling mini-pastures over nestboxes:
// go through all pens, check if they are empty and placed over a nestbox
// find female tame egg-layer who is not assigned to another pen and assign it to nestbox pasture
// maybe check for minimum age? it's not that useful to fill nestboxes with freshly hatched birds
// state and sleep setting is saved the first time autonestbox is started (to avoid writing stuff if the plugin is never used)
#include <string>
#include <vector>
#include "df/building_cagest.h"
#include "df/building_civzonest.h"
#include "df/building_nest_boxst.h"
#include "df/general_ref_building_civzone_assignedst.h"
#include "df/world.h"
#include "Debug.h"
#include "LuaTools.h"
#include "PluginManager.h"
#include "modules/Buildings.h"
#include "modules/Gui.h"
#include "modules/Persistence.h"
#include "modules/Units.h"
#include "modules/World.h"
using std::string;
using std::vector;
using namespace DFHack;
DFHACK_PLUGIN("autonestbox");
DFHACK_PLUGIN_IS_ENABLED(is_enabled);
REQUIRE_GLOBAL(world);
namespace DFHack {
// for configuration-related logging
DBG_DECLARE(autonestbox, status, DebugCategory::LINFO);
// for logging during the periodic scan
DBG_DECLARE(autonestbox, cycle, DebugCategory::LINFO);
}
static const string CONFIG_KEY = string(plugin_name) + "/config";
static PersistentDataItem config;
enum ConfigValues {
CONFIG_IS_ENABLED = 0,
CONFIG_CYCLE_TICKS = 1,
};
static int get_config_val(int index) {
if (!config.isValid())
return -1;
return config.ival(index);
}
static bool get_config_bool(int index) {
return get_config_val(index) == 1;
}
static void set_config_val(int index, int value) {
if (config.isValid())
config.ival(index) = value;
}
static void set_config_bool(int index, bool value) {
set_config_val(index, value ? 1 : 0);
}
static bool did_complain = false; // avoids message spam
static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle
static command_result df_autonestbox(color_ostream &out, vector<string> ¶meters);
static void autonestbox_cycle(color_ostream &out);
DFhackCExport command_result plugin_init(color_ostream &out, std::vector <PluginCommand> &commands) {
commands.push_back(PluginCommand(
plugin_name,
"Auto-assign egg-laying female pets to nestbox zones.",
df_autonestbox));
return CR_OK;
}
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) {
if (!Core::getInstance().isWorldLoaded()) {
out.printerr("Cannot enable %s without a loaded world.\n", plugin_name);
return CR_FAILURE;
}
if (enable != is_enabled) {
is_enabled = enable;
DEBUG(status,out).print("%s from the API; persisting\n",
is_enabled ? "enabled" : "disabled");
set_config_bool(CONFIG_IS_ENABLED, is_enabled);
} else {
DEBUG(status,out).print("%s from the API, but already %s; no action\n",
is_enabled ? "enabled" : "disabled",
is_enabled ? "enabled" : "disabled");
}
return CR_OK;
}
DFhackCExport command_result plugin_load_data (color_ostream &out) {
cycle_timestamp = 0;
config = World::GetPersistentData(CONFIG_KEY);
if (!config.isValid()) {
DEBUG(status,out).print("no config found in this save; initializing\n");
config = World::AddPersistentData(CONFIG_KEY);
set_config_bool(CONFIG_IS_ENABLED, is_enabled);
set_config_val(CONFIG_CYCLE_TICKS, 6000);
}
// we have to copy our enabled flag into the global plugin variable, but
// all the other state we can directly read/modify from the persistent
// data structure.
is_enabled = get_config_bool(CONFIG_IS_ENABLED);
DEBUG(status,out).print("loading persisted enabled state: %s\n",
is_enabled ? "true" : "false");
did_complain = false;
return CR_OK;
}
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) {
if (event == DFHack::SC_WORLD_UNLOADED) {
if (is_enabled) {
DEBUG(status,out).print("world unloaded; disabling %s\n",
plugin_name);
is_enabled = false;
}
}
return CR_OK;
}
DFhackCExport command_result plugin_onupdate(color_ostream &out) {
if (is_enabled && world->frame_counter - cycle_timestamp >= get_config_val(CONFIG_CYCLE_TICKS))
autonestbox_cycle(out);
return CR_OK;
}
/////////////////////////////////////////////////////
// configuration interface
//
struct autonestbox_options {
// whether to display help
bool help = false;
// whether to run a cycle right now
bool now = false;
// how many ticks to wait between automatic cycles, -1 means unset
int32_t ticks = -1;
static struct_identity _identity;
};
static const struct_field_info autonestbox_options_fields[] = {
{ struct_field_info::PRIMITIVE, "help", offsetof(autonestbox_options, help), &df::identity_traits<bool>::identity, 0, 0 },
{ struct_field_info::PRIMITIVE, "now", offsetof(autonestbox_options, now), &df::identity_traits<bool>::identity, 0, 0 },
{ struct_field_info::PRIMITIVE, "ticks", offsetof(autonestbox_options, ticks), &df::identity_traits<int32_t>::identity, 0, 0 },
{ struct_field_info::END }
};
struct_identity autonestbox_options::_identity(sizeof(autonestbox_options), &df::allocator_fn<autonestbox_options>, NULL, "autonestbox_options", NULL, autonestbox_options_fields);
static bool get_options(color_ostream &out,
autonestbox_options &opts,
const vector<string> ¶meters)
{
auto L = Lua::Core::State;
Lua::StackUnwinder top(L);
if (!lua_checkstack(L, parameters.size() + 2) ||
!Lua::PushModulePublic(
out, L, "plugins.autonestbox", "parse_commandline")) {
out.printerr("Failed to load autonestbox Lua code\n");
return false;
}
Lua::Push(L, &opts);
for (const string ¶m : parameters)
Lua::Push(L, param);
if (!Lua::SafeCall(out, L, parameters.size() + 1, 0))
return false;
return true;
}
static command_result df_autonestbox(color_ostream &out, vector<string> ¶meters) {
CoreSuspender suspend;
if (!Core::getInstance().isWorldLoaded()) {
out.printerr("Cannot run %s without a loaded world.\n", plugin_name);
return CR_FAILURE;
}
autonestbox_options opts;
if (!get_options(out, opts, parameters) || opts.help)
return CR_WRONG_USAGE;
if (opts.ticks > -1) {
set_config_val(CONFIG_CYCLE_TICKS, opts.ticks);
INFO(status,out).print("New cycle timer: %d ticks.\n", opts.ticks);
}
else if (opts.now) {
autonestbox_cycle(out);
}
else {
out << "autonestbox is " << (is_enabled ? "" : "not ") << "running" << std::endl;
}
return CR_OK;
}
/////////////////////////////////////////////////////
// cycle logic
//
static bool isEmptyPasture(df::building *building) {
if (!Buildings::isPenPasture(building))
return false;
df::building_civzonest *civ = (df::building_civzonest *)building;
return (civ->assigned_units.size() == 0);
}
static bool isFreeNestboxAtPos(int32_t x, int32_t y, int32_t z) {
for (auto building : world->buildings.all) {
if (building->getType() == df::building_type::NestBox
&& building->x1 == x
&& building->y1 == y
&& building->z == z) {
df::building_nest_boxst *nestbox = (df::building_nest_boxst *)building;
if (nestbox->claimed_by == -1 && nestbox->contained_items.size() == 1) {
return true;
}
}
}
return false;
}
static df::building* findFreeNestboxZone() {
for (auto building : world->buildings.all) {
if (isEmptyPasture(building) &&
Buildings::isActive(building) &&
isFreeNestboxAtPos(building->x1, building->y1, building->z)) {
return building;
}
}
return NULL;
}
static bool isInBuiltCage(df::unit *unit) {
for (auto building : world->buildings.all) {
if (building->getType() == df::building_type::Cage) {
df::building_cagest* cage = (df::building_cagest *)building;
for (auto unitid : cage->assigned_units) {
if (unitid == unit->id)
return true;
}
}
}
return false;
}
// check if assigned to pen, pit, (built) cage or chain
// note: BUILDING_CAGED is not set for animals (maybe it's used for dwarves who get caged as sentence)
// animals in cages (no matter if built or on stockpile) get the ref CONTAINED_IN_ITEM instead
// removing them from cages on stockpiles is no problem even without clearing the ref
// and usually it will be desired behavior to do so.
static bool isAssigned(df::unit *unit) {
for (auto ref : unit->general_refs) {
auto rtype = ref->getType();
if(rtype == df::general_ref_type::BUILDING_CIVZONE_ASSIGNED
|| rtype == df::general_ref_type::BUILDING_CAGED
|| rtype == df::general_ref_type::BUILDING_CHAIN
|| (rtype == df::general_ref_type::CONTAINED_IN_ITEM && isInBuiltCage(unit))) {
return true;
}
}
return false;
}
static bool isFreeEgglayer(df::unit *unit)
{
return Units::isActive(unit) && !Units::isUndead(unit)
&& Units::isFemale(unit)
&& Units::isTame(unit)
&& Units::isOwnCiv(unit)
&& Units::isEggLayer(unit)
&& !isAssigned(unit)
&& !Units::isGrazer(unit) // exclude grazing birds because they're messy
&& !Units::isMerchant(unit) // don't steal merchant mounts
&& !Units::isForest(unit); // don't steal birds from traders, they hate that
}
static df::unit * findFreeEgglayer() {
for (auto unit : world->units.all) {
if (isFreeEgglayer(unit))
return unit;
}
return NULL;
}
static df::general_ref_building_civzone_assignedst * createCivzoneRef() {
static bool vt_initialized = false;
// after having run successfully for the first time it's safe to simply create the object
if (vt_initialized) {
return (df::general_ref_building_civzone_assignedst *)
df::general_ref_building_civzone_assignedst::_identity.instantiate();
}
// being called for the first time, need to initialize the vtable
for (auto creature : world->units.all) {
for (auto ref : creature->general_refs) {
if (ref->getType() == df::general_ref_type::BUILDING_CIVZONE_ASSIGNED) {
if (strict_virtual_cast<df::general_ref_building_civzone_assignedst>(ref)) {
vt_initialized = true;
// !! calling new() doesn't work, need _identity.instantiate() instead !!
return (df::general_ref_building_civzone_assignedst *)
df::general_ref_building_civzone_assignedst::_identity.instantiate();
}
}
}
}
return NULL;
}
static bool assignUnitToZone(color_ostream &out, df::unit *unit, df::building *building) {
// try to get a fresh civzone ref
df::general_ref_building_civzone_assignedst *ref = createCivzoneRef();
if (!ref) {
ERR(cycle,out).print("Could not find a clonable activity zone reference!"
" You need to manually pen/pasture/pit at least one creature"
" before autonestbox can function.\n");
return false;
}
ref->building_id = building->id;
unit->general_refs.push_back(ref);
df::building_civzonest *civz = (df::building_civzonest *)building;
civz->assigned_units.push_back(unit->id);
INFO(cycle,out).print("Unit %d (%s) assigned to nestbox zone %d (%s)\n",
unit->id, Units::getRaceName(unit).c_str(),
building->id, building->name.c_str());
return true;
}
static size_t countFreeEgglayers() {
size_t count = 0;
for (auto unit : world->units.all) {
if (isFreeEgglayer(unit))
++count;
}
return count;
}
static size_t assign_nestboxes(color_ostream &out) {
size_t processed = 0;
df::building *free_building = NULL;
df::unit *free_unit = NULL;
do {
free_building = findFreeNestboxZone();
free_unit = findFreeEgglayer();
if (free_building && free_unit) {
if (!assignUnitToZone(out, free_unit, free_building)) {
DEBUG(cycle,out).print("Failed to assign unit to building.\n");
return processed;
}
DEBUG(cycle,out).print("assigned unit %d to zone %d\n",
free_unit->id, free_building->id);
++processed;
}
} while (free_unit && free_building);
if (free_unit && !free_building) {
static size_t old_count = 0;
size_t freeEgglayers = countFreeEgglayers();
// avoid spamming the same message
if (old_count != freeEgglayers)
did_complain = false;
old_count = freeEgglayers;
if (!did_complain) {
std::stringstream ss;
ss << freeEgglayers;
string announce = "Not enough free nestbox zones found! You need " + ss.str() + " more.";
Gui::showAnnouncement(announce, 6, true);
out << announce << std::endl;
did_complain = true;
}
}
return processed;
}
static void autonestbox_cycle(color_ostream &out) {
// mark that we have recently run
cycle_timestamp = world->frame_counter;
DEBUG(cycle,out).print("running autonestbox cycle\n");
size_t processed = assign_nestboxes(out);
if (processed > 0) {
std::stringstream ss;
ss << processed << " nestboxes were assigned.";
string announce = ss.str();
DEBUG(cycle,out).print("%s\n", announce.c_str());
Gui::showAnnouncement(announce, 2, false);
out << announce << std::endl;
// can complain again
// (might lead to spamming the same message twice, but catches the case
// where for example 2 new egglayers hatched right after 2 zones were created and assigned)
did_complain = false;
}
}
You can’t perform that action at this time.
