diff --git a/LUA_API.rst b/LUA_API.rst index 18433262d..b0f1a9f31 100644 --- a/LUA_API.rst +++ b/LUA_API.rst @@ -763,6 +763,11 @@ Items module Returns true *x,y,z* of the item, or *nil* if invalid; may be not equal to item.pos if in inventory. +* ``dfhack.items.getDescription(item, type[, decorate])`` + + Returns the string description of the item, as produced by the getItemDescription + method. If decorate is true, also adds markings for quality and improvements. + * ``dfhack.items.getGeneralRef(item, type)`` Searches for a general_ref with the given type. @@ -800,6 +805,11 @@ Items module Move the item to the building. Returns *false* if impossible. +* ``dfhack.items.moveToInventory(item,unit,use_mode,body_part)`` + + Move the item to the unit inventory. Returns *false* if impossible. + + Maps module ----------- diff --git a/Lua API.html b/Lua API.html index e2d4ec23e..4b763af25 100644 --- a/Lua API.html +++ b/Lua API.html @@ -991,6 +991,10 @@ or raws. The ignore_noble boolean disables the
  • dfhack.items.getPosition(item)

    Returns true x,y,z of the item, or nil if invalid; may be not equal to item.pos if in inventory.

  • +
  • dfhack.items.getDescription(item, type[, decorate])

    +

    Returns the string description of the item, as produced by the getItemDescription +method. If decorate is true, also adds markings for quality and improvements.

    +
  • dfhack.items.getGeneralRef(item, type)

    Searches for a general_ref with the given type.

  • @@ -1016,6 +1020,12 @@ Returns false in case of error.

  • dfhack.items.moveToContainer(item,container)

    Move the item to the container. Returns false if impossible.

  • +
  • dfhack.items.moveToBuilding(item,building,use_mode)

    +

    Move the item to the building. Returns false if impossible.

    +
  • +
  • dfhack.items.moveToInventory(item,unit,use_mode,body_part)

    +

    Move the item to the unit inventory. Returns false if impossible.

    +
  • diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 9dfb39754..6a550db87 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -764,15 +764,24 @@ static bool items_moveToBuilding(df::item *item, df::building_actual *building, return Items::moveToBuilding(mc, item, building,use_mode); } +static bool items_moveToInventory + (df::item *item, df::unit *unit, df::unit_inventory_item::T_mode mode, int body_part) +{ + MapExtras::MapCache mc; + return Items::moveToInventory(mc, item, unit, mode, body_part); +} + static const LuaWrapper::FunctionReg dfhack_items_module[] = { WRAPM(Items, getGeneralRef), WRAPM(Items, getSpecificRef), WRAPM(Items, getOwner), WRAPM(Items, setOwner), WRAPM(Items, getContainer), + WRAPM(Items, getDescription), WRAPN(moveToGround, items_moveToGround), WRAPN(moveToContainer, items_moveToContainer), WRAPN(moveToBuilding, items_moveToBuilding), + WRAPN(moveToInventory, items_moveToInventory), { NULL, NULL } }; diff --git a/library/include/modules/Items.h b/library/include/modules/Items.h index ab735f731..4236f068a 100644 --- a/library/include/modules/Items.h +++ b/library/include/modules/Items.h @@ -39,6 +39,7 @@ distribution. #include "df/specific_ref.h" #include "df/building_actual.h" #include "df/body_part_raw.h" +#include "df/unit_inventory_item.h" namespace df { @@ -146,9 +147,13 @@ DFHACK_EXPORT void getContainedItems(df::item *item, /*output*/ std::vectorpos; } +static char quality_table[] = { 0, '-', '+', '*', '=', '@' }; + +static void addQuality(std::string &tmp, int quality) +{ + if (quality > 0 && quality <= 5) { + char c = quality_table[quality]; + tmp = c + tmp + c; + } +} + +std::string Items::getDescription(df::item *item, int type, bool decorate) +{ + CHECK_NULL_POINTER(item); + + std::string tmp; + item->getItemDescription(&tmp, type); + + if (decorate) { + if (item->flags.bits.foreign) + tmp = "(" + tmp + ")"; + + addQuality(tmp, item->getQuality()); + + if (item->isImproved()) { + tmp = "<" + tmp + ">"; + addQuality(tmp, item->getImprovementQuality()); + } + } + + return tmp; +} + +static void resetUnitInvFlags(df::unit *unit, df::unit_inventory_item *inv_item) +{ + if (inv_item->mode == df::unit_inventory_item::Worn || + inv_item->mode == df::unit_inventory_item::WrappedAround) + { + unit->flags2.bits.calculated_inventory = false; + unit->flags2.bits.calculated_insulation = false; + } + else if (inv_item->mode == df::unit_inventory_item::StuckIn) + { + unit->flags3.bits.unk2 = false; + } +} + static bool detachItem(MapExtras::MapCache &mc, df::item *item) { + if (!item->specific_refs.empty()) + return false; + if (item->world_data_id != -1) + return false; + + for (size_t i = 0; i < item->itemrefs.size(); i++) + { + df::general_ref *ref = item->itemrefs[i]; + + switch (ref->getType()) + { + case general_ref_type::PROJECTILE: + case general_ref_type::BUILDING_HOLDER: + case general_ref_type::BUILDING_CAGED: + case general_ref_type::BUILDING_TRIGGER: + case general_ref_type::BUILDING_TRIGGERTARGET: + case general_ref_type::BUILDING_CIVZONE_ASSIGNED: + return false; + + default: + continue; + } + } + if (item->flags.bits.on_ground) { if (!mc.removeItemOnGround(item)) @@ -603,6 +674,14 @@ static bool detachItem(MapExtras::MapCache &mc, df::item *item) case general_ref_type::CONTAINED_IN_ITEM: if (auto item2 = ref->getItem()) { + // Viewscreens hold general_ref_contains_itemst pointers + for (auto screen = Core::getTopViewscreen(); screen; screen = screen->parent) + { + auto vsitem = strict_virtual_cast(screen); + if (vsitem && vsitem->item == item2) + return false; + } + item2->flags.bits.weight_computed = false; removeRef(item2->itemrefs, general_ref_type::CONTAINS_ITEM, item->id); @@ -610,27 +689,27 @@ static bool detachItem(MapExtras::MapCache &mc, df::item *item) break; case general_ref_type::UNIT_HOLDER: - // Remove the item from the unit's inventory - for (int inv = 0; inv < ref->getUnit()->inventory.size(); inv++) + if (auto unit = ref->getUnit()) { - df::unit_inventory_item * currInvItem = ref->getUnit()->inventory.at(inv); - if(currInvItem->item->id == item->id) + // Unit view sidebar holds inventory item pointers + if (ui->main.mode == ui_sidebar_mode::ViewUnits && + (!ui_selected_unit || + vector_get(world->units.active, *ui_selected_unit) == unit)) + return false; + + for (int i = unit->inventory.size()-1; i >= 0; i--) { - // Match found; remove it - ref->getUnit()->inventory.erase(ref->getUnit()->inventory.begin() + inv); - // No other pointers to this object should exist; delete it to prevent memleak - delete currInvItem; - // Note: we might expect to recalculate the unit's weight at this point, in order to account for the - // detached item. In fact, this recalculation occurs automatically during each dwarf's "turn". - // The slight delay in recalculation is probably not worth worrying about. - - // Since no item will ever occur twice in a unit's inventory, further searching is unnecessary. - break; + df::unit_inventory_item *inv_item = unit->inventory[i]; + if (inv_item->item != item) + continue; + + resetUnitInvFlags(unit, inv_item); + + vector_erase_at(unit->inventory, i); + delete inv_item; } } break; - case general_ref_type::BUILDING_HOLDER: - return false; default: continue; @@ -681,9 +760,6 @@ bool DFHack::Items::moveToContainer(MapExtras::MapCache &mc, df::item *item, df: if (!cpos.isValid()) return false; - if (!detachItem(mc, item)) - return false; - auto ref1 = df::allocate(); auto ref2 = df::allocate(); @@ -691,18 +767,24 @@ bool DFHack::Items::moveToContainer(MapExtras::MapCache &mc, df::item *item, df: { delete ref1; delete ref2; Core::printerr("Could not allocate container refs.\n"); - putOnGround(mc, item, cpos); + return false; + } + + if (!detachItem(mc, item)) + { + delete ref1; delete ref2; return false; } item->pos = container->pos; item->flags.bits.in_inventory = true; - container->flags.bits.container = true; + container->flags.bits.container = true; container->flags.bits.weight_computed = false; ref1->item_id = item->id; container->itemrefs.push_back(ref1); + ref2->item_id = container->id; item->itemrefs.push_back(ref2); @@ -713,6 +795,7 @@ bool DFHack::Items::moveToBuilding(MapExtras::MapCache &mc, df::item *item, df:: { CHECK_NULL_POINTER(item); CHECK_NULL_POINTER(building); + CHECK_INVALID_ARGUMENT(use_mode == 0 || use_mode == 2); auto ref = df::allocate(); if(!ref) @@ -721,11 +804,13 @@ bool DFHack::Items::moveToBuilding(MapExtras::MapCache &mc, df::item *item, df:: Core::printerr("Could not allocate building holder refs.\n"); return false; } + if (!detachItem(mc, item)) { delete ref; return false; } + item->pos.x=building->centerx; item->pos.y=building->centery; item->pos.z=building->z; @@ -738,217 +823,46 @@ bool DFHack::Items::moveToBuilding(MapExtras::MapCache &mc, df::item *item, df:: con->item=item; con->use_mode=use_mode; building->contained_items.push_back(con); + return true; } -bool DFHack::Items::moveToInventory(MapExtras::MapCache &mc, 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) { Core::print("Skipping integrity checks...\n"); } - } - else if(!item->isClothing() && !item->isArmorNotClothing()) - { - if (verbose) { Core::printerr("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) { Core::printerr("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) { Core::printerr("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) { Core::printerr("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) +bool DFHack::Items::moveToInventory( + MapExtras::MapCache &mc, df::item *item, df::unit *unit, + df::unit_inventory_item::T_mode mode, int body_part +) { + CHECK_NULL_POINTER(item); + CHECK_NULL_POINTER(unit); + CHECK_NULL_POINTER(unit->body.body_plan); + CHECK_INVALID_ARGUMENT(is_valid_enum_item(mode)); + int body_plan_size = unit->body.body_plan->body_parts.size(); + CHECK_INVALID_ARGUMENT(body_part < 0 || body_part <= body_plan_size); + + auto holderReference = df::allocate(); + if (!holderReference) { - if (verbose) { Core::printerr("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); } + Core::printerr("Could not allocate UNIT_HOLDER reference.\n"); 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; - int bpIndex; - for(bpIndex = 0; bpIndex < unit->body.body_plan->body_parts.size(); bpIndex++) + if (!detachItem(mc, item)) { - 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) { Core::printerr("Found bodypart %s; not a match; continuing search.\n", currPart->part_code.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) { Core::printerr("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"); } - return false; - } - - if (verbose) { Core::print("Inspecting bodypart %s.\n", currPart->part_code.c_str()); } - - // Inspect the current bodypart - if (item->getType() == df::enums::item_type::GLOVES && currPart->flags.is_set(df::body_part_template_flags::GRASP) && - ((item->getGloveHandedness() == const_GloveLeftHandedness && currPart->flags.is_set(df::body_part_template_flags::LEFT)) || - (item->getGloveHandedness() == const_GloveRightHandedness && currPart->flags.is_set(df::body_part_template_flags::RIGHT)))) - { - if (verbose) { Core::print("Hand found (%s)...", currPart->part_code.c_str()); } - } - else if (item->getType() == df::enums::item_type::HELM && currPart->flags.is_set(df::body_part_template_flags::HEAD)) - { - if (verbose) { Core::print("Head found (%s)...", currPart->part_code.c_str()); } - } - else if (item->getType() == df::enums::item_type::ARMOR && currPart->flags.is_set(df::body_part_template_flags::UPPERBODY)) - { - if (verbose) { Core::print("Upper body found (%s)...", currPart->part_code.c_str()); } - } - else if (item->getType() == df::enums::item_type::PANTS && currPart->flags.is_set(df::body_part_template_flags::LOWERBODY)) - { - if (verbose) { Core::print("Lower body found (%s)...", currPart->part_code.c_str()); } - } - else if (item->getType() == df::enums::item_type::SHOES && currPart->flags.is_set(df::body_part_template_flags::STANCE)) - { - if (verbose) { Core::print("Foot found (%s)...", currPart->part_code.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) { Core::print("Non-standard bodypart found (%s)...", targetBodyPart->part_code.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) { Core::printerr("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) { Core::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) { Core::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 (int inventoryID=0; inventoryID < unit->inventory.size(); inventoryID++) - { - df::unit_inventory_item * currInvItem = unit->inventory[inventoryID]; - if (currInvItem->body_part_id == bpIndex) - { - // Collision detected; have we reached the limit? - if (++collisions >= multiEquipLimit) - { - if (verbose) { Core::printerr(" 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) { Core::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) { Core::printerr("\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" ); } + delete holderReference; return false; } - // ASSERT: We've found a bodypart which is suitable for the designated item and ready to receive it (or overridden the relevant checks) + item->flags.bits.in_inventory = true; - // Step 3: Perform the manipulations - if (verbose) { Core::print("equipping item..."); } - // 3a: attempt to release the item from its current position - if (!detachItem(mc, item)) - { - if (verbose) { Core::printerr("\nEquipping failed - failed to retrieve item from its current location/container/inventory. Please move it to the ground and try again.\n"); } - return false; - } - // 3b: register the item in the unit's inventory - df::unit_inventory_item * newInventoryItem = df::allocate(); - newInventoryItem->body_part_id = bpIndex; + auto newInventoryItem = new df::unit_inventory_item(); newInventoryItem->item = item; - newInventoryItem->mode = newInventoryItem->Worn; + newInventoryItem->mode = mode; + newInventoryItem->body_part_id = body_part; unit->inventory.push_back(newInventoryItem); - item->flags.bits.in_inventory = true; - // 3c: register a "unit holds item" relationship at the item level - df::general_ref_unit_holderst * holderReference = df::allocate(); - holderReference->setID(unit->id); + holderReference->unit_id = unit->id; item->itemrefs.push_back(holderReference); - // 3d: tell the unit to begin "using" the item (note: if this step is skipped then the unit may not gain any actual protection from its armour) - if (item->isClothing() || item->isArmorNotClothing()) { - df::unit::T_used_items * newUsedItem = df::allocate(); - newUsedItem->id = item->id; - unit->used_items.push_back(newUsedItem); - if (verbose) { Core::print("Item is clothing/armor; protection aspects have been enabled.\n"); } - } - else - { - if (verbose) { Core::print("Item is neither clothing nor armor; unit has NOT been instructed to \"use\" it as such.\n"); } - } - - // 3e: Remove the item from its current location (note: if this step is skipped then the item will appear BOTH on the ground and in the unit's inventory) - mc.removeItemOnGround(item); + resetUnitInvFlags(unit, newInventoryItem); - if (verbose) { Core::print(" Success!\n"); } return true; } diff --git a/plugins/autodump.cpp b/plugins/autodump.cpp index 753a8fb90..536b2501b 100644 --- a/plugins/autodump.cpp +++ b/plugins/autodump.cpp @@ -148,8 +148,6 @@ static command_result autodump_main(color_ostream &out, vector & parame } } - bool inventoryDumpingSkipped = false; - // proceed with the dumpification operation for(size_t i=0; i< numItems; i++) { @@ -175,14 +173,6 @@ static command_result autodump_main(color_ostream &out, vector & parame continue; if (!need_forbidden && itm->flags.bits.forbid) continue; - if (itm->flags.bits.in_inventory && Gui::getSelectedUnit(out, true)) - { - // Due to GUI caching/redraw rules, Dwarf Fortress tends to crash if we make any changes to a unit's inventory - // while the player is looking at the inventory menu. Therefore, we'll simply skip such items until they - // change something (e.g. switch from "v" to "k" mode). - inventoryDumpingSkipped = true; - continue; - } if(!destroy) // move to cursor { @@ -192,7 +182,11 @@ static command_result autodump_main(color_ostream &out, vector & parame // Don't move items if they're already at the cursor if (pos_cursor != pos_item) - Items::moveToGround(MC, itm, pos_cursor); + { + if (!Items::moveToGround(MC, itm, pos_cursor)) + out.print("Could not move item: %s\n", + Items::getDescription(itm, 0, true).c_str()); + } } else // destroy { @@ -213,7 +207,6 @@ static command_result autodump_main(color_ostream &out, vector & parame if(!destroy) MC.WriteAll(); - if (inventoryDumpingSkipped) { out.printerr("Some inventory items could not be autodumped because the unit/inventory screen is currently active. Please close the unit screen and repeat the operation.\n"); } out.print("Done. %d items %s.\n", dumped_total, destroy ? "marked for destruction" : "quickdumped"); return CR_OK; } diff --git a/plugins/forceequip.cpp b/plugins/forceequip.cpp index dd4e213a1..0d493143b 100644 --- a/plugins/forceequip.cpp +++ b/plugins/forceequip.cpp @@ -48,6 +48,9 @@ using MapExtras::Block; using MapExtras::MapCache; using df::global::world; +const int const_GloveRightHandedness = 1; +const int const_GloveLeftHandedness = 2; + DFHACK_PLUGIN("forceequip"); command_result df_forceequip(color_ostream &out, vector & parameters); @@ -226,6 +229,187 @@ DFhackCExport command_result plugin_shutdown ( color_ostream &out ) return CR_OK; } +static bool moveToInventory(MapExtras::MapCache &mc, 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) { Core::print("Skipping integrity checks...\n"); } + } + else if(!item->isClothing() && !item->isArmorNotClothing()) + { + if (verbose) { Core::printerr("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) { Core::printerr("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) { Core::printerr("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) { Core::printerr("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) { Core::printerr("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; + int 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) { Core::printerr("Found bodypart %s; not a match; continuing search.\n", currPart->part_code.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) { Core::printerr("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"); } + return false; + } + + if (verbose) { Core::print("Inspecting bodypart %s.\n", currPart->part_code.c_str()); } + + // Inspect the current bodypart + if (item->getType() == df::enums::item_type::GLOVES && currPart->flags.is_set(df::body_part_template_flags::GRASP) && + ((item->getGloveHandedness() == const_GloveLeftHandedness && currPart->flags.is_set(df::body_part_template_flags::LEFT)) || + (item->getGloveHandedness() == const_GloveRightHandedness && currPart->flags.is_set(df::body_part_template_flags::RIGHT)))) + { + if (verbose) { Core::print("Hand found (%s)...", currPart->part_code.c_str()); } + } + else if (item->getType() == df::enums::item_type::HELM && currPart->flags.is_set(df::body_part_template_flags::HEAD)) + { + if (verbose) { Core::print("Head found (%s)...", currPart->part_code.c_str()); } + } + else if (item->getType() == df::enums::item_type::ARMOR && currPart->flags.is_set(df::body_part_template_flags::UPPERBODY)) + { + if (verbose) { Core::print("Upper body found (%s)...", currPart->part_code.c_str()); } + } + else if (item->getType() == df::enums::item_type::PANTS && currPart->flags.is_set(df::body_part_template_flags::LOWERBODY)) + { + if (verbose) { Core::print("Lower body found (%s)...", currPart->part_code.c_str()); } + } + else if (item->getType() == df::enums::item_type::SHOES && currPart->flags.is_set(df::body_part_template_flags::STANCE)) + { + if (verbose) { Core::print("Foot found (%s)...", currPart->part_code.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) { Core::print("Non-standard bodypart found (%s)...", targetBodyPart->part_code.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) { Core::printerr("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) { Core::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) { Core::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 (int inventoryID=0; inventoryID < unit->inventory.size(); inventoryID++) + { + df::unit_inventory_item * currInvItem = unit->inventory[inventoryID]; + if (currInvItem->body_part_id == bpIndex) + { + // Collision detected; have we reached the limit? + if (++collisions >= multiEquipLimit) + { + if (verbose) { Core::printerr(" 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) { Core::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) { Core::printerr("\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(mc, item, unit, df::unit_inventory_item::Worn, bpIndex)) + { + if (verbose) { Core::printerr("\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) { Core::print(" Success!\n"); } + return true; +} + + command_result df_forceequip(color_ostream &out, vector & parameters) { // The "here" option is hardcoded to true, because the plugin currently doesn't support @@ -425,7 +609,7 @@ command_result df_forceequip(color_ostream &out, vector & parameters) else { itemsFound ++; // Track the number of items found under the cursor (for feedback purposes) - if (Items::moveToInventory(mc, currentItem, targetUnit, targetBodyPart, ignore, multiEquipLimit, verbose)) + if (moveToInventory(mc, currentItem, targetUnit, targetBodyPart, ignore, multiEquipLimit, verbose)) { // // TODO TEMP EXPERIMENTAL - try to alter the item size in order to conform to its wearer // currentItem->getRace(); diff --git a/plugins/lua/sort/items.lua b/plugins/lua/sort/items.lua index 2e1b3fd1f..13b62ff9b 100644 --- a/plugins/lua/sort/items.lua +++ b/plugins/lua/sort/items.lua @@ -19,14 +19,7 @@ orders.type = { orders.description = { key = function(item) - return dfhack.with_temp_object( - df.new "string", - function(str,item) - item:getItemDescription(str,0) - return str.value - end, - item - ) + return dfhack.items.getDescription(item,0) end }