forked from DFHack/dfhack
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathforceequip.cpp
More file actions
477 lines (430 loc) · 20.2 KB
/
forceequip.cpp
File metadata and controls
477 lines (430 loc) · 20.2 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
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
// forceequip plugin
// Moves local items from the ground into a unit's inventory
#include <iostream>
#include <iomanip>
#include <sstream>
#include <climits>
#include <vector>
#include <string>
#include <algorithm>
#include <set>
#include "Debug.h"
#include "Console.h"
#include "Export.h"
#include "PluginManager.h"
#include "modules/Maps.h"
#include "modules/Gui.h"
#include "modules/Items.h"
#include "modules/Materials.h"
#include "DataDefs.h"
#include "df/item.h"
#include "df/itemdef.h"
#include "df/world.h"
#include "df/general_ref.h"
#include "df/unit.h"
#include "df/body_part_raw.h"
#include "MiscUtils.h"
#include "df/unit_inventory_item.h"
#include "df/body_part_raw_flags.h"
#include "df/creature_raw.h"
#include "df/caste_raw.h"
#include "df/body_detail_plan.h"
#include "df/body_template.h"
#include "df/body_part_template.h"
#include "df/unit_soul.h"
#include "df/unit_skill.h"
using namespace DFHack;
using namespace df::enums;
using std::string;
using std::vector;
using std::endl;
DFHACK_PLUGIN("forceequip");
REQUIRE_GLOBAL(world);
namespace DFHack {
DBG_DECLARE(forceequip, log, DebugCategory::LINFO);
}
const int const_GloveRightHandedness = 1;
const int const_GloveLeftHandedness = 2;
command_result df_forceequip(color_ostream &out, vector <string> & parameters);
DFhackCExport command_result plugin_init ( color_ostream &out, vector <PluginCommand> &commands)
{
commands.push_back(PluginCommand(
"forceequip",
"Move items into a unit's inventory.",
df_forceequip));
return CR_OK;
}
DFhackCExport command_result plugin_shutdown ( color_ostream &out )
{
return CR_OK;
}
static bool moveToInventory(df::item *item, df::unit *unit, df::body_part_raw * targetBodyPart, bool ignoreRestrictions, int multiEquipLimit, bool verbose)
{
// Step 1: Check for anti-requisite conditions
df::unit * itemOwner = Items::getOwner(item);
if (ignoreRestrictions)
{
// If the ignoreRestrictions cmdline switch was specified, then skip all of the normal preventative rules
if (verbose) { DEBUG(log).print("Skipping integrity checks...\n"); }
}
else if(!item->isClothing() && !item->isArmorNotClothing())
{
if (verbose) { WARN(log).print("Item %d is not clothing or armor; it cannot be equipped. Please choose a different item (or use the Ignore option if you really want to equip an inappropriate item).\n", item->id); }
return false;
}
else if (item->getType() != df::enums::item_type::GLOVES &&
item->getType() != df::enums::item_type::HELM &&
item->getType() != df::enums::item_type::ARMOR &&
item->getType() != df::enums::item_type::PANTS &&
item->getType() != df::enums::item_type::SHOES &&
!targetBodyPart)
{
if (verbose) { WARN(log).print("Item %d is of an unrecognized type; it cannot be equipped (because the module wouldn't know where to put it).\n", item->id); }
return false;
}
else if (itemOwner && itemOwner->id != unit->id)
{
if (verbose) { WARN(log).print("Item %d is owned by someone else. Equipping it on this unit is not recommended. Please use DFHack's Confiscate plugin, choose a different item, or use the Ignore option to proceed in spite of this warning.\n", item->id); }
return false;
}
else if (item->flags.bits.in_inventory)
{
if (verbose) { WARN(log).print("Item %d is already in a unit's inventory. Direct inventory transfers are not recommended; please move the item to the ground first (or use the Ignore option).\n", item->id); }
return false;
}
else if (item->flags.bits.in_job)
{
if (verbose) { WARN(log).print("Item %d is reserved for use in a queued job. Equipping it is not recommended, as this might interfere with the completion of vital jobs. Use the Ignore option to ignore this warning.\n", item->id); }
return false;
}
// ASSERT: anti-requisite conditions have been satisfied (or disregarded)
// Step 2: Try to find a bodypart which is eligible to receive equipment AND which is appropriate for the specified item
df::body_part_raw * confirmedBodyPart = NULL;
size_t bpIndex;
for(bpIndex = 0; bpIndex < unit->body.body_plan->body_parts.size(); bpIndex++)
{
df::body_part_raw * currPart = unit->body.body_plan->body_parts[bpIndex];
// Short-circuit the search process if a BP was specified in the function call
// Note: this causes a bit of inefficient busy-looping, but the search space is tiny (<100) and we NEED to get the correct bpIndex value in order to perform inventory manipulations
if (!targetBodyPart)
{
// The function call did not specify any particular body part; proceed with normal iteration and evaluation of BP eligibility
}
else if (currPart == targetBodyPart)
{
// A specific body part was included in the function call, and we've found it; proceed with the normal BP evaluation (suitability, emptiness, etc)
}
else if (bpIndex < unit->body.body_plan->body_parts.size())
{
// The current body part is not the one that was specified in the function call, but we can keep searching
if (verbose) { WARN(log).print("Found bodypart %s; not a match; continuing search.\n", currPart->token.c_str()); }
continue;
}
else
{
// The specified body part has not been found, and we've reached the end of the list. Report failure.
if (verbose) { WARN(log).print("The specified body part (%s) does not belong to the chosen unit. Please double-check to ensure that your spelling is correct, and that you have not chosen a dismembered bodypart.\n",targetBodyPart->token.c_str()); }
return false;
}
if (verbose) { DEBUG(log).print("Inspecting bodypart %s.\n", currPart->token.c_str()); }
// Inspect the current bodypart
if (item->getType() == df::enums::item_type::GLOVES && currPart->flags.is_set(df::body_part_raw_flags::GRASP) &&
((item->getGloveHandedness() == const_GloveLeftHandedness && currPart->flags.is_set(df::body_part_raw_flags::LEFT)) ||
(item->getGloveHandedness() == const_GloveRightHandedness && currPart->flags.is_set(df::body_part_raw_flags::RIGHT))))
{
if (verbose) { DEBUG(log).print("Hand found (%s)...", currPart->token.c_str()); }
}
else if (item->getType() == df::enums::item_type::HELM && currPart->flags.is_set(df::body_part_raw_flags::HEAD))
{
if (verbose) { DEBUG(log).print("Head found (%s)...", currPart->token.c_str()); }
}
else if (item->getType() == df::enums::item_type::ARMOR && currPart->flags.is_set(df::body_part_raw_flags::UPPERBODY))
{
if (verbose) { DEBUG(log).print("Upper body found (%s)...", currPart->token.c_str()); }
}
else if (item->getType() == df::enums::item_type::PANTS && currPart->flags.is_set(df::body_part_raw_flags::LOWERBODY))
{
if (verbose) { DEBUG(log).print("Lower body found (%s)...", currPart->token.c_str()); }
}
else if (item->getType() == df::enums::item_type::SHOES && currPart->flags.is_set(df::body_part_raw_flags::STANCE))
{
if (verbose) { DEBUG(log).print("Foot found (%s)...", currPart->token.c_str()); }
}
else if (targetBodyPart && ignoreRestrictions)
{
// The BP in question would normally be considered ineligible for equipment. But since it was deliberately specified by the user, we'll proceed anyways.
if (verbose) { DEBUG(log).print("Non-standard bodypart found (%s)...", targetBodyPart->token.c_str()); }
}
else if (targetBodyPart)
{
// The BP in question is not eligible for equipment and the ignore flag was not specified. Report failure.
if (verbose) { WARN(log).print("Non-standard bodypart found, but it is ineligible for standard equipment. Use the Ignore flag to override this warning.\n"); }
return false;
}
else
{
if (verbose) { DEBUG(log).print("Skipping ineligible bodypart.\n"); }
// This body part is not eligible for the equipment in question; skip it
continue;
}
// ASSERT: The current body part is able to support the specified equipment (or the test has been overridden). Check whether it is currently empty/available.
if (multiEquipLimit == INT_MAX)
{
// Note: this loop/check is skipped if the MultiEquip option is specified; we'll simply add the item to the bodyPart even if it's already holding a dozen gloves, shoes, and millstones (or whatever)
if (verbose) { DEBUG(log).print(" inventory checking skipped..."); }
confirmedBodyPart = currPart;
break;
}
else
{
confirmedBodyPart = currPart; // Assume that the bodypart is valid; we'll invalidate it if we detect too many collisions while looping
int collisions = 0;
for (df::unit_inventory_item * currInvItem : unit->inventory)
{
if (currInvItem->body_part_id == int32_t(bpIndex))
{
// Collision detected; have we reached the limit?
if (++collisions >= multiEquipLimit)
{
if (verbose) { WARN(log).print(" but it already carries %d piece(s) of equipment. Either remove the existing equipment or use the Multi option.\n", multiEquipLimit); }
confirmedBodyPart = NULL;
break;
}
}
}
if (confirmedBodyPart)
{
// Match found; no need to examine any other BPs
if (verbose) { DEBUG(log).print(" eligibility confirmed..."); }
break;
}
else if (!targetBodyPart)
{
// This body part is not eligible to receive the specified equipment; return to the loop and check the next BP
continue;
}
else
{
// A specific body part was designated in the function call, but it was found to be ineligible.
// Don't return to the BP loop; just fall-through to the failure-reporting code a few lines below.
break;
}
}
}
if (!confirmedBodyPart) {
// No matching body parts found; report failure
if (verbose) { WARN(log).print("\nThe item could not be equipped because the relevant body part(s) of the unit are missing or already occupied. Try again with the Multi option if you're like to over-equip a body part, or choose a different unit-item combination (e.g. stop trying to put shoes on a trout).\n" ); }
return false;
}
if (!Items::moveToInventory(item, unit, df::inv_item_role_type::Worn, bpIndex))
{
if (verbose) { WARN(log).print("\nEquipping failed - failed to retrieve item from its current location/container/inventory. Please move it to the ground and try again.\n"); }
return false;
}
if (verbose) { DEBUG(log).print(" Success!\n"); }
return true;
}
command_result df_forceequip(color_ostream &out, vector <string> & parameters)
{
// The "here" option is hardcoded to true, because the plugin currently doesn't support
// equip-at-a-distance (e.g. grab items within 10 squares of the targeted unit)
bool here = true;
(void)here;
// For balance (anti-cheating) reasons, the plugin applies a limit on the number of
// item that can be equipped on any bodypart. This limit defaults to 1 but can be
// overridden with cmdline switches.
int multiEquipLimit = 1;
// The plugin applies several pre-checks in order to reduce the risk of conflict
// and unintended side-effects. Most of these checks can be disabled via cmdline
bool ignore = false;
// By default, the plugin uses all gear piled on the selected square. Optionally,
// it can target only a single item (selected on the k menu) instead
bool selected = false;
// Most of the plugin's text output is suppressed by default. It can be enabled
// to provide insight into errors, and/or for debugging purposes.
bool verbose = false;
// By default, the plugin will mate each item to an appropriate bodypart. This
// behaviour can be skipped if the user specifies a particular BP in the cmdline input.
std::string targetBodyPartCode;
// Parse the input
for (size_t i = 0; i < parameters.size(); i++)
{
string & p = parameters[i];
if (p == "help" || p == "?" || p == "h" || p == "/?" || p == "info" || p == "man")
{
return CR_WRONG_USAGE;
}
else if (p == "here" || p == "h")
{
here = true;
}
else if (p == "ignore" || p == "i")
{
ignore = true;
}
else if (p == "multi" || p == "m")
{
multiEquipLimit = INT_MAX;
}
else if (p == "m2")
{
multiEquipLimit = 2;
}
else if (p == "m3")
{
multiEquipLimit = 3;
}
else if (p == "m4")
{
multiEquipLimit = 4;
}
else if (p == "selected" || p == "s")
{
selected = true;
}
else if (p == "verbose" || p == "v")
{
verbose = true;
}
else if (p == "bodypart" || p == "bp" )
{
// must be followed by bodypart code (e.g. NECK)
if(i == parameters.size()-1 || parameters[i+1].size() == 0)
{
WARN(log).print("The bp switch must be followed by a bodypart code!\n");
return CR_FAILURE;
}
targetBodyPartCode = parameters[i+1];
i++;
}
else
{
out << p << ": Unknown command! Type \"forceequip help\" for assistance." << endl;
return CR_FAILURE;
}
}
// Ensure that the map information is available (e.g. a game is actually in-progress)
if (!Maps::IsValid())
{
WARN(log).print("Map is not available!\n");
return CR_FAILURE;
}
// Lookup the cursor position
int cx, cy, cz;
DFCoord pos_cursor;
// needs a cursor
if (!Gui::getCursorCoords(cx,cy,cz))
{
WARN(log).print("Cursor position not found. Please enable the cursor.\n");
return CR_FAILURE;
}
pos_cursor = DFCoord(cx,cy,cz);
// Iterate over all units, process the first one whose pos == pos_cursor
df::unit * targetUnit = nullptr;
size_t numUnits = world->units.all.size();
for(size_t i=0; i< numUnits; i++)
{
targetUnit = world->units.all[i]; // tentatively assume that we have a match; then verify
DFCoord pos_unit(targetUnit->pos.x, targetUnit->pos.y, targetUnit->pos.z);
if (pos_unit == pos_cursor)
break;
targetUnit = nullptr;
}
if (!targetUnit)
{
WARN(log).print("No unit found at cursor!\n");
return CR_FAILURE;
}
// Assert: unit found.
// If a specific bodypart was included in the command arguments, then search for it now
df::body_part_raw * targetBodyPart = NULL;
if (targetBodyPartCode.size() > 0) {
for (size_t bpIndex = 0; bpIndex < targetUnit->body.body_plan->body_parts.size(); bpIndex ++)
{
// Tentatively assume that the part is a match
targetBodyPart = targetUnit->body.body_plan->body_parts.at(bpIndex);
if (targetBodyPart->token.compare(targetBodyPartCode) == 0)
{
// It is indeed a match; exit the loop (while leaving the variable populated)
if (verbose) { INFO(log).print("Matching bodypart (%s) found.\n", targetBodyPart->token.c_str()); }
break;
}
else
{
// Not a match; nullify the variable (it will get re-populated on the next pass through the loop)
if (verbose) { WARN(log).print("Bodypart \"%s\" does not match \"%s\".\n", targetBodyPart->token.c_str(), targetBodyPartCode.c_str()); }
targetBodyPart = NULL;
}
}
if (!targetBodyPart)
{
// Loop iteration is complete but no match was found.
WARN(log).print("The unit does not possess a bodypart of type \"%s\". Please check the spelling or choose a different unit.\n", targetBodyPartCode.c_str());
return CR_FAILURE;
}
}
// Search for item(s)
// iterate over all items, process those where pos == pos_cursor
int itemsEquipped = 0;
int itemsFound = 0;
int numItems = world->items.all.size(); // Normally, we iterate through EVERY ITEM in the world. This is expensive, but currently necessary.
if (selected) { numItems = 1; } // If the user wants to process only the selected item, then the loop is trivialized (only one pass is needed).
for(int i=0; i< numItems; i++)
{
df::item * currentItem;
// Search behaviour depends on whether the operation is driven by cursor location or UI selection
if (selected)
{
// The "search" is trivial - the selection must always cover either one or zero items
currentItem = Gui::getSelectedItem(out);
if (!currentItem) { return CR_FAILURE; }
}
else
{
// Lookup the current item in the world-space
currentItem = world->items.all[i];
// Test the item's position
DFCoord pos_item(currentItem->pos.x, currentItem->pos.y, currentItem->pos.z);
if (pos_item != pos_cursor)
{
// The item is in the wrong place; skip it
// Note: we do not emit any notification, even with the "verbose" switch, because the search space is enormous and we'd invariably flood the UI with useless text
continue;
}
// Bypass any forbidden items
else if (currentItem->flags.bits.forbid == 1)
{
// The item is forbidden; skip it
if (verbose) { WARN(log).print("Forbidden item encountered; skipping to next item.\n"); }
}
}
// Test the item; check whether we have any grounds to disqualify/reject it
if (currentItem->flags.bits.in_inventory == 1)
{
// The item is in a unit's inventory; skip it
if (verbose) { WARN(log).print("Inventory item encountered; skipping to next item.\n"); }
}
else
{
itemsFound ++; // Track the number of items found under the cursor (for feedback purposes)
if (moveToInventory(currentItem, targetUnit, targetBodyPart, ignore, multiEquipLimit, verbose))
{
// // TODO TEMP EXPERIMENTAL - try to alter the item size in order to conform to its wearer
// currentItem->getRace();
// INFO(log).print("Critter size: %d| %d | Armor size: %d", world->raws.creatures.all[targetUnit->race]->caste[targetUnit->caste]->body_size_1, world->raws.creatures.all[targetUnit->race]->caste[targetUnit->caste]->body_size_2, currentItem->getTotalDimension());
itemsEquipped++; // Track the number of items successfully processed (for feedback purposes)
}
}
}
if (itemsFound == 0) {
WARN(log).print("No usable items found at the cursor position. Please choose a different location and try again.\n");
return CR_OK;
}
if (itemsEquipped == 0 && !verbose) { WARN(log).print("Some items were found but no equipment changes could be made. Use the /verbose switch to display the reasons for failure.\n"); }
if (itemsEquipped > 0) { INFO(log).print("%d items equipped.\n", itemsEquipped); }
// Note: we might expect to recalculate the unit's weight at this point, in order to account for the
// added items. In fact, this recalculation occurs automatically during each dwarf's "turn".
// The slight delay in recalculation is probably not worth worrying about.
// Work complete; report success
return CR_OK;
}