diff --git a/depends/lua/CMakeLists.txt b/depends/lua/CMakeLists.txt index 8c58263d5..f286d4827 100644 --- a/depends/lua/CMakeLists.txt +++ b/depends/lua/CMakeLists.txt @@ -96,7 +96,8 @@ add_library(lua SHARED ${SRC_LIBLUA}) target_link_libraries(lua ${LIBS}) if(MSVC) - target_compile_options(lua PRIVATE /FI dfhack_llimits.h) + # need no space to prevent /FI from being stripped: https://github.com/DFHack/dfhack/issues/1455 + target_compile_options(lua PRIVATE "/FIdfhack_llimits.h") else() target_compile_options(lua PRIVATE -include dfhack_llimits.h) endif() diff --git a/dfhack.init-example b/dfhack.init-example index bdf035bd3..d8cf832c7 100644 --- a/dfhack.init-example +++ b/dfhack.init-example @@ -262,6 +262,9 @@ enable \ # enable mouse controls and sand indicator in embark screen embark-tools enable sticky sand mouse +# enable option to enter embark assistant +enable embark-assistant + ########### # Scripts # ########### diff --git a/docs/Authors.rst b/docs/Authors.rst index 5c3aae2fd..fa315677a 100644 --- a/docs/Authors.rst +++ b/docs/Authors.rst @@ -56,8 +56,12 @@ IndigoFenix James Logsdon jlogsdon Japa JapaMala Jared Adams +Jeremy Apthorp nornagon Jim Lisi stonetoad +Jimbo Whales jimbowhales +jimcarreer jimcarreer jj jjyg jj`` +Joel Meador janxious John Beisley huin John Shade gsvslto Jonas Ask diff --git a/docs/Plugins.rst b/docs/Plugins.rst index 2b78fd700..ac1f7dae8 100644 --- a/docs/Plugins.rst +++ b/docs/Plugins.rst @@ -1480,7 +1480,7 @@ Some widgets support additional options: .. _dwarfvet: dwarfvet -============ +======== Enables Animal Caretaker functionality Always annoyed your dragons become useless after a minor injury? Well, with @@ -1829,7 +1829,63 @@ nestboxes ========= Automatically scan for and forbid fertile eggs incubating in a nestbox. -Toggle status with `enable` or `disable`. +Toggle status with `enable` or `disable `. + +.. _tailor: + +tailor +====== + +Whenever the bookkeeper updates stockpile records, this plugin will scan every unit in the fort, +count up the number that are worn, and then order enough more made to replace all worn items. +If there are enough replacement items in inventory to replace all worn items, the units wearing them +will have the worn items confiscated (in the same manner as the `cleanowned` plugin) so that they'll +reeequip with replacement items. + +Use the `enable` and `disable ` commands to toggle this plugin's status, or run +``tailor status`` to check its current status. + +.. _autoclothing: + +autoclothing +============ + +Automatically manage clothing work orders, allowing the user to set how many of +each clothing type every citizen should have. Usage:: + + autoclothing [number] + +Examples: + +* ``autoclothing cloth "short skirt" 10``: + Sets the desired number of cloth short skirts available per citizen to 10. +* ``autoclothing cloth dress``: + Displays the currently set number of cloth dresses chosen per citizen. + +.. _autofarm: + +autofarm +======== + +Automatically handles crop selection in farm plots based on current plant +stocks, and selects crops for planting if current stock is below a threshold. +Selected crops are dispatched on all farmplots. (Note that this plugin replaces +an older Ruby script of the same name.) + +Use the `enable` or `disable ` commands to change whether this plugin is +enabled. + +Usage: + +* ``autofarm runonce``: + Updates all farm plots once, without enabling the plugin +* ``autofarm status``: + Prints status information, including any applied limits +* ``autofarm default 30``: + Sets the default threshold +* ``autofarm threshold 150 helmet_plump tail_pig``: + Sets thresholds of individual plants + ================ Map modification @@ -2242,16 +2298,27 @@ by spaces. Options: -:-t: Select trees only (exclude shrubs) -:-s: Select shrubs only (exclude trees) -:-c: Clear designations instead of setting them -:-x: Apply selected action to all plants except those specified (invert +:``-t``: Select trees only (exclude shrubs) +:``-s``: Select shrubs only (exclude trees) +:``-c``: Clear designations instead of setting them +:``-x``: Apply selected action to all plants except those specified (invert selection) -:-a: Select every type of plant (obeys ``-t``/``-s``) +:``-a``: Select every type of plant (obeys ``-t``/``-s``) +:``-v``: Lists the number of (un)designations per plant Specifying both ``-t`` and ``-s`` will have no effect. If no plant IDs are specified, all valid plant IDs will be listed. +.. note:: + + DF is capable of determining that a shrub has already been picked, leaving + an unusable structure part behind. This plugin does not perform such a check + (as the location of the required information has not yet been identified). + This leads to some shrubs being designated when they shouldn't be, causing a + plant gatherer to walk there and do nothing (except clearing the + designation). See :issue:`1479` for details. + + .. _infiniteSky: infiniteSky @@ -2266,8 +2333,10 @@ Usage: ``infiniteSky enable/disable`` Enables/disables monitoring of constructions. If you build anything in the second to highest z-level, it will allocate one more sky level. This is so you can continue to build stairs upward. -:issue:`Sometimes <254>` new z-levels disappear and cause cave-ins. -Saving and loading after creating new z-levels should fix the problem. +.. warning:: + + :issue:`Sometimes <254>` new z-levels disappear and cause cave-ins. + Saving and loading after creating new z-levels should fix the problem. .. _liquids: diff --git a/docs/changelog.txt b/docs/changelog.txt index 47dcaa58b..41405a53a 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -52,6 +52,9 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - `embark-assistant`: - fixed bug causing crash on worlds without generated metals (as well as pruning vectors as originally intended). - fixed bug causing mineral matching to fail to cut off at the magma sea, reporting presence of things that aren't (like DF does currently). + - fixed bug causing half of the river tiles not to be recognized. + - added logic to detect some river tiles DF doesn't generate data for (but are definitely present). +- `getplants`: fixed designation of plants out of season and added verbose flag, but failed to identify picked plants (which are still designated incorrectly) - `gui/autogems`: fixed error when no world is loaded - `gui/companion-order`: - fixed error when resetting group leaders @@ -85,6 +88,8 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - changed matching to take incursions, i.e. parts of other biomes, into consideration when evaluating tiles. This allows for e.g. finding multiple biomes on single tile embarks. - changed overlay display to show when incursion surveying is incomplete - changed overlay display to show evil weather + - added optional parameter "fileresult" for crude external harness automated match support + - improved focus movement logic to go to only required world tiles, increasing speed of subsequent searches considerably - `exportlegends`: added rivers to custom XML export - `exterminate`: added support for a special ``enemy`` caste - `modtools/create-unit`: diff --git a/library/DataStatics.cpp b/library/DataStatics.cpp index 31c8c0200..61b7b6378 100644 --- a/library/DataStatics.cpp +++ b/library/DataStatics.cpp @@ -1,3 +1,4 @@ +#include "Core.h" #include "Internal.h" #include "DataDefs.h" #include "MiscUtils.h" diff --git a/library/DataStaticsFields.cpp b/library/DataStaticsFields.cpp index d6f0414bb..8318b523d 100644 --- a/library/DataStaticsFields.cpp +++ b/library/DataStaticsFields.cpp @@ -1,5 +1,7 @@ #include +#include + #ifndef STATIC_FIELDS_GROUP #include "DataDefs.h" #endif diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 2840aa5b9..7c3ef965d 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1611,6 +1611,7 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = { WRAPM(Units, isOwnCiv), WRAPM(Units, isOwnGroup), WRAPM(Units, isOwnRace), + WRAPM(Units, getPhysicalDescription), WRAPM(Units, getRaceName), WRAPM(Units, getRaceNamePlural), WRAPM(Units, getRaceBabyName), diff --git a/library/TileTypes.cpp b/library/TileTypes.cpp index bd2c52a11..fc2d044ee 100644 --- a/library/TileTypes.cpp +++ b/library/TileTypes.cpp @@ -26,6 +26,7 @@ distribution. #include "TileTypes.h" #include "Export.h" +#include #include using namespace DFHack; diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h index 149ac47a0..7c3b534b6 100644 --- a/library/include/DataDefs.h +++ b/library/include/DataDefs.h @@ -32,7 +32,6 @@ distribution. #include #include -#include "Core.h" #include "BitArray.h" // Stop some MS stupidity @@ -48,6 +47,7 @@ typedef struct lua_State lua_State; namespace DFHack { + class Core; class virtual_class {}; enum identity_type { diff --git a/library/include/DataFuncs.h b/library/include/DataFuncs.h index 6541ae900..63b40ef52 100644 --- a/library/include/DataFuncs.h +++ b/library/include/DataFuncs.h @@ -32,6 +32,10 @@ distribution. #include "DataIdentity.h" #include "LuaWrapper.h" +namespace DFHack { + class color_ostream; +} + namespace df { // A very simple and stupid implementation of some stuff from boost template struct is_same_type { static const bool value = false; }; diff --git a/library/include/DataIdentity.h b/library/include/DataIdentity.h index 62a9ff274..88a96f9f3 100644 --- a/library/include/DataIdentity.h +++ b/library/include/DataIdentity.h @@ -24,6 +24,7 @@ distribution. #pragma once +#include #include #include #include diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index 814afe179..df89d184f 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -30,6 +30,7 @@ distribution. #include #include +#include "ColorText.h" #include "DataDefs.h" #include diff --git a/library/include/MiscUtils.h b/library/include/MiscUtils.h index ec2c7be58..35f8be73b 100644 --- a/library/include/MiscUtils.h +++ b/library/include/MiscUtils.h @@ -45,6 +45,17 @@ using std::endl; #define DFHACK_FUNCTION_SIG __func__ #endif +#ifdef _WIN32 +// On x86 MSVC, __thiscall passes |this| in ECX. On x86_64, __thiscall is the +// same as the standard calling convention. +// See https://docs.microsoft.com/en-us/cpp/cpp/thiscall for more info. +#define THISCALL __thiscall +#else +// On other platforms, there's no special calling convention for calling member +// functions. +#define THISCALL +#endif + namespace DFHack { class color_ostream; } diff --git a/library/include/Types.h b/library/include/Types.h index 157b23a0e..38830f53c 100644 --- a/library/include/Types.h +++ b/library/include/Types.h @@ -25,6 +25,8 @@ distribution. #pragma once +#include + #include "Pragma.h" #include "Export.h" diff --git a/library/include/modules/Buildings.h b/library/include/modules/Buildings.h index 4032b96af..50cbc898f 100644 --- a/library/include/modules/Buildings.h +++ b/library/include/modules/Buildings.h @@ -53,6 +53,9 @@ namespace df namespace DFHack { + +class color_ostream; + namespace Buildings { /** diff --git a/library/include/modules/Job.h b/library/include/modules/Job.h index 814f1e062..cde5c64dd 100644 --- a/library/include/modules/Job.h +++ b/library/include/modules/Job.h @@ -47,6 +47,8 @@ namespace df namespace DFHack { + class color_ostream; + namespace Job { // Duplicate the job structure. It is not linked into any DF lists. DFHACK_EXPORT df::job *cloneJobStruct(df::job *job, bool keepEverything=false); diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h index 09c39be55..f5995583b 100644 --- a/library/include/modules/Units.h +++ b/library/include/modules/Units.h @@ -120,6 +120,7 @@ DFHACK_EXPORT bool isVisible(df::unit* unit); DFHACK_EXPORT std::string getRaceNameById(int32_t race_id); DFHACK_EXPORT std::string getRaceName(df::unit* unit); +DFHACK_EXPORT std::string getPhysicalDescription(df::unit* unit); DFHACK_EXPORT std::string getRaceNamePluralById(int32_t race_id); DFHACK_EXPORT std::string getRaceNamePlural(df::unit* unit); DFHACK_EXPORT std::string getRaceBabyNameById(int32_t race_id); diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index 93ab48dca..049297385 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -765,14 +765,15 @@ function dfhack.run_command_silent(...) end function dfhack.run_command(...) - local output, status = _run_command(...) - for i, fragment in pairs(output) do - if type(fragment) == 'table' then - dfhack.color(fragment[1]) - dfhack.print(fragment[2]) + local result = _run_command(...) + for i, f in pairs(result) do + if type(f) == 'table' then + dfhack.color(f[1]) + dfhack.print(f[2]) end end dfhack.color(COLOR_RESET) + return result.status end -- Per-save init file diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index f891442b4..3e5222100 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -716,13 +716,16 @@ end function FilteredList:onInput(keys) if self.edit_key and keys[self.edit_key] and not self.edit.active then self.edit.active = true + return true elseif keys.LEAVESCREEN and self.edit.active then self.edit.active = false + return true else - self:inputToSubviews(keys) + return self:inputToSubviews(keys) end end + function FilteredList:getChoices() return self.choices end diff --git a/library/modules/Buildings.cpp b/library/modules/Buildings.cpp index 732956a3e..607f63a36 100644 --- a/library/modules/Buildings.cpp +++ b/library/modules/Buildings.cpp @@ -1199,7 +1199,7 @@ void Buildings::clearBuildings(color_ostream& out) { void Buildings::updateBuildings(color_ostream& out, void* ptr) { - int32_t id = *((int32_t*)ptr); + int32_t id = (int32_t)(intptr_t)ptr; auto building = df::building::find(id); if (building) diff --git a/library/modules/EventManager.cpp b/library/modules/EventManager.cpp index 404d2342c..7d8b7e6d5 100644 --- a/library/modules/EventManager.cpp +++ b/library/modules/EventManager.cpp @@ -273,7 +273,7 @@ void DFHack::EventManager::onStateChange(color_ostream& out, state_change_event } for ( size_t a = 0; a < df::global::world->buildings.all.size(); a++ ) { df::building* b = df::global::world->buildings.all[a]; - Buildings::updateBuildings(out, (void*)&(b->id)); + Buildings::updateBuildings(out, (void*)intptr_t(b->id)); buildings.insert(b->id); } lastSyndromeTime = -1; @@ -609,7 +609,7 @@ static void manageBuildingEvent(color_ostream& out) { buildings.insert(a); for ( auto b = copy.begin(); b != copy.end(); b++ ) { EventHandler bob = (*b).second; - bob.eventHandler(out, (void*)&a); + bob.eventHandler(out, (void*)intptr_t(a)); } } nextBuilding = *df::global::building_next_id; @@ -625,7 +625,7 @@ static void manageBuildingEvent(color_ostream& out) { for ( auto b = copy.begin(); b != copy.end(); b++ ) { EventHandler bob = (*b).second; - bob.eventHandler(out, (void*)&id); + bob.eventHandler(out, (void*)intptr_t(id)); } a = buildings.erase(a); } diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 861c25ce9..1907b1641 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -541,6 +541,25 @@ string Units::getRaceName(df::unit* unit) return getRaceNameById(unit->race); } +void df_unit_get_physical_description(df::unit* unit, string* out_str) +{ + static auto* const fn = + reinterpret_cast( + Core::getInstance().vinfo->getAddress("unit_get_physical_description")); + if (fn) + fn(unit, out_str); + else + *out_str = ""; +} + +string Units::getPhysicalDescription(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + string str; + df_unit_get_physical_description(unit, &str); + return str; +} + // get plural of race name (used for display in autobutcher UI and for sorting the watchlist) string Units::getRaceNamePluralById(int32_t id) { @@ -1549,8 +1568,8 @@ bool Units::isGay(df::unit* unit) if (!unit->status.current_soul) return false; df::orientation_flags orientation = unit->status.current_soul->orientation_flags; - return (Units::isFemale(unit) && ! (orientation.whole & (orientation.mask_marry_male | orientation.mask_romance_male))) - || (!Units::isFemale(unit) && ! (orientation.whole & (orientation.mask_marry_female | orientation.mask_romance_female))); + return (!Units::isFemale(unit) || !(orientation.whole & (orientation.mask_marry_male | orientation.mask_romance_male))) + && (!Units::isMale(unit) || !(orientation.whole & (orientation.mask_marry_female | orientation.mask_romance_female))); } bool Units::isNaked(df::unit* unit) diff --git a/library/xml b/library/xml index 4388fbfb8..4053321b2 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 4388fbfb8f51be41777406c6e7c518f738c195c7 +Subproject commit 4053321b202a29f667d64d824ba8339ec1b1df4f diff --git a/package/darwin/dfhack b/package/darwin/dfhack index 445b600f7..9ededf5d5 100755 --- a/package/darwin/dfhack +++ b/package/darwin/dfhack @@ -11,6 +11,12 @@ else export DYLD_FALLBACK_FRAMEWORK_PATH="./hack:./libs:./hack/libs" fi +# attempt to remove quarantine flag: https://github.com/DFHack/dfhack/issues/1465 +if ! test -f hack/quarantine-removed; then + find hack/ libs/ dwarfort.exe -name '*.dylib' -or -name '*.exe' -print0 | xargs -0 xattr -d com.apple.quarantine 2>&1 | grep -iv 'no such xattr' + echo "quarantine flag removed on $(date); remove this file to re-run" > hack/quarantine-removed +fi + old_tty_settings=$(stty -g) DYLD_INSERT_LIBRARIES=./hack/libdfhack.dylib ./dwarfort.exe "$@" stty "$old_tty_settings" diff --git a/package/linux/dfhack b/package/linux/dfhack index d0c70765b..1bd3e8d92 100755 --- a/package/linux/dfhack +++ b/package/linux/dfhack @@ -69,6 +69,16 @@ fi PRELOAD_LIB="${PRELOAD_LIB:+$PRELOAD_LIB:}${LIBSAN}${LIB}" setarch_arch=$(cat hack/dfhack_setarch.txt || printf i386) +if ! setarch "$setarch_arch" -R true 2>/dev/null; then + echo "warn: architecture '$setarch_arch' not supported by setarch" >&2 + if [ "$setarch_arch" = "i386" ]; then + setarch_arch=linux32 + else + setarch_arch=linux64 + fi + echo "using '$setarch_arch' instead. To silence this warning, edit" >&2 + echo "hack/dfhack_setarch.txt to contain an architecture that works on your system." >&2 +fi case "$1" in -g | --gdb) diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 68fd44191..678b7eec2 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -84,7 +84,9 @@ if(BUILD_SUPPORTED) dfhack_plugin(add-spatter add-spatter.cpp) # dfhack_plugin(advtools advtools.cpp) dfhack_plugin(autochop autochop.cpp) + dfhack_plugin(autoclothing autoclothing.cpp) dfhack_plugin(autodump autodump.cpp) + dfhack_plugin(autofarm autofarm.cpp) dfhack_plugin(autogems autogems.cpp LINK_LIBRARIES jsoncpp_lib_static) dfhack_plugin(autohauler autohauler.cpp) dfhack_plugin(autolabor autolabor.cpp) @@ -164,6 +166,7 @@ if(BUILD_SUPPORTED) add_subdirectory(stockpiles) dfhack_plugin(stocks stocks.cpp) dfhack_plugin(strangemood strangemood.cpp) + dfhack_plugin(tailor tailor.cpp) dfhack_plugin(tiletypes tiletypes.cpp Brushes.h) dfhack_plugin(title-folder title-folder.cpp) dfhack_plugin(title-version title-version.cpp) diff --git a/plugins/autoclothing.cpp b/plugins/autoclothing.cpp new file mode 100644 index 000000000..d19e647d5 --- /dev/null +++ b/plugins/autoclothing.cpp @@ -0,0 +1,672 @@ + +// some headers required for a plugin. Nothing special, just the basics. +#include "Core.h" +#include +#include +#include + +#include + +// DF data structure definition headers +#include "DataDefs.h" + +#include "modules/Items.h" +#include "modules/Maps.h" +#include "modules/Materials.h" +#include "modules/Units.h" +#include "modules/World.h" + +#include "df/itemdef_armorst.h" +#include "df/itemdef_glovesst.h" +#include "df/itemdef_shoesst.h" +#include "df/itemdef_helmst.h" +#include "df/itemdef_pantsst.h" +#include "df/manager_order.h" +#include "df/creature_raw.h" +#include "df/world.h" + +using namespace DFHack; +using namespace DFHack::Items; +using namespace DFHack::Units; +using namespace df::enums; + + +// A plugin must be able to return its name and version. +// The name string provided must correspond to the filename - +// skeleton.plug.so, skeleton.plug.dylib, or skeleton.plug.dll in this case +DFHACK_PLUGIN("autoclothing"); + +// Any globals a plugin requires (e.g. world) should be listed here. +// For example, this line expands to "using df::global::world" and prevents the +// plugin from being loaded if df::global::world is null (i.e. missing from symbols.xml): +// +REQUIRE_GLOBAL(world); + +// Only run if this is enabled +DFHACK_PLUGIN_IS_ENABLED(autoclothing_enabled); + +// Here go all the command declarations... +// mostly to allow having the mandatory stuff on top of the file and commands on the bottom +struct ClothingRequirement; +command_result autoclothing(color_ostream &out, std::vector & parameters); +static void init_state(color_ostream &out); +static void save_state(color_ostream &out); +static void cleanup_state(color_ostream &out); +static void do_autoclothing(); +static bool validateMaterialCategory(ClothingRequirement * requirement); +static bool setItem(std::string name, ClothingRequirement* requirement); + +std::vectorclothingOrders; + +struct ClothingRequirement +{ + df::job_type jobType; + df::item_type itemType; + int16_t item_subtype; + df::job_material_category material_category; + int16_t needed_per_citizen; + std::map total_needed_per_race; + + bool matches(ClothingRequirement * b) + { + if (b->jobType != this->jobType) + return false; + if (b->itemType != this->itemType) + return false; + if (b->item_subtype != this->item_subtype) + return false; + if (b->material_category.whole != this->material_category.whole) + return false; + return true; + } + + std::string Serialize() + { + stringstream stream; + stream << ENUM_KEY_STR(job_type, jobType) << " "; + stream << ENUM_KEY_STR(item_type,itemType) << " "; + stream << item_subtype << " "; + stream << material_category.whole << " "; + stream << needed_per_citizen; + return stream.str(); + } + + void Deserialize(std::string s) + { + stringstream stream(s); + std::string loadedJob; + stream >> loadedJob; + FOR_ENUM_ITEMS(job_type, job) + { + if (ENUM_KEY_STR(job_type, job) == loadedJob) + { + jobType = job; + break; + } + } + std::string loadedItem; + stream >> loadedItem; + FOR_ENUM_ITEMS(item_type, item) + { + if (ENUM_KEY_STR(item_type, item) == loadedItem) + { + itemType = item; + break; + } + } + stream >> item_subtype; + stream >> material_category.whole; + stream >> needed_per_citizen; + } + + bool SetFromParameters(color_ostream &out, std::vector & parameters) + { + if (!set_bitfield_field(&material_category, parameters[0], 1)) + { + out << "Unrecognized material type: " << parameters[0] << endl; + } + if (!setItem(parameters[1], this)) + { + out << "Unrecognized item name or token: " << parameters[1] << endl; + return false; + } + if (!validateMaterialCategory(this)) + { + out << parameters[0] << " is not a valid material category for " << parameters[1] << endl; + return false; + } + return true; + } + + std::string ToReadableLabel() + { + stringstream stream; + stream << bitfield_to_string(material_category) << " "; + std::string adjective = ""; + std::string name = ""; + switch (itemType) + { + case df::enums::item_type::ARMOR: + adjective = world->raws.itemdefs.armor[item_subtype]->adjective; + name = world->raws.itemdefs.armor[item_subtype]->name; + break; + case df::enums::item_type::SHOES: + adjective = world->raws.itemdefs.shoes[item_subtype]->adjective; + name = world->raws.itemdefs.shoes[item_subtype]->name; + break; + case df::enums::item_type::HELM: + adjective = world->raws.itemdefs.helms[item_subtype]->adjective; + name = world->raws.itemdefs.helms[item_subtype]->name; + break; + case df::enums::item_type::GLOVES: + adjective = world->raws.itemdefs.gloves[item_subtype]->adjective; + name = world->raws.itemdefs.gloves[item_subtype]->name; + break; + case df::enums::item_type::PANTS: + adjective = world->raws.itemdefs.pants[item_subtype]->adjective; + name = world->raws.itemdefs.pants[item_subtype]->name; + break; + default: + break; + } + if (!adjective.empty()) + stream << adjective << " "; + stream << name << " "; + stream << needed_per_citizen; + + return stream.str(); + } +}; + + +// Mandatory init function. If you have some global state, create it here. +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) +{ + // Fill the command list with your commands. + commands.push_back(PluginCommand( + "autoclothing", "Automatically manage clothing work orders", + autoclothing, false, /* true means that the command can't be used from non-interactive user interface */ + // Extended help string. Used by CR_WRONG_USAGE and the help command: + " autoclothing [number]\n" + "Example:\n" + " autoclothing cloth \"short skirt\" 10\n" + " Sets the desired number of cloth short skirts available per citizen to 10.\n" + " autoclothing cloth dress\n" + " Displays the currently set number of cloth dresses chosen per citizen.\n" + )); + return CR_OK; +} + +// This is called right before the plugin library is removed from memory. +DFhackCExport command_result plugin_shutdown(color_ostream &out) +{ + // You *MUST* kill all threads you created before this returns. + // If everything fails, just return CR_FAILURE. Your plugin will be + // in a zombie state, but things won't crash. + cleanup_state(out); + + return CR_OK; +} + +// Called to notify the plugin about important state changes. +// Invoked with DF suspended, and always before the matching plugin_onupdate. +// More event codes may be added in the future. + +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) +{ + switch (event) { + case SC_WORLD_LOADED: + init_state(out); + break; + case SC_WORLD_UNLOADED: + cleanup_state(out); + break; + default: + break; + } + return CR_OK; +} + + +// Whatever you put here will be done in each game step. Don't abuse it. +// It's optional, so you can just comment it out like this if you don't need it. + +DFhackCExport command_result plugin_onupdate(color_ostream &out) +{ + if (!autoclothing_enabled) + return CR_OK; + + if (!Maps::IsValid()) + return CR_OK; + + if (DFHack::World::ReadPauseState()) + return CR_OK; + + if ((world->frame_counter + 500) % 1200 != 0) // Check every day, but not the same day as other things + return CR_OK; + + do_autoclothing(); + + return CR_OK; +} + +static bool setItemFromName(std::string name, ClothingRequirement* requirement) +{ +#define SEARCH_ITEM_RAWS(rawType, job, item) \ +for (auto& itemdef : world->raws.itemdefs.rawType) \ +{ \ + std::string fullName = itemdef->adjective.empty() ? itemdef->name : itemdef->adjective + " " + itemdef->name; \ + if (fullName == name) \ + { \ + requirement->jobType = job_type::job; \ + requirement->itemType = item_type::item; \ + requirement->item_subtype = itemdef->subtype; \ + return true; \ + } \ +} + SEARCH_ITEM_RAWS(armor, MakeArmor, ARMOR); + SEARCH_ITEM_RAWS(gloves, MakeGloves, GLOVES); + SEARCH_ITEM_RAWS(shoes, MakeShoes, SHOES); + SEARCH_ITEM_RAWS(helms, MakeHelm, HELM); + SEARCH_ITEM_RAWS(pants, MakePants, PANTS); + return false; +} + +static bool setItemFromToken(std::string token, ClothingRequirement* requirement) +{ + ItemTypeInfo itemInfo; + if (!itemInfo.find(token)) + return false; + switch (itemInfo.type) + { + case item_type::ARMOR: + requirement->jobType = job_type::MakeArmor; + break; + case item_type::GLOVES: + requirement->jobType = job_type::MakeGloves; + break; + case item_type::SHOES: + requirement->jobType = job_type::MakeShoes; + break; + case item_type::HELM: + requirement->jobType = job_type::MakeHelm; + break; + case item_type::PANTS: + requirement->jobType = job_type::MakePants; + break; + default: + return false; + } + requirement->itemType = itemInfo.type; + requirement->item_subtype = itemInfo.subtype; + return true; +} + +static bool setItem(std::string name, ClothingRequirement* requirement) +{ + if (setItemFromName(name, requirement)) + return true; + if (setItemFromToken(name, requirement)) + return true; + return false; +} + +static bool armorFlagsMatch(BitArray * flags, df::job_material_category * category) +{ + if (flags->is_set(df::armor_general_flags::SOFT) && category->bits.cloth) + return true; + if (flags->is_set(df::armor_general_flags::SOFT) && category->bits.yarn) + return true; + if (flags->is_set(df::armor_general_flags::SOFT) && category->bits.silk) + return true; + if (flags->is_set(df::armor_general_flags::BARRED) && category->bits.bone) + return true; + if (flags->is_set(df::armor_general_flags::SCALED) && category->bits.shell) + return true; + if (flags->is_set(df::armor_general_flags::LEATHER) && category->bits.leather) + return true; + return false; +} + +static bool validateMaterialCategory(ClothingRequirement * requirement) +{ + auto itemDef = getSubtypeDef(requirement->itemType, requirement->item_subtype); + switch (requirement->itemType) + { + case item_type::ARMOR: + if (STRICT_VIRTUAL_CAST_VAR(armor, df::itemdef_armorst, itemDef)) + return armorFlagsMatch(&armor->props.flags, &requirement->material_category); + case item_type::GLOVES: + if (STRICT_VIRTUAL_CAST_VAR(armor, df::itemdef_glovesst, itemDef)) + return armorFlagsMatch(&armor->props.flags, &requirement->material_category); + case item_type::SHOES: + if (STRICT_VIRTUAL_CAST_VAR(armor, df::itemdef_shoesst, itemDef)) + return armorFlagsMatch(&armor->props.flags, &requirement->material_category); + case item_type::HELM: + if (STRICT_VIRTUAL_CAST_VAR(armor, df::itemdef_helmst, itemDef)) + return armorFlagsMatch(&armor->props.flags, &requirement->material_category); + case item_type::PANTS: + if (STRICT_VIRTUAL_CAST_VAR(armor, df::itemdef_pantsst, itemDef)) + return armorFlagsMatch(&armor->props.flags, &requirement->material_category); + default: + return false; + } +} + + + +// A command! It sits around and looks pretty. And it's nice and friendly. +command_result autoclothing(color_ostream &out, std::vector & parameters) +{ + // It's nice to print a help message you get invalid options + // from the user instead of just acting strange. + // This can be achieved by adding the extended help string to the + // PluginCommand registration as show above, and then returning + // CR_WRONG_USAGE from the function. The same string will also + // be used by 'help your-command'. + if (parameters.size() == 0) + { + out << "Currently set " << clothingOrders.size() << " automatic clothing orders" << endl; + for (size_t i = 0; i < clothingOrders.size(); i++) + { + out << clothingOrders[i].ToReadableLabel() << endl; + } + return CR_OK; + } + else if (parameters.size() < 2 || parameters.size() > 3) + { + out << "Wrong number of arguments." << endl; + return CR_WRONG_USAGE; + } + // Commands are called from threads other than the DF one. + // Suspend this thread until DF has time for us. If you + // use CoreSuspender, it'll automatically resume DF when + // execution leaves the current scope. + CoreSuspender suspend; + + + // Create a new requirement from the available parameters. + ClothingRequirement newRequirement; + if (!newRequirement.SetFromParameters(out, parameters)) + return CR_WRONG_USAGE; + //all checks are passed. Now we either show or set the amount. + bool settingSize = false; + bool matchedExisting = false; + if (parameters.size() > 2) + { + try + { + newRequirement.needed_per_citizen = std::stoi(parameters[2]); + } + catch (const std::exception&) + { + out << parameters[2] << " is not a valid number." << endl; + return CR_WRONG_USAGE; + } + settingSize = true; + } + + for (size_t i = 0; i < clothingOrders.size(); i++) + { + if (!clothingOrders[i].matches(&newRequirement)) + continue; + matchedExisting = true; + if (settingSize) + { + if (newRequirement.needed_per_citizen == 0) + { + clothingOrders.erase(clothingOrders.begin() + i); + out << "Unset " << parameters[0] << " " << parameters[1] << endl; + } + else + { + clothingOrders[i] = newRequirement; + out << "Set " << parameters[0] << " " << parameters[1] << " to " << parameters[2] << endl; + } + } + else + { + out << parameters[0] << " " << parameters[1] << " is set to " << clothingOrders[i].needed_per_citizen << endl; + } + break; + } + if (!matchedExisting) + { + if (settingSize) + { + if (newRequirement.needed_per_citizen == 0) + { + out << parameters[0] << " " << parameters[1] << " already unset." << endl; + } + else + { + clothingOrders.push_back(newRequirement); + out << "Added order for " << parameters[0] << " " << parameters[1] << " to " << parameters[2] << endl; + } + } + else + { + out << parameters[0] << " " << parameters[1] << " is not set." << endl; + } + } + if (settingSize) + { + if (!autoclothing_enabled) + { + out << "Enabling automatic clothing management" << endl; + autoclothing_enabled = true; + } + do_autoclothing(); + } + save_state(out); + + // Give control back to DF. + return CR_OK; +} + +static void find_needed_clothing_items() +{ + for (auto& unit : world->units.active) + { + //obviously we don't care about illegal aliens. + if (!isCitizen(unit)) + continue; + + //now check each clothing order to see what the unit might be missing. + for (auto& clothingOrder : clothingOrders) + { + int alreadyOwnedAmount = 0; + + //looping through the items first, then clothing order might be a little faster, but this way is cleaner. + for (auto& ownedItem : unit->owned_items) + { + auto item = findItemByID(ownedItem); + + if (item->getType() != clothingOrder.itemType) + continue; + if (item->getSubtype() != clothingOrder.item_subtype) + continue; + + MaterialInfo matInfo; + matInfo.decode(item); + + if (!matInfo.matches(clothingOrder.material_category)) + continue; + + alreadyOwnedAmount++; + } + int neededAmount = clothingOrder.needed_per_citizen - alreadyOwnedAmount; + + if (neededAmount <= 0) + continue; + + //technically, there's some leeway in sizes, but only caring about exact sizes is simpler. + clothingOrder.total_needed_per_race[unit->race] += neededAmount; + } + } +} + +static void remove_available_clothing() +{ + for (auto& item : world->items.all) + { + //skip any owned items. + if (getOwner(item)) + continue; + + //again, for each item, find if any clothing order matches + for (auto& clothingOrder : clothingOrders) + { + if (item->getType() != clothingOrder.itemType) + continue; + if (item->getSubtype() != clothingOrder.item_subtype) + continue; + + MaterialInfo matInfo; + matInfo.decode(item); + + if (!matInfo.matches(clothingOrder.material_category)) + continue; + + clothingOrder.total_needed_per_race[item->getMakerRace()] --; + } + } +} + +static void add_clothing_orders() +{ + for (auto& clothingOrder : clothingOrders) + { + for (auto& orderNeeded : clothingOrder.total_needed_per_race) + { + auto race = orderNeeded.first; + auto amount = orderNeeded.second; + orderNeeded.second = 0; //once we get what we need, set it back to zero so we don't add it to further counts. + //Previous operations can easily make this negative. That jus means we have more than we need already. + if (amount <= 0) + continue; + + bool orderExistedAlready = false; + for (auto& managerOrder : world->manager_orders) + { + //Annoyingly, the manager orders store the job type for clothing orders, and actual item type is left at -1; + if (managerOrder->job_type != clothingOrder.jobType) + continue; + if (managerOrder->item_subtype != clothingOrder.item_subtype) + continue; + if (managerOrder->hist_figure_id != race) + continue; + + //We found a work order, that means we don't need to make a new one. + orderExistedAlready = true; + amount -= managerOrder->amount_left; + if (amount > 0) + { + managerOrder->amount_left += amount; + managerOrder->amount_total += amount; + } + } + //if it wasn't there, we need to make a new one. + if (!orderExistedAlready) + { + df::manager_order * newOrder = new df::manager_order(); + + newOrder->id = world->manager_order_next_id; + world->manager_order_next_id++; + newOrder->job_type = clothingOrder.jobType; + newOrder->item_subtype = clothingOrder.item_subtype; + newOrder->hist_figure_id = race; + newOrder->material_category = clothingOrder.material_category; + newOrder->amount_left = amount; + newOrder->amount_total = amount; + world->manager_orders.push_back(newOrder); + } + } + } +} + +static void do_autoclothing() +{ + if (clothingOrders.size() == 0) + return; + + //first we look through all the units on the map to see who needs new clothes. + find_needed_clothing_items(); + + //Now we go through all the items in the map to see how many clothing items we have but aren't owned yet. + remove_available_clothing(); + + //Finally loop through the clothing orders to find ones that need more made. + add_clothing_orders(); +} + +static void cleanup_state(color_ostream &out) +{ + clothingOrders.clear(); + autoclothing_enabled = false; +} + +static void init_state(color_ostream &out) +{ + auto enabled = World::GetPersistentData("autoclothing/enabled"); + if (enabled.isValid() && enabled.ival(0) == 1) + { + out << "autoclothing enabled" << endl; + autoclothing_enabled = true; + } + else + { + autoclothing_enabled = false; + } + + + // Parse constraints + std::vector items; + World::GetPersistentData(&items, "autoclothing/clothingItems"); + + for (auto& item : items) + { + if (!item.isValid()) + continue; + ClothingRequirement req; + req.Deserialize(item.val()); + clothingOrders.push_back(req); + out << "autoclothing added " << req.ToReadableLabel() << endl; + } +} + +static void save_state(color_ostream &out) +{ + auto enabled = World::GetPersistentData("autoclothing/enabled"); + if (!enabled.isValid()) + enabled = World::AddPersistentData("autoclothing/enabled"); + enabled.ival(0) = autoclothing_enabled; + + for (auto& order : clothingOrders) + { + auto orderSave = World::AddPersistentData("autoclothing/clothingItems"); + orderSave.val() = order.Serialize(); + } + + + // Parse constraints + std::vector items; + World::GetPersistentData(&items, "autoclothing/clothingItems"); + + for (size_t i = 0; i < items.size(); i++) + { + if (i < clothingOrders.size()) + { + items[i].val() = clothingOrders[i].Serialize(); + } + else + { + World::DeletePersistentData(items[i]); + } + } + for (size_t i = items.size(); i < clothingOrders.size(); i++) + { + auto item = World::AddPersistentData("autoclothing/clothingItems"); + item.val() = clothingOrders[i].Serialize(); + } +} diff --git a/plugins/autofarm.cpp b/plugins/autofarm.cpp new file mode 100644 index 000000000..54ce3600d --- /dev/null +++ b/plugins/autofarm.cpp @@ -0,0 +1,436 @@ +#include "Core.h" +#include "Console.h" +#include "Export.h" +#include "PluginManager.h" + +#include "DataDefs.h" +#include "df/world.h" +#include "df/ui.h" +#include "df/building_type.h" +#include "df/building_farmplotst.h" +#include "df/buildings_other_id.h" +#include "df/global_objects.h" +#include "df/item.h" +#include "df/item_plantst.h" +#include "df/items_other_id.h" +#include "df/unit.h" +#include "df/building.h" +#include "df/plant_raw.h" +#include "df/plant_raw_flags.h" +#include "df/biome_type.h" +#include "modules/Items.h" +#include "modules/Maps.h" +#include "modules/World.h" + +#include + +using std::vector; +using std::string; +using std::map; +using std::set; +using std::queue; +using std::endl; +using namespace DFHack; +using namespace df::enums; + +using df::global::world; +using df::global::ui; + +static command_result autofarm(color_ostream &out, vector & parameters); + +DFHACK_PLUGIN("autofarm"); + +DFHACK_PLUGIN_IS_ENABLED(enabled); + +const char *tagline = "Automatically handle crop selection in farm plots based on current plant stocks."; +const char *usage = ( + "``enable autofarm``: Enables the plugin\n" + "``autofarm runonce``: Updates farm plots (one-time only)\n" + "``autofarm status``: Prints status information\n" + "``autofarm default 30``: Sets the default threshold\n" + "``autofarm threshold 150 helmet_plump tail_pig``: Sets thresholds\n" + ); + +class AutoFarm { +private: + map thresholds; + int defaultThreshold = 50; + + map lastCounts; + +public: + void initialize() + { + thresholds.clear(); + defaultThreshold = 50; + + lastCounts.clear(); + } + + void setThreshold(int id, int val) + { + thresholds[id] = val; + } + + int getThreshold(int id) + { + return (thresholds.count(id) > 0) ? thresholds[id] : defaultThreshold; + } + + void setDefault(int val) + { + defaultThreshold = val; + } + +private: + const df::plant_raw_flags seasons[4] = { df::plant_raw_flags::SPRING, df::plant_raw_flags::SUMMER, df::plant_raw_flags::AUTUMN, df::plant_raw_flags::WINTER }; + +public: + bool is_plantable(df::plant_raw* plant) + { + bool has_seed = plant->flags.is_set(df::plant_raw_flags::SEED); + bool is_tree = plant->flags.is_set(df::plant_raw_flags::TREE); + + int8_t season = *df::global::cur_season; + int harvest = (*df::global::cur_season_tick) + plant->growdur * 10; + bool can_plant = has_seed && !is_tree && plant->flags.is_set(seasons[season]); + while (can_plant && harvest >= 10080) { + season = (season + 1) % 4; + harvest -= 10080; + can_plant = can_plant && plant->flags.is_set(seasons[season]); + } + + return can_plant; + } + +private: + map> plantable_plants; + + const map biomeFlagMap = { + { df::plant_raw_flags::BIOME_MOUNTAIN, df::biome_type::MOUNTAIN }, + { df::plant_raw_flags::BIOME_GLACIER, df::biome_type::GLACIER }, + { df::plant_raw_flags::BIOME_TUNDRA, df::biome_type::TUNDRA }, + { df::plant_raw_flags::BIOME_SWAMP_TEMPERATE_FRESHWATER, df::biome_type::SWAMP_TEMPERATE_FRESHWATER }, + { df::plant_raw_flags::BIOME_SWAMP_TEMPERATE_SALTWATER, df::biome_type::SWAMP_TEMPERATE_SALTWATER }, + { df::plant_raw_flags::BIOME_MARSH_TEMPERATE_FRESHWATER, df::biome_type::MARSH_TEMPERATE_FRESHWATER }, + { df::plant_raw_flags::BIOME_MARSH_TEMPERATE_SALTWATER, df::biome_type::MARSH_TEMPERATE_SALTWATER }, + { df::plant_raw_flags::BIOME_SWAMP_TROPICAL_FRESHWATER, df::biome_type::SWAMP_TROPICAL_FRESHWATER }, + { df::plant_raw_flags::BIOME_SWAMP_TROPICAL_SALTWATER, df::biome_type::SWAMP_TROPICAL_SALTWATER }, + { df::plant_raw_flags::BIOME_SWAMP_MANGROVE, df::biome_type::SWAMP_MANGROVE }, + { df::plant_raw_flags::BIOME_MARSH_TROPICAL_FRESHWATER, df::biome_type::MARSH_TROPICAL_FRESHWATER }, + { df::plant_raw_flags::BIOME_MARSH_TROPICAL_SALTWATER, df::biome_type::MARSH_TROPICAL_SALTWATER }, + { df::plant_raw_flags::BIOME_FOREST_TAIGA, df::biome_type::FOREST_TAIGA }, + { df::plant_raw_flags::BIOME_FOREST_TEMPERATE_CONIFER, df::biome_type::FOREST_TEMPERATE_CONIFER }, + { df::plant_raw_flags::BIOME_FOREST_TEMPERATE_BROADLEAF, df::biome_type::FOREST_TEMPERATE_BROADLEAF }, + { df::plant_raw_flags::BIOME_FOREST_TROPICAL_CONIFER, df::biome_type::FOREST_TROPICAL_CONIFER }, + { df::plant_raw_flags::BIOME_FOREST_TROPICAL_DRY_BROADLEAF, df::biome_type::FOREST_TROPICAL_DRY_BROADLEAF }, + { df::plant_raw_flags::BIOME_FOREST_TROPICAL_MOIST_BROADLEAF, df::biome_type::FOREST_TROPICAL_MOIST_BROADLEAF }, + { df::plant_raw_flags::BIOME_GRASSLAND_TEMPERATE, df::biome_type::GRASSLAND_TEMPERATE }, + { df::plant_raw_flags::BIOME_SAVANNA_TEMPERATE, df::biome_type::SAVANNA_TEMPERATE }, + { df::plant_raw_flags::BIOME_SHRUBLAND_TEMPERATE, df::biome_type::SHRUBLAND_TEMPERATE }, + { df::plant_raw_flags::BIOME_GRASSLAND_TROPICAL, df::biome_type::GRASSLAND_TROPICAL }, + { df::plant_raw_flags::BIOME_SAVANNA_TROPICAL, df::biome_type::SAVANNA_TROPICAL }, + { df::plant_raw_flags::BIOME_SHRUBLAND_TROPICAL, df::biome_type::SHRUBLAND_TROPICAL }, + { df::plant_raw_flags::BIOME_DESERT_BADLAND, df::biome_type::DESERT_BADLAND }, + { df::plant_raw_flags::BIOME_DESERT_ROCK, df::biome_type::DESERT_ROCK }, + { df::plant_raw_flags::BIOME_DESERT_SAND, df::biome_type::DESERT_SAND }, + { df::plant_raw_flags::BIOME_OCEAN_TROPICAL, df::biome_type::OCEAN_TROPICAL }, + { df::plant_raw_flags::BIOME_OCEAN_TEMPERATE, df::biome_type::OCEAN_TEMPERATE }, + { df::plant_raw_flags::BIOME_OCEAN_ARCTIC, df::biome_type::OCEAN_ARCTIC }, + { df::plant_raw_flags::BIOME_POOL_TEMPERATE_FRESHWATER, df::biome_type::POOL_TEMPERATE_FRESHWATER }, + { df::plant_raw_flags::BIOME_POOL_TEMPERATE_BRACKISHWATER, df::biome_type::POOL_TEMPERATE_BRACKISHWATER }, + { df::plant_raw_flags::BIOME_POOL_TEMPERATE_SALTWATER, df::biome_type::POOL_TEMPERATE_SALTWATER }, + { df::plant_raw_flags::BIOME_POOL_TROPICAL_FRESHWATER, df::biome_type::POOL_TROPICAL_FRESHWATER }, + { df::plant_raw_flags::BIOME_POOL_TROPICAL_BRACKISHWATER, df::biome_type::POOL_TROPICAL_BRACKISHWATER }, + { df::plant_raw_flags::BIOME_POOL_TROPICAL_SALTWATER, df::biome_type::POOL_TROPICAL_SALTWATER }, + { df::plant_raw_flags::BIOME_LAKE_TEMPERATE_FRESHWATER, df::biome_type::LAKE_TEMPERATE_FRESHWATER }, + { df::plant_raw_flags::BIOME_LAKE_TEMPERATE_BRACKISHWATER, df::biome_type::LAKE_TEMPERATE_BRACKISHWATER }, + { df::plant_raw_flags::BIOME_LAKE_TEMPERATE_SALTWATER, df::biome_type::LAKE_TEMPERATE_SALTWATER }, + { df::plant_raw_flags::BIOME_LAKE_TROPICAL_FRESHWATER, df::biome_type::LAKE_TROPICAL_FRESHWATER }, + { df::plant_raw_flags::BIOME_LAKE_TROPICAL_BRACKISHWATER, df::biome_type::LAKE_TROPICAL_BRACKISHWATER }, + { df::plant_raw_flags::BIOME_LAKE_TROPICAL_SALTWATER, df::biome_type::LAKE_TROPICAL_SALTWATER }, + { df::plant_raw_flags::BIOME_RIVER_TEMPERATE_FRESHWATER, df::biome_type::RIVER_TEMPERATE_FRESHWATER }, + { df::plant_raw_flags::BIOME_RIVER_TEMPERATE_BRACKISHWATER, df::biome_type::RIVER_TEMPERATE_BRACKISHWATER }, + { df::plant_raw_flags::BIOME_RIVER_TEMPERATE_SALTWATER, df::biome_type::RIVER_TEMPERATE_SALTWATER }, + { df::plant_raw_flags::BIOME_RIVER_TROPICAL_FRESHWATER, df::biome_type::RIVER_TROPICAL_FRESHWATER }, + { df::plant_raw_flags::BIOME_RIVER_TROPICAL_BRACKISHWATER, df::biome_type::RIVER_TROPICAL_BRACKISHWATER }, + { df::plant_raw_flags::BIOME_RIVER_TROPICAL_SALTWATER, df::biome_type::RIVER_TROPICAL_SALTWATER }, + { df::plant_raw_flags::BIOME_SUBTERRANEAN_WATER, df::biome_type::SUBTERRANEAN_WATER }, + { df::plant_raw_flags::BIOME_SUBTERRANEAN_CHASM, df::biome_type::SUBTERRANEAN_CHASM }, + { df::plant_raw_flags::BIOME_SUBTERRANEAN_LAVA, df::biome_type::SUBTERRANEAN_LAVA } + }; + + +public: + void find_plantable_plants() + { + plantable_plants.clear(); + + map counts; + + df::item_flags bad_flags; + bad_flags.whole = 0; + +#define F(x) bad_flags.bits.x = true; + F(dump); F(forbid); F(garbage_collect); + F(hostile); F(on_fire); F(rotten); F(trader); + F(in_building); F(construction); F(artifact); +#undef F + + for (auto ii : world->items.other[df::items_other_id::SEEDS]) + { + df::item_plantst* i = (df::item_plantst*)ii; + if ((i->flags.whole & bad_flags.whole) == 0) + counts[i->mat_index] += i->stack_size; + } + + for (auto ci : counts) + { + if (df::global::ui->tasks.discovered_plants[ci.first]) + { + df::plant_raw* plant = world->raws.plants.all[ci.first]; + if (is_plantable(plant)) + for (auto flagmap : biomeFlagMap) + if (plant->flags.is_set(flagmap.first)) + plantable_plants[plant->index].insert(flagmap.second); + } + } + } + + void set_farms(color_ostream& out, set plants, vector farms) + { + // this algorithm attempts to change as few farms as possible, while ensuring that + // the number of farms planting each eligible plant is "as equal as possible" + + if (farms.empty() || plants.empty()) + return; // do nothing if there are no farms or no plantable plants + + int season = *df::global::cur_season; + + int min = farms.size() / plants.size(); // the number of farms that should plant each eligible plant, rounded down + int extra = farms.size() - min * plants.size(); // the remainder that cannot be evenly divided + + map counters; + counters.empty(); + + queue toChange; + toChange.empty(); + + for (auto farm : farms) + { + int o = farm->plant_id[season]; + if (plants.count(o)==0 || counters[o] > min || (counters[o] == min && extra == 0)) + toChange.push(farm); // this farm is an excess instance for the plant it is currently planting + else + { + if (counters[o] == min) + extra--; // allocate off one of the remainder farms + counters[o]++; + } + } + + for (auto n : plants) + { + int c = counters[n]; + while (toChange.size() > 0 && (c < min || (c == min && extra > 0))) + { + // pick one of the excess farms and change it to plant this plant + df::building_farmplotst* farm = toChange.front(); + int o = farm->plant_id[season]; + farm->plant_id[season] = n; + out << "autofarm: changing farm #" << farm->id << + " from " << ((o == -1) ? "NONE" : world->raws.plants.all[o]->name) << + " to " << ((n == -1) ? "NONE" : world->raws.plants.all[n]->name) << endl; + toChange.pop(); + if (c++ == min) + extra--; + } + } + } + + void process(color_ostream& out) + { + if (!enabled) + return; + + find_plantable_plants(); + + lastCounts.clear(); + + df::item_flags bad_flags; + bad_flags.whole = 0; + +#define F(x) bad_flags.bits.x = true; + F(dump); F(forbid); F(garbage_collect); + F(hostile); F(on_fire); F(rotten); F(trader); + F(in_building); F(construction); F(artifact); +#undef F + + for (auto ii : world->items.other[df::items_other_id::PLANT]) + { + df::item_plantst* i = (df::item_plantst*)ii; + if ((i->flags.whole & bad_flags.whole) == 0 && + plantable_plants.count(i->mat_index) > 0) + { + lastCounts[i->mat_index] += i->stack_size; + } + } + + map> plants; + plants.clear(); + + for (auto plantable : plantable_plants) + { + df::plant_raw* plant = world->raws.plants.all[plantable.first]; + if (lastCounts[plant->index] < getThreshold(plant->index)) + for (auto biome : plantable.second) + { + plants[biome].insert(plant->index); + } + } + + map> farms; + farms.clear(); + + for (auto bb : world->buildings.other[df::buildings_other_id::FARM_PLOT]) + { + df::building_farmplotst* farm = (df::building_farmplotst*) bb; + if (farm->flags.bits.exists) + { + df::biome_type biome; + if (Maps::getTileDesignation(bb->centerx, bb->centery, bb->z)->bits.subterranean) + biome = biome_type::SUBTERRANEAN_WATER; + else { + df::coord2d region(Maps::getTileBiomeRgn(df::coord(bb->centerx, bb->centery, bb->z))); + biome = Maps::GetBiomeType(region.x, region.y); + } + farms[biome].push_back(farm); + } + } + + for (auto ff : farms) + { + set_farms(out, plants[ff.first], ff.second); + } + } + + void status(color_ostream& out) + { + out << (enabled ? "Running." : "Stopped.") << endl; + for (auto lc : lastCounts) + { + auto plant = world->raws.plants.all[lc.first]; + out << plant->id << " limit " << getThreshold(lc.first) << " current " << lc.second << endl; + } + + for (auto th : thresholds) + { + if (lastCounts[th.first] > 0) + continue; + auto plant = world->raws.plants.all[th.first]; + out << plant->id << " limit " << getThreshold(th.first) << " current 0" << endl; + } + out << "Default: " << defaultThreshold << endl; + } +}; + +static AutoFarm* autofarmInstance; + + +DFhackCExport command_result plugin_init (color_ostream &out, std::vector &commands) +{ + if (world && ui) { + commands.push_back( + PluginCommand("autofarm", tagline, + autofarm, false, usage + ) + ); + } + autofarmInstance = new AutoFarm(); + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown ( color_ostream &out ) +{ + delete autofarmInstance; + + return CR_OK; +} + +DFhackCExport command_result plugin_onupdate(color_ostream &out) +{ + if (!autofarmInstance) + return CR_OK; + + if (!Maps::IsValid()) + return CR_OK; + + if (DFHack::World::ReadPauseState()) + return CR_OK; + + if (world->frame_counter % 50 != 0) // Check every hour + return CR_OK; + + { + CoreSuspender suspend; + autofarmInstance->process(out); + } + + return CR_OK; +} + +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) +{ + enabled = enable; + return CR_OK; +} + +static command_result setThresholds(color_ostream& out, vector & parameters) +{ + int val = atoi(parameters[1].c_str()); + for (int i = 2; i < parameters.size(); i++) + { + string id = parameters[i]; + transform(id.begin(), id.end(), id.begin(), ::toupper); + + bool ok = false; + for (auto plant : world->raws.plants.all) + { + if (plant->flags.is_set(df::plant_raw_flags::SEED) && (plant->id == id)) + { + autofarmInstance->setThreshold(plant->index, val); + ok = true; + break; + } + } + if (!ok) + { + out << "Cannot find plant with id " << id << endl; + return CR_WRONG_USAGE; + } + } + return CR_OK; +} + +static command_result autofarm(color_ostream &out, vector & parameters) +{ + CoreSuspender suspend; + + if (parameters.size() == 1 && parameters[0] == "runonce") + autofarmInstance->process(out); + else if (parameters.size() == 1 && parameters[0] == "enable") + plugin_enable(out, true); + else if (parameters.size() == 1 && parameters[0] == "disable") + plugin_enable(out, false); + else if (parameters.size() == 2 && parameters[0] == "default") + autofarmInstance->setDefault(atoi(parameters[1].c_str())); + else if (parameters.size() >= 3 && parameters[0] == "threshold") + return setThresholds(out, parameters); + else if (parameters.size() == 0 || parameters.size() == 1 && parameters[0] == "status") + autofarmInstance->status(out); + else + return CR_WRONG_USAGE; + + return CR_OK; +} + diff --git a/plugins/createitem.cpp b/plugins/createitem.cpp index 70794e38c..939afebc2 100644 --- a/plugins/createitem.cpp +++ b/plugins/createitem.cpp @@ -303,7 +303,12 @@ command_result df_createitem (color_ostream &out, vector & parameters) case item_type::PET: case item_type::EGG: split_string(&tokens, material_str, ":"); - if (tokens.size() != 2) + if (tokens.size() == 1) + { + // default to empty caste to display a list of valid castes later + tokens.push_back(""); + } + else if (tokens.size() != 2) { out.printerr("You must specify a creature ID and caste for this item type!\n"); return CR_FAILURE; @@ -311,12 +316,14 @@ command_result df_createitem (color_ostream &out, vector & parameters) for (size_t i = 0; i < world->raws.creatures.all.size(); i++) { + string castes = ""; df::creature_raw *creature = world->raws.creatures.all[i]; if (creature->creature_id == tokens[0]) { for (size_t j = 0; j < creature->caste.size(); j++) { df::caste_raw *caste = creature->caste[j]; + castes += " " + creature->caste[j]->caste_id; if (creature->caste[j]->caste_id == tokens[1]) { mat_type = i; @@ -326,7 +333,15 @@ command_result df_createitem (color_ostream &out, vector & parameters) } if (mat_type == -1) { - out.printerr("The creature you specified has no such caste!\n"); + if (tokens[1].empty()) + { + out.printerr("You must also specify a caste.\n"); + } + else + { + out.printerr("The creature you specified has no such caste!\n"); + } + out.printerr("Valid castes:%s\n", castes.c_str()); return CR_FAILURE; } } diff --git a/plugins/devel/CMakeLists.txt b/plugins/devel/CMakeLists.txt index 975175db2..b6c509116 100644 --- a/plugins/devel/CMakeLists.txt +++ b/plugins/devel/CMakeLists.txt @@ -2,6 +2,8 @@ if(UNIX) dfhack_plugin(vectors vectors.cpp) endif() +include(FindThreads) + add_definitions(-DDEV_PLUGIN) dfhack_plugin(buildprobe buildprobe.cpp) dfhack_plugin(color-dfhack-text color-dfhack-text.cpp) diff --git a/plugins/devel/eventExample.cpp b/plugins/devel/eventExample.cpp index b5f9d34d4..ba9c2a00d 100644 --- a/plugins/devel/eventExample.cpp +++ b/plugins/devel/eventExample.cpp @@ -175,8 +175,15 @@ void unitAttack(color_ostream& out, void* ptr) { EventManager::UnitAttackData* data = (EventManager::UnitAttackData*)ptr; out.print("unit %d attacks unit %d\n", data->attacker, data->defender); df::unit* defender = df::unit::find(data->defender); + if (!defender) { + out.printerr("defender %d does not exist\n", data->defender); + return; + } int32_t woundIndex = df::unit_wound::binsearch_index(defender->body.wounds, data->wound); - df::unit_wound* wound = defender->body.wounds[woundIndex]; + df::unit_wound* wound = vector_get(defender->body.wounds, woundIndex); + if (!wound) { + return; + } set parts; for ( auto a = wound->parts.begin(); a != wound->parts.end(); a++ ) { parts.insert((*a)->body_part_id); diff --git a/plugins/dwarfmonitor.cpp b/plugins/dwarfmonitor.cpp index 29d0198b3..fc1679a19 100644 --- a/plugins/dwarfmonitor.cpp +++ b/plugins/dwarfmonitor.cpp @@ -163,7 +163,6 @@ namespace dm_lua { delete out; out = NULL; } - lua_close(state); } bool init_call (const char *func) { @@ -1873,6 +1872,12 @@ static bool set_monitoring_mode(const string &mode, const bool &state) if (!is_enabled) return false; + /* + NOTE: although we are not touching DF directly but there might be + code running that uses these values. So this could use another mutex + or just suspend the core while we edit our values. + */ + CoreSuspender guard; if (mode == "work" || mode == "all") { @@ -1908,7 +1913,10 @@ static bool load_config() DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (enable) + { + CoreSuspender guard; load_config(); + } if (is_enabled != enable) { if (!INTERPOSE_HOOK(dwarf_monitor_hook, render).apply(enable)) @@ -1963,16 +1971,19 @@ static command_result dwarfmonitor_cmd(color_ostream &out, vector & par } else if (cmd == 's' || cmd == 'S') { + CoreSuspender guard; if(Maps::IsValid()) Screen::show(dts::make_unique(), plugin_self); } else if (cmd == 'p' || cmd == 'P') { + CoreSuspender guard; if(Maps::IsValid()) Screen::show(dts::make_unique(), plugin_self); } else if (cmd == 'r' || cmd == 'R') { + CoreSuspender guard; load_config(); } else @@ -2024,7 +2035,7 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector + #include "Core.h" -#include -#include -#include +#include "Console.h" +#include "Export.h" +#include "PluginManager.h" -#include -#include -#include +#include "modules/Gui.h" +#include "modules/Screen.h" +#include "../uicommon.h" #include "DataDefs.h" #include "df/coord2d.h" @@ -27,6 +29,7 @@ #include "survey.h" DFHACK_PLUGIN("embark-assistant"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); using namespace DFHack; using namespace df::enums; @@ -134,11 +137,43 @@ command_result embark_assistant (color_ostream &out, std::vector & //======================================================================================= +struct start_site_hook : df::viewscreen_choose_start_sitest { + typedef df::viewscreen_choose_start_sitest interpose_base; + + DEFINE_VMETHOD_INTERPOSE(void, render, ()) + { + INTERPOSE_NEXT(render)(); + if (embark_assist::main::state) + return; + auto dims = Screen::getWindowSize(); + int x = 60; + int y = dims.y - 2; + OutputString(COLOR_LIGHTRED, x, y, " " + Screen::getKeyDisplay(interface_key::CUSTOM_A)); + OutputString(COLOR_WHITE, x, y, ": Embark "); + OutputString(COLOR_WHITE, x, y, dims.x > 82 ? "Assistant" : "Asst."); + } + + DEFINE_VMETHOD_INTERPOSE(void, feed, (std::set *input)) + { + if (!embark_assist::main::state && input->count(interface_key::CUSTOM_A)) + { + Core::getInstance().setHotkeyCmd("embark-assistant"); + return; + } + INTERPOSE_NEXT(feed)(input); + } +}; + +IMPLEMENT_VMETHOD_INTERPOSE(start_site_hook, render); +IMPLEMENT_VMETHOD_INTERPOSE(start_site_hook, feed); + +//======================================================================================= + DFhackCExport command_result plugin_init (color_ostream &out, std::vector &commands) { commands.push_back(PluginCommand( "embark-assistant", "Embark site selection support.", - embark_assistant, true, /* true means that the command can't be used from non-interactive user interface */ + embark_assistant, false, /* false means that the command can be used from non-interactive user interface */ // Extended help string. Used by CR_WRONG_USAGE and the help command: " This command starts the embark-assist plugin that provides embark site\n" " selection help. It has to be called while the pre-embark screen is\n" @@ -160,6 +195,22 @@ DFhackCExport command_result plugin_shutdown (color_ostream &out) //======================================================================================= +DFhackCExport command_result plugin_enable (color_ostream &out, bool enable) +{ + if (is_enabled != enable) + { + if (!INTERPOSE_HOOK(start_site_hook, render).apply(enable) || + !INTERPOSE_HOOK(start_site_hook, feed).apply(enable)) + { + return CR_FAILURE; + } + is_enabled = enable; + } + return CR_OK; +} + +//======================================================================================= + DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { switch (event) { @@ -202,8 +253,15 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan command_result embark_assistant(color_ostream &out, std::vector & parameters) { - if (!parameters.empty()) + bool fileresult = false; + + if (parameters.size() == 1 && + parameters[0] == "fileresult") { + remove(fileresult_file_name); + fileresult = true; + } else if (!parameters.empty()) { return CR_WRONG_USAGE; + } CoreSuspender suspend; @@ -226,8 +284,11 @@ command_result embark_assistant(color_ostream &out, std::vector & // Find the end of the normal inorganic definitions. embark_assist::main::state->max_inorganic = 0; - for (uint16_t i = 0; i < world->raws.inorganics.size(); i++) { - if (!world->raws.inorganics[i]->flags.is_set(df::inorganic_flags::GENERATED)) embark_assist::main::state->max_inorganic = i; + for (uint16_t i = world->raws.inorganics.size() - 1; i >= 0 ; i--) { + if (!world->raws.inorganics[i]->flags.is_set(df::inorganic_flags::GENERATED)) { + embark_assist::main::state->max_inorganic = i; + break; + } } embark_assist::main::state->max_inorganic++; // To allow it to be used as size() replacement @@ -297,5 +358,9 @@ command_result embark_assistant(color_ostream &out, std::vector & embark_assist::survey::survey_embark(&mlt, &embark_assist::main::state->survey_results, &embark_assist::main::state->site_info, false); embark_assist::overlay::set_embark(&embark_assist::main::state->site_info); + if (fileresult) { + embark_assist::overlay::fileresult(); + } + return CR_OK; } diff --git a/plugins/embark-assistant/finder_ui.cpp b/plugins/embark-assistant/finder_ui.cpp index 80c61dc6e..bae87aaa2 100644 --- a/plugins/embark-assistant/finder_ui.cpp +++ b/plugins/embark-assistant/finder_ui.cpp @@ -1557,11 +1557,18 @@ namespace embark_assist { // Exported operations //=============================================================================== -void embark_assist::finder_ui::init(DFHack::Plugin *plugin_self, embark_assist::defs::find_callbacks find_callback, uint16_t max_inorganic) { +void embark_assist::finder_ui::init(DFHack::Plugin *plugin_self, embark_assist::defs::find_callbacks find_callback, uint16_t max_inorganic, bool fileresult) { if (!embark_assist::finder_ui::state) { // First call. Have to do the setup embark_assist::finder_ui::ui_setup(find_callback, max_inorganic); } - Screen::show(dts::make_unique(), plugin_self); + if (!fileresult) { + Screen::show(dts::make_unique(), plugin_self); + } + else + { + load_profile(); + find(); + } } //=============================================================================== diff --git a/plugins/embark-assistant/finder_ui.h b/plugins/embark-assistant/finder_ui.h index 70bf4ce42..1011bf251 100644 --- a/plugins/embark-assistant/finder_ui.h +++ b/plugins/embark-assistant/finder_ui.h @@ -10,7 +10,7 @@ using namespace DFHack; namespace embark_assist { namespace finder_ui { - void init(DFHack::Plugin *plugin_self, embark_assist::defs::find_callbacks find_callback, uint16_t max_inorganic); + void init(DFHack::Plugin *plugin_self, embark_assist::defs::find_callbacks find_callback, uint16_t max_inorganic, bool fileresult); void activate(); void shutdown(); } diff --git a/plugins/embark-assistant/help_ui.cpp b/plugins/embark-assistant/help_ui.cpp index ef1bc2b39..ebb1deb78 100644 --- a/plugins/embark-assistant/help_ui.cpp +++ b/plugins/embark-assistant/help_ui.cpp @@ -167,25 +167,25 @@ namespace embark_assist{ help_text.push_back("DF's display of resources in the region DF currently displays. Secondly, the"); help_text.push_back("DF display doesn't take elevation based soil erosion or the magma sea depth"); help_text.push_back("into consideration, so it can display resources that actually are cut away."); - help_text.push_back("Thirdly, it takes 'intrusions', i.e. small sections of neighboring tiles'"); + help_text.push_back("Thirdly, it takes 'incursions', i.e. small sections of neighboring tiles'"); help_text.push_back("biomes into consideration for many fields."); help_text.push_back("(It can be noted that the DFHack Sand indicator does take the first two"); help_text.push_back("elements into account)."); help_text.push_back("The info the Embark Assistant displays is:"); - help_text.push_back("Incompl. Survey if all intrusions couldn't be examined because that requires"); + help_text.push_back("Incompl. Survey if all incursions couldn't be examined because that requires"); help_text.push_back("info from neighboring world tiles that haven't been surveyed."); - help_text.push_back("Sand, if present, including through intrusions."); - help_text.push_back("Clay, if present, including thorugh intrusions."); - help_text.push_back("Min and Max soil depth in the embark rectangle, including intrusions."); - help_text.push_back("Flat indicator if all the tiles and intrusions have the same elevation."); - help_text.push_back("Aquifer indicator: Part(ial) or Full, when present, including intrusions."); + help_text.push_back("Sand, if present, including through incursions."); + help_text.push_back("Clay, if present, including thorugh incursions."); + help_text.push_back("Min and Max soil depth in the embark rectangle, including incursions."); + help_text.push_back("Flat indicator if all the tiles and incursions have the same elevation."); + help_text.push_back("Aquifer indicator: Part(ial) or Full, when present, including incursions."); help_text.push_back("Waterfall and largest Z level drop if the river has elevation differences"); help_text.push_back("Evil weather, when present: BR = Blood Rain, TS = Temporary Syndrome"); - help_text.push_back("PS = Permanent Syndrome, Re = Reanimating, and Th = Thralling. Intrusions."); - help_text.push_back("Flux, if present. NOT allowing for small intrusion bits."); - help_text.push_back("A list of all metals present in the embark. Not intrusions."); + help_text.push_back("PS = Permanent Syndrome, Re = Reanimating, and Th = Thralling. Incursions."); + help_text.push_back("Flux, if present. NOT allowing for small incursion bits."); + help_text.push_back("A list of all metals present in the embark. Not incursions."); help_text.push_back("A list of all economic minerals present in the embark. Both clays and flux"); - help_text.push_back("stones are economic, so they show up here as well. Not intrusions."); + help_text.push_back("stones are economic, so they show up here as well. Not incursions."); help_text.push_back("In addition to the above, the Find functionality can also produce blinking"); help_text.push_back("overlays over the Local, Region, and World maps to indicate where"); help_text.push_back("matching embarks are found. The Local display marks the top left corner of"); @@ -256,7 +256,7 @@ namespace embark_assist{ help_text.push_back("block) at a time, and the results are displayed as green inverted X on"); help_text.push_back("the same map (replacing or erasing the yellow ones). Local map overlay"); help_text.push_back("data is generated as well."); - help_text.push_back("Since 'intrusion' processing requires that the neighboring tiles that may"); + help_text.push_back("Since 'incursion' processing requires that the neighboring tiles that may"); help_text.push_back("provide them are surveyed before the current tile and tiles have to be"); help_text.push_back("surveyed in some order, the find function can not perform a complete"); help_text.push_back("survey of prospective embarks that border world tiles yet to be surveyed"); @@ -266,6 +266,19 @@ namespace embark_assist{ help_text.push_back("ones."); help_text.push_back(""); help_text.push_back("Caveats & technical stuff:"); + help_text.push_back("- The plugin does in fact allow for a single, optional case sensitive"); + help_text.push_back(" parameter when invoked: 'fileresult'. When this parameter is provided"); + help_text.push_back(" The plugin will read the search profile stored to file and immediately"); + help_text.push_back(" initiate a search for matches. This search is performed twice to ensure"); + help_text.push_back(" incursions are handled correctly, and then the number of matching world"); + help_text.push_back(" is written to the file /data/init/embark_assistant_fileresult.txt."); + help_text.push_back(" It can be noted that this file is deleted before the first search is"); + help_text.push_back(" started. The purpose of this mode is to allow external harnesses to"); + help_text.push_back(" generate worlds, search them for matches, and use the file results to"); + help_text.push_back(" to determine which worlds to keep. It can be noted that after the search"); + help_text.push_back(" the plugin continues to work essentially as usual, including external"); + help_text.push_back(" to terminate, and that the author plugin can provide no help when it comes"); + help_text.push_back(" to setting up any kind of harness using the plugin functionality."); help_text.push_back("- The Find searching uses simulated cursor movement input to DF to get it"); help_text.push_back(" to load feature shells and detailed region data, and this costs the"); help_text.push_back(" least when done one feature shell at a time."); @@ -274,14 +287,17 @@ namespace embark_assist{ help_text.push_back(" set of preliminary matches (yellow tiles) than a previous search."); help_text.push_back(" Note that the first search can miss a fair number of matches for"); help_text.push_back(" technical reasons discussed above and below."); + + break; + + case pages::Caveats_2: + Screen::drawBorder(" Embark Assistant Help/Info Caveats 2 Page "); + help_text.push_back("- The site info is deduced by the author, so there may be errors and"); help_text.push_back(" there are probably site types that end up not being identified."); help_text.push_back("- Aquifer indications are based on the author's belief that they occur"); help_text.push_back(" whenever an aquifer supporting layer is present at a depth of 3 or"); help_text.push_back(" more."); - help_text.push_back("- The biome determination logic comes from code provided by Ragundo,"); - help_text.push_back(" with only marginal changes by the author. References can be found in"); - help_text.push_back(" the source file."); help_text.push_back("- Thralling is determined by whether material interactions causes"); help_text.push_back(" blinking, which the author believes is one of 4 thralling changes."); help_text.push_back("- The geo information is gathered by code which is essentially a"); @@ -293,12 +309,6 @@ namespace embark_assist{ help_text.push_back(" reaching caverns that have been removed at world gen to fail to be"); help_text.push_back(" generated at all. It's likely this bug also affects magma pools."); help_text.push_back(" This plugin does not address this but scripts can correct it."); - - break; - - case pages::Caveats_2: - Screen::drawBorder(" Embark Assistant Help/Info Caveats 2 Page "); - help_text.push_back("- The plugin detects 'incursions' of neighboring tiles into embarks, but"); help_text.push_back(" this functionality is incomplete when the incursion comes from a"); help_text.push_back(" neighboring tile that hasn't been surveyed yet. The embark info displays"); @@ -314,9 +324,9 @@ namespace embark_assist{ help_text.push_back(" economics/minerals (including Flux and Coal) as any volumes are typically"); help_text.push_back(" too small to be of interest. Rivers, Waterfalls, Spires, and Magma Pools"); help_text.push_back(" are not incursion related features."); - help_text.push_back("- There are special rules for handing of intrusions from Lakes and Oceans,"); + help_text.push_back("- There are special rules for handing of incursions from Lakes and Oceans,"); help_text.push_back(" as well as Mountains into everything that isn't a Lake or Ocean, and the"); - help_text.push_back(" rules state that these intrusions should be reversed (i.e. 'normal' biomes"); + help_text.push_back(" rules state that these incursions should be reversed (i.e. 'normal' biomes"); help_text.push_back(" should push into Lakes, Oceans, and Mountains, even when the indicators"); help_text.push_back(" say otherwise). This rule is clear for edges, but not for corners, as it"); help_text.push_back(" does not specify which of the potentially multiple 'superior' biomes"); @@ -325,7 +335,7 @@ namespace embark_assist{ help_text.push_back(" the N, followed by the one to the W, and lastly the one acting as the"); help_text.push_back(" reference. This means there's a risk embarks with such 'trouble' corners"); help_text.push_back(" may get affected corner(s) evaluated incorrectly."); - help_text.push_back("Version 0.9 2019-07-12"); + help_text.push_back("Version 0.10 2019-09-21"); break; } diff --git a/plugins/embark-assistant/matcher.cpp b/plugins/embark-assistant/matcher.cpp index a3975a980..106cc6090 100644 --- a/plugins/embark-assistant/matcher.cpp +++ b/plugins/embark-assistant/matcher.cpp @@ -2,6 +2,7 @@ #include +#include "Core.h" #include "DataDefs.h" #include "df/biome_type.h" #include "df/inorganic_raw.h" @@ -176,8 +177,8 @@ namespace embark_assist { result->sand_found = true; } - // Flux. N/A for intrusions. - // Coal. N/A for intrusions + // Flux. N/A for incursions. + // Coal. N/A for incursions // Min Soil if (finder->soil_min != embark_assist::defs::soil_ranges::NA && @@ -270,8 +271,8 @@ namespace embark_assist { result->thralling_found = true; } - // Spires. N/A for intrusions - // Magma. N/A for intrusions + // Spires. N/A for incursions + // Magma. N/A for incursions // Biomes result->biomes[survey_results->at(x).at(y).biome[mlt->biome_offset]] = true; @@ -279,8 +280,8 @@ namespace embark_assist { // Region Type result->region_types[world_data->regions[survey_results->at(x).at(y).biome_index[mlt->biome_offset]]->type] = true; - // Metals. N/A for intrusions - // Economics. N/A for intrusions + // Metals. N/A for incursions + // Economics. N/A for incursions } //======================================================================================= @@ -2076,7 +2077,7 @@ namespace embark_assist { uint32_t preliminary_world_match(embark_assist::defs::world_tile_data *survey_results, embark_assist::defs::finders *finder, embark_assist::defs::match_results *match_results) { - // color_ostream_proxy out(Core::getInstance().getConsole()); +// color_ostream_proxy out(Core::getInstance().getConsole()); uint32_t count = 0; for (uint16_t i = 0; i < world->worldgen.worldgen_parms.dim_x; i++) { for (uint16_t k = 0; k < world->worldgen.worldgen_parms.dim_y; k++) { @@ -2299,6 +2300,8 @@ uint16_t embark_assist::matcher::find(embark_assist::defs::match_iterators *iter while (screen->location.region_pos.x != 0 || screen->location.region_pos.y != 0) { screen->feed_key(df::interface_key::CURSOR_UPLEFT_FAST); } + iterator->target_location_x = 0; + iterator->target_location_y = 0; iterator->active = true; iterator->i = 0; iterator->k = 0; @@ -2327,21 +2330,24 @@ uint16_t embark_assist::matcher::find(embark_assist::defs::match_iterators *iter for (uint16_t l = 0; l <= x_end; l++) { for (uint16_t m = 0; m <= y_end; m++) { // This is where the payload goes - if (match_results->at(screen->location.region_pos.x).at(screen->location.region_pos.y).preliminary_match) { + if (!survey_results->at(iterator->target_location_x).at(iterator->target_location_y).surveyed || + match_results->at(iterator->target_location_x).at(iterator->target_location_y).preliminary_match) { + move_cursor(iterator->target_location_x, iterator->target_location_y); + match_world_tile(geo_summary, survey_results, &iterator->finder, match_results, - screen->location.region_pos.x, - screen->location.region_pos.y); - if (match_results->at(screen->location.region_pos.x).at(screen->location.region_pos.y).contains_match) { + iterator->target_location_x, + iterator->target_location_y); + if (match_results->at(iterator->target_location_x).at(iterator->target_location_y).contains_match) { iterator->count++; } } else { for (uint16_t n = 0; n < 16; n++) { for (uint16_t p = 0; p < 16; p++) { - match_results->at(screen->location.region_pos.x).at(screen->location.region_pos.y).mlt_match[n][p] = false; + match_results->at(iterator->target_location_x).at(iterator->target_location_y).mlt_match[n][p] = false; } } } @@ -2349,15 +2355,15 @@ uint16_t embark_assist::matcher::find(embark_assist::defs::match_iterators *iter if (m != y_end) { if (iterator->y_down) { - screen->feed_key(df::interface_key::CURSOR_DOWN); + if (iterator->target_location_y < world->worldgen.worldgen_parms.dim_y - 1) iterator->target_location_y++; } else { - screen->feed_key(df::interface_key::CURSOR_UP); + if (iterator->target_location_y > 0) iterator->target_location_y--; } } else { - if (screen->location.region_pos.x != 0 && - screen->location.region_pos.x != world->worldgen.worldgen_parms.dim_x - 1) { + if (iterator->target_location_x != 0 && + iterator->target_location_x != world->worldgen.worldgen_parms.dim_x - 1) { turn = true; } else { @@ -2370,24 +2376,24 @@ uint16_t embark_assist::matcher::find(embark_assist::defs::match_iterators *iter } else { if (iterator->y_down) { - screen->feed_key(df::interface_key::CURSOR_DOWN); + if (iterator->target_location_y < world->worldgen.worldgen_parms.dim_y - 1) iterator->target_location_y++; } else { - screen->feed_key(df::interface_key::CURSOR_UP); + if (iterator->target_location_y > 0) iterator->target_location_y--; } } } } if (iterator->x_right) { // Won't do anything at the edge, so we don't bother filter those cases. - screen->feed_key(df::interface_key::CURSOR_RIGHT); + if (iterator->target_location_x < world->worldgen.worldgen_parms.dim_x - 1) iterator->target_location_x++; } else { - screen->feed_key(df::interface_key::CURSOR_LEFT); + if (iterator->target_location_x > 0) iterator->target_location_x--; } if (!iterator->x_right && - screen->location.region_pos.x == 0) { + iterator->target_location_x == 0) { turn = !turn; if (turn) { @@ -2395,7 +2401,7 @@ uint16_t embark_assist::matcher::find(embark_assist::defs::match_iterators *iter } } else if (iterator->x_right && - screen->location.region_pos.x == world->worldgen.worldgen_parms.dim_x - 1) { + iterator->target_location_x == world->worldgen.worldgen_parms.dim_x - 1) { turn = !turn; if (turn) { diff --git a/plugins/embark-assistant/overlay.cpp b/plugins/embark-assistant/overlay.cpp index a0defd0d5..c689f21ad 100644 --- a/plugins/embark-assistant/overlay.cpp +++ b/plugins/embark-assistant/overlay.cpp @@ -54,6 +54,9 @@ namespace embark_assist { uint16_t match_count = 0; uint16_t max_inorganic; + + bool fileresult = false; + uint8_t fileresult_pass = 0; }; static states *state = nullptr; @@ -113,7 +116,7 @@ namespace embark_assist { } else if (input->count(df::interface_key::CUSTOM_F)) { if (!state->match_active && !state->matching) { - embark_assist::finder_ui::init(embark_assist::overlay::plugin_self, state->find_callback, state->max_inorganic); + embark_assist::finder_ui::init(embark_assist::overlay::plugin_self, state->find_callback, state->max_inorganic, false); } } else if (input->count(df::interface_key::CUSTOM_I)) { @@ -311,6 +314,7 @@ void embark_assist::overlay::match_progress(uint16_t count, embark_assist::defs: // color_ostream_proxy out(Core::getInstance().getConsole()); state->matching = !done; state->match_count = count; + for (uint16_t i = 0; i < world->worldgen.worldgen_parms.dim_x; i++) { for (uint16_t k = 0; k < world->worldgen.worldgen_parms.dim_y; k++) { if (match_results->at(i).at(k).preliminary_match) { @@ -324,6 +328,18 @@ void embark_assist::overlay::match_progress(uint16_t count, embark_assist::defs: } } } + + if (done && state->fileresult) { + state->fileresult_pass++; + if (state->fileresult_pass == 1) { + embark_assist::finder_ui::init(embark_assist::overlay::plugin_self, state->find_callback, state->max_inorganic, true); + } + else { + FILE* outfile = fopen(fileresult_file_name, "w"); + fprintf(outfile, "%i\n", count); + fclose(outfile); + } + } } //==================================================================== @@ -463,6 +479,14 @@ void embark_assist::overlay::clear_match_results() { //==================================================================== +void embark_assist::overlay::fileresult() { + // Have to search twice, as the first pass cannot be complete due to mutual dependencies. + state->fileresult = true; + embark_assist::finder_ui::init(embark_assist::overlay::plugin_self, state->find_callback, state->max_inorganic, true); +} + +//==================================================================== + void embark_assist::overlay::shutdown() { if (state && state->world_match_grid) { diff --git a/plugins/embark-assistant/overlay.h b/plugins/embark-assistant/overlay.h index d66d6d7fd..f4f7cf3eb 100644 --- a/plugins/embark-assistant/overlay.h +++ b/plugins/embark-assistant/overlay.h @@ -32,6 +32,7 @@ namespace embark_assist { void set_embark(embark_assist::defs::site_infos *site_info); void set_mid_level_tile_match(embark_assist::defs::mlt_matches mlt_matches); void clear_match_results(); + void fileresult(); void shutdown(); } } \ No newline at end of file diff --git a/plugins/embark-assistant/survey.cpp b/plugins/embark-assistant/survey.cpp index 3574219bc..a2fb648f2 100644 --- a/plugins/embark-assistant/survey.cpp +++ b/plugins/embark-assistant/survey.cpp @@ -1043,11 +1043,11 @@ void embark_assist::survey::survey_mid_level_tile(embark_assist::defs::geo_data mlt->at(i).at(k).river_present = false; mlt->at(i).at(k).river_elevation = 100; - if (details->rivers_vertical.active[i][k] == 1) { + if (details->rivers_vertical.active[i][k] != 0) { mlt->at(i).at(k).river_present = true; mlt->at(i).at(k).river_elevation = details->rivers_vertical.elevation[i][k]; } - else if (details->rivers_horizontal.active[i][k] == 1) { + else if (details->rivers_horizontal.active[i][k] != 0) { mlt->at(i).at(k).river_present = true; mlt->at(i).at(k).river_elevation = details->rivers_horizontal.elevation[i][k]; } @@ -1179,6 +1179,51 @@ void embark_assist::survey::survey_mid_level_tile(embark_assist::defs::geo_data } } + // This is messy. DF has some weird logic to leave out river bends with a South and an East connection, as well + // as river sources (and presumably sinks) that are to the North or the West of the connecting river. + // Experiments indicate these implicit river bends inherit their River Elevation from the lower of the two + // "parents", and it's assumed river sources and sinks similarly inherit it from their sole "parent". + // Two issues are known: + // - Lake and Ocean tiles may be marked as having a river when DF doesn't. However, DF does allow for rivers to + // exist in Ocean/Lake tiles, as well as sources/sinks. + // - DF generates rivers on/under glaciers, but does not display them (as they're frozen), nor are their names + // displayed. + // + for (uint8_t i = 1; i < 16; i++) { + for (uint8_t k = 0; k < 15; k++) { + if (details->rivers_horizontal.active[i][k] != 0 && + details->rivers_vertical.active[i - 1][k + 1] != 0 && + !mlt->at(i - 1).at(k).river_present) { // Probably never true + mlt->at(i - 1).at(k).river_present = true; + mlt->at(i - 1).at(k).river_elevation = mlt->at(i).at(k).river_elevation; + + if (mlt->at(i - 1).at(k).river_elevation > mlt->at(i - 1).at(k + 1).river_elevation) { + mlt->at(i - 1).at(k).river_elevation = mlt->at(i - 1).at(k + 1).river_elevation; + } + } + } + } + + for (uint8_t i = 0; i < 16; i++) { + for (uint8_t k = 1; k < 16; k++) { + if (details->rivers_vertical.active[i][k] != 0 && + !mlt->at(i).at(k - 1).river_present) { + mlt->at(i).at(k - 1).river_present = true; + mlt->at(i).at(k - 1).river_elevation = mlt->at(i).at(k).river_elevation; + } + } + } + + for (uint8_t i = 1; i < 16; i++) { + for (uint8_t k = 0; k < 16; k++) { + if (details->rivers_horizontal.active[i][k] != 0 && + !mlt->at(i - 1).at(k).river_present) { + mlt->at(i - 1).at(k).river_present = true; + mlt->at(i - 1).at(k).river_elevation = mlt->at(i).at(k).river_elevation; + } + } + } + survey_results->at(x).at(y).aquifer_count = 0; survey_results->at(x).at(y).clay_count = 0; survey_results->at(x).at(y).sand_count = 0; @@ -1366,7 +1411,7 @@ void embark_assist::survey::survey_mid_level_tile(embark_assist::defs::geo_data for (uint8_t i = 0; i < 16; i++) { for (uint8_t k = 0; k < 16; k++) { - tile->region_type[i][k] = world_data->regions[tile->biome[mlt->at(i).at(k).biome_offset]]->type; + tile->region_type[i][k] = world_data->regions[tile->biome_index[mlt->at(i).at(k).biome_offset]]->type; } } diff --git a/plugins/getplants.cpp b/plugins/getplants.cpp index 2cf382d01..ebd64d8d1 100644 --- a/plugins/getplants.cpp +++ b/plugins/getplants.cpp @@ -1,5 +1,4 @@ // (un)designate matching plants for gathering/cutting - #include #include "Core.h" @@ -11,12 +10,14 @@ #include "df/map_block.h" #include "df/plant.h" +#include "df/plant_growth.h" #include "df/plant_raw.h" #include "df/tile_dig_designation.h" #include "df/world.h" #include "modules/Designations.h" #include "modules/Maps.h" +#include "modules/Materials.h" using std::string; using std::vector; @@ -27,15 +28,136 @@ using namespace df::enums; DFHACK_PLUGIN("getplants"); REQUIRE_GLOBAL(world); +REQUIRE_GLOBAL(cur_year_tick); + +enum class selectability { + Selectable, + Grass, + Nonselectable, + OutOfSeason, + Unselected +}; + +//selectability selectablePlant(color_ostream &out, const df::plant_raw *plant) +selectability selectablePlant(const df::plant_raw *plant) +{ + const DFHack::MaterialInfo basic_mat = DFHack::MaterialInfo(plant->material_defs.type_basic_mat, plant->material_defs.idx_basic_mat); + bool outOfSeason = false; + + if (plant->flags.is_set(plant_raw_flags::TREE)) + { +// out.print("%s is a selectable tree\n", plant->id.c_str()); + return selectability::Selectable; + } + else if (plant->flags.is_set(plant_raw_flags::GRASS)) + { +// out.print("%s is a non selectable Grass\n", plant->id.c_str()); + return selectability::Grass; + } + + if (basic_mat.material->flags.is_set(material_flags::EDIBLE_RAW) || + basic_mat.material->flags.is_set(material_flags::EDIBLE_COOKED)) + { +// out.print("%s is edible\n", plant->id.c_str()); + return selectability::Selectable; + } + + if (plant->flags.is_set(plant_raw_flags::THREAD) || + plant->flags.is_set(plant_raw_flags::MILL) || + plant->flags.is_set(plant_raw_flags::EXTRACT_VIAL) || + plant->flags.is_set(plant_raw_flags::EXTRACT_BARREL) || + plant->flags.is_set(plant_raw_flags::EXTRACT_STILL_VIAL)) + { +// out.print("%s is thread/mill/extract\n", plant->id.c_str()); + return selectability::Selectable; + } + + if (basic_mat.material->reaction_product.id.size() > 0 || + basic_mat.material->reaction_class.size() > 0) + { +// out.print("%s has a reaction\n", plant->id.c_str()); + return selectability::Selectable; + } + + for (size_t i = 0; i < plant->growths.size(); i++) + { + if (plant->growths[i]->item_type == df::item_type::SEEDS || // Only trees have seed growths in vanilla, but raws can be modded... + plant->growths[i]->item_type == df::item_type::PLANT_GROWTH) + { + const DFHack::MaterialInfo growth_mat = DFHack::MaterialInfo(plant->growths[i]->mat_type, plant->growths[i]->mat_index); + if ((plant->growths[i]->item_type == df::item_type::SEEDS && + (growth_mat.material->flags.is_set(material_flags::EDIBLE_COOKED) || + growth_mat.material->flags.is_set(material_flags::EDIBLE_RAW))) || + (plant->growths[i]->item_type == df::item_type::PLANT_GROWTH && + growth_mat.material->flags.is_set(material_flags::LEAF_MAT))) // Will change name to STOCKPILE_PLANT_GROWTH any day now... + { + if (*cur_year_tick >= plant->growths[i]->timing_1 && + (plant->growths[i]->timing_2 == -1 || + *cur_year_tick <= plant->growths[i]->timing_2)) + { +// out.print("%s has an edible seed or a stockpile growth\n", plant->id.c_str()); + return selectability::Selectable; + } + else + { + outOfSeason = true; + } + } + } +/* else if (plant->growths[i]->behavior.bits.has_seed) // This code designates beans, etc. when DF doesn't, but plant gatherers still fail to collect anything, so it's useless: bug #0006940. + { + const DFHack::MaterialInfo seed_mat = DFHack::MaterialInfo(plant->material_defs.type_seed, plant->material_defs.idx_seed); + + if (seed_mat.material->flags.is_set(material_flags::EDIBLE_RAW) || + seed_mat.material->flags.is_set(material_flags::EDIBLE_COOKED)) + { + if (*cur_year_tick >= plant->growths[i]->timing_1 && + (plant->growths[i]->timing_2 == -1 || + *cur_year_tick <= plant->growths[i]->timing_2)) + { + return selectability::Selectable; + } + else + { + outOfSeason = true; + } + } + } */ + } + + if (outOfSeason) + { +// out.print("%s has an out of season growth\n", plant->id.c_str()); + return selectability::OutOfSeason; + } + else + { +// out.printerr("%s cannot be gathered\n", plant->id.c_str()); + return selectability::Nonselectable; + } +} command_result df_getplants (color_ostream &out, vector & parameters) { string plantMatStr = ""; - set plantIDs; + std::vector plantSelections; + std::vector collectionCount; set plantNames; - bool deselect = false, exclude = false, treesonly = false, shrubsonly = false, all = false; + bool deselect = false, exclude = false, treesonly = false, shrubsonly = false, all = false, verbose = false; int count = 0; + + plantSelections.resize(world->raws.plants.all.size()); + collectionCount.resize(world->raws.plants.all.size()); + + for (size_t i = 0; i < plantSelections.size(); i++) + { + plantSelections[i] = selectability::Unselected; + collectionCount[i] = 0; + } + + bool anyPlantsSelected = false; + for (size_t i = 0; i < parameters.size(); i++) { if(parameters[i] == "help" || parameters[i] == "?") @@ -50,6 +172,8 @@ command_result df_getplants (color_ostream &out, vector & parameters) exclude = true; else if(parameters[i] == "-a") all = true; + else if(parameters[i] == "-v") + verbose = true; else plantNames.insert(parameters[i]); } @@ -75,11 +199,35 @@ command_result df_getplants (color_ostream &out, vector & parameters) { df::plant_raw *plant = world->raws.plants.all[i]; if (all) - plantIDs.insert(i); - else if (plantNames.find(plant->id) != plantNames.end()) + { +// plantSelections[i] = selectablePlant(out, plant); + plantSelections[i] = selectablePlant(plant); + } + else if (plantNames.find(plant->id) != plantNames.end()) { plantNames.erase(plant->id); - plantIDs.insert(i); +// plantSelections[i] = selectablePlant(out, plant); + plantSelections[i] = selectablePlant(plant); + switch (plantSelections[i]) + { + case selectability::Grass: + out.printerr("%s is a grass and cannot be gathered\n", plant->id.c_str()); + break; + + case selectability::Nonselectable: + out.printerr("%s does not have any parts that can be gathered\n", plant->id.c_str()); + break; + + case selectability::OutOfSeason: + out.printerr("%s is out of season, with nothing that can be gathered now\n", plant->id.c_str()); + break; + + case selectability::Selectable: + break; + + case selectability::Unselected: + break; // We won't get to this option + } } } if (plantNames.size() > 0) @@ -91,15 +239,44 @@ command_result df_getplants (color_ostream &out, vector & parameters) return CR_FAILURE; } - if (plantIDs.size() == 0) + for (size_t i = 0; i < plantSelections.size(); i++) + { + if (plantSelections[i] == selectability::OutOfSeason || + plantSelections[i] == selectability::Selectable) + { + anyPlantsSelected = true; + break; + } + } + + if (!anyPlantsSelected) { out.print("Valid plant IDs:\n"); for (size_t i = 0; i < world->raws.plants.all.size(); i++) { df::plant_raw *plant = world->raws.plants.all[i]; - if (plant->flags.is_set(plant_raw_flags::GRASS)) +// switch (selectablePlant(out, plant)) + switch (selectablePlant(plant)) + { + case selectability::Grass: + case selectability::Nonselectable: continue; - out.print("* (%s) %s - %s\n", plant->flags.is_set(plant_raw_flags::TREE) ? "tree" : "shrub", plant->id.c_str(), plant->name.c_str()); + + case selectability::OutOfSeason: + { + out.print("* (shrub) %s - %s is out of season\n", plant->id.c_str(), plant->name.c_str()); + break; + } + + case selectability::Selectable: + { + out.print("* (%s) %s - %s\n", plant->flags.is_set(plant_raw_flags::TREE) ? "tree" : "shrub", plant->id.c_str(), plant->name.c_str()); + break; + } + + case selectability::Unselected: // Should never get this alternative + break; + } } return CR_OK; } @@ -113,9 +290,11 @@ command_result df_getplants (color_ostream &out, vector & parameters) int x = plant->pos.x % 16; int y = plant->pos.y % 16; - if (plantIDs.find(plant->material) != plantIDs.end()) + if (plantSelections[plant->material] == selectability::OutOfSeason || + plantSelections[plant->material] == selectability::Selectable) { - if (exclude) + if (exclude || + plantSelections[plant->material] == selectability::OutOfSeason) continue; } else @@ -134,15 +313,30 @@ command_result df_getplants (color_ostream &out, vector & parameters) continue; if (deselect && Designations::unmarkPlant(plant)) { + collectionCount[plant->material]++; ++count; } if (!deselect && Designations::markPlant(plant)) { +// out.print("Designated %s at (%i, %i, %i), %d\n", world->raws.plants.all[plant->material]->id.c_str(), plant->pos.x, plant->pos.y, plant->pos.z, (int)i); + collectionCount[plant->material]++; ++count; } } if (count) - out.print("Updated %d plant designations.\n", count); + { + if (verbose) + { + for (size_t i = 0; i < plantSelections.size(); i++) + { + if (collectionCount[i] > 0) + out.print("Updated %d %s designations.\n", (int)collectionCount[i], world->raws.plants.all[i]->id.c_str()); + } + out.print("\n"); + } + } + out.print("Updated %d plant designations.\n", (int)count); + return CR_OK; } @@ -154,11 +348,12 @@ DFhackCExport command_result plugin_init ( color_ostream &out, vector #include @@ -1670,9 +1670,9 @@ void GetWounds(df::unit_wound * wound, UnitWound * send_wound) static command_result GetUnitListInside(color_ostream &stream, const BlockRequest *in, UnitList *out) { auto world = df::global::world; - for (size_t i = 0; i < world->units.all.size(); i++) + for (size_t i = 0; i < world->units.active.size(); i++) { - df::unit * unit = world->units.all[i]; + df::unit * unit = world->units.active[i]; auto send_unit = out->add_creature_list(); send_unit->set_id(unit->id); send_unit->set_pos_x(unit->pos.x); @@ -1734,6 +1734,8 @@ static command_result GetUnitListInside(color_ostream &stream, const BlockReques appearance->add_colors(unit->appearance.colors[j]); appearance->set_size_modifier(unit->appearance.size_modifier); + appearance->set_physical_description(Units::getPhysicalDescription(unit)); + send_unit->set_profession_id(unit->profession); std::vector pvec; diff --git a/plugins/stockpiles/StockpileSerializer.cpp b/plugins/stockpiles/StockpileSerializer.cpp index ef8c557c3..cdeced990 100644 --- a/plugins/stockpiles/StockpileSerializer.cpp +++ b/plugins/stockpiles/StockpileSerializer.cpp @@ -30,6 +30,8 @@ // protobuf #include +#include + using std::endl; using namespace DFHack; using namespace df::enums; diff --git a/plugins/stonesense b/plugins/stonesense index 03e96477c..4fdb2be54 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 03e96477ca84e42c87db93bd2d781c73687795a8 +Subproject commit 4fdb2be54365442b8abea86f21746795f83fbdc2 diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp new file mode 100644 index 000000000..4ab6dc4f7 --- /dev/null +++ b/plugins/tailor.cpp @@ -0,0 +1,510 @@ +/* + * Tailor plugin. Automatically manages keeping your dorfs clothed. + * For best effect, place "tailor enable" in your dfhack.init configuration, + * or set AUTOENABLE to true. + */ + +#include "Core.h" +#include "DataDefs.h" +#include "PluginManager.h" + +#include "df/creature_raw.h" +#include "df/global_objects.h" +#include "df/historical_entity.h" +#include "df/itemdef_armorst.h" +#include "df/itemdef_glovesst.h" +#include "df/itemdef_helmst.h" +#include "df/itemdef_pantsst.h" +#include "df/itemdef_shoesst.h" +#include "df/items_other_id.h" +#include "df/job.h" +#include "df/job_type.h" +#include "df/manager_order.h" +#include "df/ui.h" +#include "df/world.h" + +#include "modules/Maps.h" +#include "modules/Units.h" +#include "modules/Translation.h" +#include "modules/World.h" + +using namespace DFHack; +using namespace std; + +using df::global::world; +using df::global::ui; + +DFHACK_PLUGIN("tailor"); +#define AUTOENABLE false +DFHACK_PLUGIN_IS_ENABLED(enabled); + +REQUIRE_GLOBAL(world); +REQUIRE_GLOBAL(ui); + +const char *tagline = "Allow the bookkeeper to queue jobs to keep dwarfs in adequate clothing."; +const char *usage = ( + " tailor enable\n" + " Enable the plugin.\n" + " tailor disable\n" + " Disable the plugin.\n" + " tailor status\n" + " Display plugin status\n" + "\n" + "Whenever the bookkeeper updates stockpile records, this plugin will scan every unit in the fort,\n" + "count up the number that are worn, and then order enough more made to replace all worn items.\n" + "If there are enough replacement items in inventory to replace all worn items, the units wearing them\n" + "will have the worn items confiscated (in the same manner as the _cleanowned_ plugin) so that they'll\n" + "reeequip with replacement items.\n" +); + +// ARMOR, SHOES, HELM, GLOVES, PANTS + +// ah, if only STL had a bimap + +static map jobTypeMap = { + { df::job_type::MakeArmor, df::item_type::ARMOR }, + { df::job_type::MakePants, df::item_type::PANTS }, + { df::job_type::MakeHelm, df::item_type::HELM }, + { df::job_type::MakeGloves, df::item_type::GLOVES }, + { df::job_type::MakeShoes, df::item_type::SHOES } +}; + +static map itemTypeMap = { + { df::item_type::ARMOR, df::job_type::MakeArmor }, + { df::item_type::PANTS, df::job_type::MakePants }, + { df::item_type::HELM, df::job_type::MakeHelm}, + { df::item_type::GLOVES, df::job_type::MakeGloves}, + { df::item_type::SHOES, df::job_type::MakeShoes} +}; + +void do_scan(color_ostream& out) +{ + map, int> available; // key is item type & size + map, int> needed; // same + map, int> queued; // same + + map sizes; // this maps body size to races + + map, int> orders; // key is item type, item subtype, size + + df::item_flags bad_flags; + bad_flags.whole = 0; + +#define F(x) bad_flags.bits.x = true; + F(dump); F(forbid); F(garbage_collect); + F(hostile); F(on_fire); F(rotten); F(trader); + F(in_building); F(construction); F(owned); +#undef F + + available.empty(); + needed.empty(); + queued.empty(); + orders.empty(); + + int silk = 0, yarn = 0, cloth = 0, leather = 0; + + // scan for useable clothing + + for (auto i : world->items.other[df::items_other_id::ANY_GENERIC37]) // GENERIC37 is "clothing" + { + if (i->flags.whole & bad_flags.whole) + continue; + if (i->flags.bits.owned) + continue; + if (i->getWear() >= 1) + continue; + df::item_type t = i->getType(); + int size = world->raws.creatures.all[i->getMakerRace()]->adultsize; + + available[make_pair(t, size)] += 1; + } + + // scan for clothing raw materials + + for (auto i : world->items.other[df::items_other_id::CLOTH]) + { + if (i->flags.whole & bad_flags.whole) + continue; + if (!i->hasImprovements()) // only count dyed + continue; + MaterialInfo mat(i); + int ss = i->getStackSize(); + + if (mat.material) + { + if (mat.material->flags.is_set(df::material_flags::SILK)) + silk += ss; + else if (mat.material->flags.is_set(df::material_flags::THREAD_PLANT)) + cloth += ss; + else if (mat.material->flags.is_set(df::material_flags::YARN)) + yarn += ss; + } + } + + for (auto i : world->items.other[df::items_other_id::SKIN_TANNED]) + { + if (i->flags.whole & bad_flags.whole) + continue; + leather += i->getStackSize(); + } + + out.print("available: silk %d yarn %d cloth %d leather %d\n", silk, yarn, cloth, leather); + + // scan for units who need replacement clothing + + for (auto u : world->units.active) + { + if (!Units::isOwnCiv(u) || + !Units::isOwnGroup(u) || + !Units::isActive(u) || + Units::isBaby(u)) + continue; // skip units we don't control + + set wearing; + wearing.empty(); + + deque worn; + worn.empty(); + + for (auto inv : u->inventory) + { + if (inv->mode != df::unit_inventory_item::Worn) + continue; + if (inv->item->getWear() > 0) + worn.push_back(inv->item); + else + wearing.insert(inv->item->getType()); + } + + int size = world->raws.creatures.all[u->race]->adultsize; + sizes[size] = u->race; + + for (auto ty : set{ df::item_type::ARMOR, df::item_type::PANTS, df::item_type::SHOES }) + { + if (wearing.count(ty) == 0) + needed[make_pair(ty, size)] += 1; + } + + for (auto w : worn) + { + auto ty = w->getType(); + auto oo = itemTypeMap.find(ty); + if (oo == itemTypeMap.end()) + continue; + df::job_type o = oo->second; + + int size = world->raws.creatures.all[w->getMakerRace()]->adultsize; + std::string description; + w->getItemDescription(&description, 0); + + if (available[make_pair(ty, size)] > 0) + { + if (w->flags.bits.owned) + { + bool confiscated = Items::setOwner(w, NULL); + + out.print( + "%s %s from %s.\n", + (confiscated ? "Confiscated" : "Could not confiscate"), + description.c_str(), + Translation::TranslateName(&u->name, false).c_str() + ); + } + + if (wearing.count(ty) == 0) + available[make_pair(ty, size)] -= 1; + + if (w->getWear() > 1) + w->flags.bits.dump = true; + } + else + { +// out.print("%s worn by %s needs replacement\n", +// description.c_str(), +// Translation::TranslateName(&u->name, false).c_str() +// ); + orders[make_tuple(o, w->getSubtype(), size)] += 1; + } + } + } + + auto entity = world->entities.all[ui->civ_id]; + + for (auto a : needed) + { + df::item_type ty = a.first.first; + int size = a.first.second; + int count = a.second; + + int sub = 0; + vector v; + + switch (ty) { + case df::item_type::ARMOR: v = entity->resources.armor_type; break; + case df::item_type::GLOVES: v = entity->resources.gloves_type; break; + case df::item_type::HELM: v = entity->resources.helm_type; break; + case df::item_type::PANTS: v = entity->resources.pants_type; break; + case df::item_type::SHOES: v = entity->resources.shoes_type; break; + default: break; + } + + for (auto vv : v) { + bool isClothing = false; + switch (ty) { + case df::item_type::ARMOR: isClothing = world->raws.itemdefs.armor[vv] ->armorlevel == 0; break; + case df::item_type::GLOVES: isClothing = world->raws.itemdefs.gloves[vv]->armorlevel == 0; break; + case df::item_type::HELM: isClothing = world->raws.itemdefs.helms[vv] ->armorlevel == 0; break; + case df::item_type::PANTS: isClothing = world->raws.itemdefs.pants[vv] ->armorlevel == 0; break; + case df::item_type::SHOES: isClothing = world->raws.itemdefs.shoes[vv] ->armorlevel == 0; break; + default: break; + } + if (isClothing) + { + sub = vv; + break; + } + } + + orders[make_tuple(itemTypeMap[ty], sub, size)] += count; + } + + // scan orders + + for (auto o : world->manager_orders) + { + auto f = jobTypeMap.find(o->job_type); + if (f == jobTypeMap.end()) + continue; + + auto sub = o->item_subtype; + int race = o->hist_figure_id; + if (race == -1) + continue; // -1 means that the race of the worker will determine the size made; we must ignore these jobs + + int size = world->raws.creatures.all[race]->adultsize; + + orders[make_tuple(o->job_type, sub, size)] -= o->amount_left; + } + + // place orders + + for (auto o : orders) + { + df::job_type ty; + int sub; + int size; + + tie(ty, sub, size) = o.first; + int count = o.second; + + if (count > 0) + { + vector v; + BitArray* fl; + string name_s, name_p; + + switch (ty) { + case df::job_type::MakeArmor: + name_s = world->raws.itemdefs.armor[sub]->name; + name_p = world->raws.itemdefs.armor[sub]->name_plural; + v = entity->resources.armor_type; + fl = &world->raws.itemdefs.armor[sub]->props.flags; + break; + case df::job_type::MakeGloves: + name_s = world->raws.itemdefs.gloves[sub]->name; + name_p = world->raws.itemdefs.gloves[sub]->name_plural; + v = entity->resources.gloves_type; + fl = &world->raws.itemdefs.gloves[sub]->props.flags; + break; + case df::job_type::MakeHelm: + name_s = world->raws.itemdefs.helms[sub]->name; + name_p = world->raws.itemdefs.helms[sub]->name_plural; + v = entity->resources.helm_type; + fl = &world->raws.itemdefs.helms[sub]->props.flags; + break; + case df::job_type::MakePants: + name_s = world->raws.itemdefs.pants[sub]->name; + name_p = world->raws.itemdefs.pants[sub]->name_plural; + v = entity->resources.pants_type; + fl = &world->raws.itemdefs.pants[sub]->props.flags; + break; + case df::job_type::MakeShoes: + name_s = world->raws.itemdefs.shoes[sub]->name; + name_p = world->raws.itemdefs.shoes[sub]->name_plural; + v = entity->resources.shoes_type; + fl = &world->raws.itemdefs.shoes[sub]->props.flags; + break; + default: + break; + } + + bool can_make = false; + for (auto vv : v) + { + if (vv == sub) + { + can_make = true; + break; + } + } + + if (!can_make) + { + out.print("Cannot make %s, skipped\n", name_p.c_str()); + continue; // this civilization does not know how to make this item, so sorry + } + + switch (ty) { + case df::item_type::ARMOR: break; + case df::item_type::GLOVES: break; + case df::item_type::HELM: break; + case df::item_type::PANTS: break; + case df::item_type::SHOES: break; + default: break; + } + + df::job_material_category mat; + + if (silk > count + 10 && fl->is_set(df::armor_general_flags::SOFT)) { + mat.whole = df::job_material_category::mask_silk; + silk -= count; + } + else if (cloth > count + 10 && fl->is_set(df::armor_general_flags::SOFT)) { + mat.whole = df::job_material_category::mask_cloth; + cloth -= count; + } + else if (yarn > count + 10 && fl->is_set(df::armor_general_flags::SOFT)) { + mat.whole = df::job_material_category::mask_yarn; + yarn -= count; + } + else if (leather > count + 10 && fl->is_set(df::armor_general_flags::LEATHER)) { + mat.whole = df::job_material_category::mask_leather; + leather -= count; + } + else // not enough appropriate material available + continue; + + auto order = new df::manager_order(); + order->job_type = ty; + order->item_type = df::item_type::NONE; + order->item_subtype = sub; + order->mat_type = -1; + order->mat_index = -1; + order->amount_left = count; + order->amount_total = count; + order->status.bits.validated = false; + order->status.bits.active = false; + order->id = world->manager_order_next_id++; + order->hist_figure_id = sizes[size]; + order->material_category = mat; + + world->manager_orders.push_back(order); + + out.print("Added order #%d for %d %s %s (sized for %s)\n", + order->id, + count, + bitfield_to_string(order->material_category).c_str(), + (count > 1) ? name_p.c_str() : name_s.c_str(), + world->raws.creatures.all[order->hist_figure_id]->name[1].c_str() + ); + } + } +} + +#define DELTA_TICKS 600 + +DFhackCExport command_result plugin_onupdate(color_ostream &out) +{ + if (!enabled) + return CR_OK; + + if (!Maps::IsValid()) + return CR_OK; + + if (DFHack::World::ReadPauseState()) + return CR_OK; + + if (world->frame_counter % DELTA_TICKS != 0) + return CR_OK; + + bool found = false; + + for (df::job_list_link* link = &world->jobs.list; link != NULL; link = link->next) + { + if (link->item == NULL) continue; + if (link->item->job_type == df::enums::job_type::UpdateStockpileRecords) + { + found = true; + break; + } + } + + if (found) + { + do_scan(out); + } + + return CR_OK; +} + +static command_result tailor_cmd(color_ostream &out, vector & parameters) { + bool desired = enabled; + if (parameters.size() == 1) + { + if (parameters[0] == "enable" || parameters[0] == "on" || parameters[0] == "1") + { + desired = true; + } + else if (parameters[0] == "disable" || parameters[0] == "off" || parameters[0] == "0") + { + desired = false; + } + else if (parameters[0] == "usage" || parameters[0] == "help" || parameters[0] == "?") + { + out.print("%s: %s\nUsage:\n%s", plugin_name, tagline, usage); + return CR_OK; + } + else if (parameters[0] == "test") + { + do_scan(out); + return CR_OK; + } + else if (parameters[0] != "status") + { + return CR_WRONG_USAGE; + } + } + else + return CR_WRONG_USAGE; + + out.print("Tailor is %s %s.\n", (desired == enabled)? "currently": "now", desired? "enabled": "disabled"); + enabled = desired; + + return CR_OK; +} + + +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) +{ + return CR_OK; +} + +DFhackCExport command_result plugin_enable(color_ostream& out, bool enable) +{ + enabled = enable; + return CR_OK; +} + +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) +{ + if (AUTOENABLE) { + enabled = true; + } + + commands.push_back(PluginCommand(plugin_name, tagline, tailor_cmd, false, usage)); + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown(color_ostream &out) { + return plugin_enable(out, false); +} diff --git a/plugins/title-folder.cpp b/plugins/title-folder.cpp index eae381b95..4f93a6c7f 100644 --- a/plugins/title-folder.cpp +++ b/plugins/title-folder.cpp @@ -5,11 +5,16 @@ #include "MemAccess.h" #include "PluginManager.h" +#include "df/init.h" + using namespace DFHack; +using namespace df::enums; DFHACK_PLUGIN("title-folder"); DFHACK_PLUGIN_IS_ENABLED(is_enabled); +REQUIRE_GLOBAL(init); + // SDL frees the old window title when changed static std::string original_title; @@ -36,6 +41,12 @@ DFhackCExport command_result plugin_shutdown (color_ostream &out); DFhackCExport command_result plugin_init (color_ostream &out, std::vector &commands) { + if (init->display.flag.is_set(init_display_flags::TEXT)) + { + // Don't bother initializing in text mode. + return CR_OK; + } + for (auto it = sdl_libs.begin(); it != sdl_libs.end(); ++it) { if ((sdl_handle = OpenPlugin(it->c_str()))) @@ -92,6 +103,12 @@ DFhackCExport command_result plugin_enable (color_ostream &out, bool state) if (state) { + if (init->display.flag.is_set(init_display_flags::TEXT)) + { + out.printerr("title-folder: cannot enable with PRINT_MODE:TEXT.\n"); + return CR_FAILURE; + } + std::string path = Core::getInstance().p->getPath(); std::string folder; size_t pos = path.find_last_of('/'); diff --git a/scripts b/scripts index 8ef283377..61d8e935f 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 8ef283377c9830fb932ea888d89b551873af36cf +Subproject commit 61d8e935ffd3c705ac13082874e2a3cb0363251b