diff --git a/docs/guides/quickfort-user-guide.rst b/docs/guides/quickfort-user-guide.rst index 6f606c43a..ecee46918 100644 --- a/docs/guides/quickfort-user-guide.rst +++ b/docs/guides/quickfort-user-guide.rst @@ -1308,7 +1308,7 @@ legacy Python Quickfort. This setting has no effect on DFHack Quickfort, which will use buildingplan to manage everything designated in a ``#build`` blueprint regardless of the buildingplan UI settings. -However, quickfort *does* use `buildingplan's filters ` +However, quickfort *does* use `buildingplan's filters ` for each building type. For example, you can use the buildingplan UI to set the type of stone you want your walls made out of. Or you can specify that all buildingplan-managed chairs and tables must be of Masterful quality. The current diff --git a/docs/plugins/buildingplan.rst b/docs/plugins/buildingplan.rst index 1eb18b1d5..f77e5b590 100644 --- a/docs/plugins/buildingplan.rst +++ b/docs/plugins/buildingplan.rst @@ -2,91 +2,93 @@ buildingplan ============ .. dfhack-tool:: - :summary: Plan building construction before you have materials. - :tags: untested fort design buildings - -This plugin adds a planning mode for building placement. You can then place -furniture, constructions, and other buildings before the required materials are -available, and they will be created in a suspended state. Buildingplan will -periodically scan for appropriate items, and the jobs will be unsuspended when -the items are available. - -This is very useful when combined with manager work orders or `workflow` -- you -can set a constraint to always have one or two doors/beds/tables/chairs/etc. -available, and place as many as you like. Materials are used to build the -planned buildings as they are produced, with minimal space dedicated to -stockpiles. + :summary: Plan building layouts with or without materials. + :tags: fort design buildings + +Buildingplan allows you to place furniture, constructions, and other buildings, +regardless of whether the required materials are available. This allows you to +focus purely on design elements when you are laying out your fort, and defers +item production concerns to a more convenient time. + +Buildingplan is as an alternative to the vanilla building placement UI. It +appears after you have selected the type of building, furniture, or construction +that you want to place in the vanilla build menu. Buildingplan then takes over +for the actual placement step. If any building materials are not available yet +for the placed building, it will be created in a suspended state. Buildingplan +will periodically scan for appropriate items and attach them. Once all items are +attached, the construction job will be unsuspended and a dwarf will come and +build the building. If you have the `unsuspend` overlay enabled (it is enabled +by default), then buildingplan-suspended buildings will appear with a ``P`` marker +on the main map, as opposed to the usual ``x`` marker for "regular" suspended +buildings. + +If you want to impose restrictions on which items are chosen for the buildings, +buildingplan has full support for quality and material filters. Before you place +a building, you can select a component item in the list and hit ``f`` or click on +the ``filter`` button next to the item description. This will let you choose your +desired item quality range, whether the item must be decorated, and even which +specific materials the item must be made out of. This lets you create layouts +with a consistent color, if that is part of your design. + +If you just care about the heat sensitivity of the building, you can set the +building to be fire- or magma-proof in the placement UI screen or in any item +filter screen, and the restriction will apply to all building items. This makes it +very easy to create magma-safe pump stacks, for example. + +Buildingplan works very well in conjuction with other design tools like +`gui/quickfort`, which allow you to apply a building layout from a blueprint. You +can apply very large, complicated layouts, and the buildings will simply be built +when your dwarves get around to producing the needed materials. If you set filters +in the buildingplan UI before applying the blueprint, the filters will be applied +to the blueprint buildings, just as if you had planned them from the buildingplan +placement UI. + +One way to integrate buildingplan into your gameplay is to create manager +workorders to ensure you always have a few blocks/doors/beds/etc. available. You +can then place as many of each building as you like. Produced items will be used +to build the planned buildings as they are produced, with minimal space dedicated +to stockpiles. The DFHack `orders` library can help with setting up these manager +workorders for you. + +If you do not wish to use the ``buildingplan`` interface, you can turn off the +``buildingplan.planner`` overlay in `gui/overlay`. You should not disable the +``buildingplan`` service entirely in `gui/control-panel` since then existing +planned buildings in loaded forts will stop functioning. Usage ----- :: - enable buildingplan - buildingplan set - buildingplan set true|false + buildingplan [status] + buildingplan set (true|false) + +Examples +-------- -Running ``buildingplan set`` without parameters displays the current settings. +``buildingplan`` + Print a report of current settings, which kinds of buildings are planned, + and what kinds of materials the buildings are waiting for. .. _buildingplan-settings: Global settings --------------- -The buildingplan plugin has global settings that can be set from the UI -(:kbd:`G` from any building placement screen, for example: -:kbd:`b`:kbd:`a`:kbd:`G`). These settings can also be set via the -``buildingplan set`` command. The available settings are: +The buildingplan plugin has several global settings that affect what materials +can be chosen when attaching items to planned buildings: -``all_enabled`` (default: false) - Enable planning mode for all building types. ``blocks``, ``boulders``, ``logs``, ``bars`` (defaults: true, true, true, false) Allow blocks, boulders, logs, or bars to be matched for generic "building material" items. -``quickfort_mode`` (default: false) - Enable compatibility mode for the legacy Python Quickfort (this setting is - not required for DFHack `quickfort`) -The settings for ``blocks``, ``boulders``, ``logs``, and ``bars`` are saved with -your fort, so you only have to set them once and they will be persisted in your -save. +These settings are saved with your fort, so you only have to set them once and +they will be persisted in your save. If you normally embark with some blocks on hand for early workshops, you might want to add this line to your ``dfhack-config/init/onMapLoad.init`` file to -always configure buildingplan to just use blocks for buildings and +always configure `buildingplan` to just use blocks for buildings and constructions:: - on-new-fortress buildingplan set boulders false; buildingplan set logs false - -.. _buildingplan-filters: - -Item filtering --------------- - -While placing a building, you can set filters for what materials you want the -building made out of, what quality you want the component items to be, and -whether you want the items to be decorated. - -If a building type takes more than one item to construct, use -:kbd:`Ctrl`:kbd:`Left` and :kbd:`Ctrl`:kbd:`Right` to select the item that you -want to set filters for. Any filters that you set will be used for all buildings -of the selected type placed from that point onward (until you set a new filter -or clear the current one). Buildings placed before the filters were changed will -keep the filter values that were set when the building was placed. - -For example, you can be sure that all your constructed walls are the same color -by setting a filter to accept only certain types of stone. - -Quickfort mode --------------- - -If you use the external Python Quickfort to apply building blueprints instead of -the native DFHack `quickfort` script, you must enable Quickfort mode. This -temporarily enables buildingplan for all building types and adds an extra blank -screen after every building placement. This "dummy" screen is needed for Python -Quickfort to interact successfully with Dwarf Fortress. - -Note that Quickfort mode is only for compatibility with the legacy Python -Quickfort. The DFHack `quickfort` script does not need this Quickfort mode to be -enabled. The `quickfort` script will successfully integrate with buildingplan as -long as the buildingplan plugin itself is enabled. + on-new-fortress buildingplan set boulders false + on-new-fortress buildingplan set logs false diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 7940c3994..82722f16a 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -89,7 +89,7 @@ dfhack_plugin(autonestbox autonestbox.cpp LINK_LIBRARIES lua) dfhack_plugin(blueprint blueprint.cpp LINK_LIBRARIES lua) #dfhack_plugin(burrows burrows.cpp LINK_LIBRARIES lua) #dfhack_plugin(building-hacks building-hacks.cpp LINK_LIBRARIES lua) -dfhack_plugin(buildingplan buildingplan.cpp LINK_LIBRARIES lua) +add_subdirectory(buildingplan) #dfhack_plugin(changeitem changeitem.cpp) dfhack_plugin(changelayer changelayer.cpp) dfhack_plugin(changevein changevein.cpp) diff --git a/plugins/buildingplan.cpp b/plugins/buildingplan.cpp deleted file mode 100644 index 039c83b0f..000000000 --- a/plugins/buildingplan.cpp +++ /dev/null @@ -1,689 +0,0 @@ -#include "Core.h" -#include "Debug.h" -#include "LuaTools.h" -#include "PluginManager.h" - -#include "modules/Items.h" -#include "modules/Job.h" -#include "modules/Materials.h" -#include "modules/Persistence.h" -#include "modules/World.h" - -#include "df/building.h" -#include "df/building_design.h" -#include "df/item.h" -#include "df/job_item.h" -#include "df/world.h" - -#include -#include -#include -#include - -using std::map; -using std::pair; -using std::queue; -using std::string; -using std::unordered_map; -using std::vector; - -using namespace DFHack; - -DFHACK_PLUGIN("buildingplan"); -DFHACK_PLUGIN_IS_ENABLED(is_enabled); - -REQUIRE_GLOBAL(world); - -// logging levels can be dynamically controlled with the `debugfilter` command. -namespace DFHack { - // for configuration-related logging - DBG_DECLARE(buildingplan, status, DebugCategory::LINFO); - // for logging during the periodic scan - DBG_DECLARE(buildingplan, cycle, DebugCategory::LINFO); -} - -static const string CONFIG_KEY = string(plugin_name) + "/config"; -static const string BLD_CONFIG_KEY = string(plugin_name) + "/building"; - -enum ConfigValues { - CONFIG_BLOCKS = 1, - CONFIG_BOULDERS = 2, - CONFIG_LOGS = 3, - CONFIG_BARS = 4, -}; - -enum BuildingConfigValues { - BLD_CONFIG_ID = 0, -}; - -static int get_config_val(PersistentDataItem &c, int index) { - if (!c.isValid()) - return -1; - return c.ival(index); -} -static bool get_config_bool(PersistentDataItem &c, int index) { - return get_config_val(c, index) == 1; -} -static void set_config_val(PersistentDataItem &c, int index, int value) { - if (c.isValid()) - c.ival(index) = value; -} -static void set_config_bool(PersistentDataItem &c, int index, bool value) { - set_config_val(c, index, value ? 1 : 0); -} - -class PlannedBuilding { -public: - const df::building::key_field_type id; - - PlannedBuilding(color_ostream &out, df::building *building) : id(building->id) { - DEBUG(status,out).print("creating persistent data for building %d\n", id); - bld_config = DFHack::World::AddPersistentData(BLD_CONFIG_KEY); - set_config_val(bld_config, BLD_CONFIG_ID, id); - } - - PlannedBuilding(DFHack::PersistentDataItem &bld_config) - : id(get_config_val(bld_config, BLD_CONFIG_ID)), bld_config(bld_config) { } - - void remove(color_ostream &out); - - // Ensure the building still exists and is in a valid state. It can disappear - // for lots of reasons, such as running the game with the buildingplan plugin - // disabled, manually removing the building, modifying it via the API, etc. - df::building * getBuildingIfValidOrRemoveIfNot(color_ostream &out) { - auto bld = df::building::find(id); - bool valid = bld && bld->getBuildStage() == 0; - if (!valid) { - remove(out); - return NULL; - } - return bld; - } - -private: - DFHack::PersistentDataItem bld_config; -}; - -static PersistentDataItem config; -// building id -> PlannedBuilding -unordered_map planned_buildings; -// vector id -> filter bucket -> queue of (building id, job_item index) -map>>> tasks; - -// note that this just removes the PlannedBuilding. the tasks will get dropped -// as we discover them in the tasks queues and they fail to be found in planned_buildings. -// this "lazy" task cleaning algorithm works because there is no way to -// re-register a building once it has been removed -- if it has been booted out of -// planned_buildings, then it has either been built or desroyed. therefore there is -// no chance of duplicate tasks getting added to the tasks queues. -void PlannedBuilding::remove(color_ostream &out) { - DEBUG(status,out).print("removing persistent data for building %d\n", id); - DFHack::World::DeletePersistentData(config); - if (planned_buildings.count(id) > 0) - planned_buildings.erase(id); -} - -static const int32_t CYCLE_TICKS = 600; // twice per game day -static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle - -static command_result do_command(color_ostream &out, vector ¶meters); -static void do_cycle(color_ostream &out); -static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb); - -DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(status,out).print("initializing %s\n", plugin_name); - - // provide a configuration interface for the plugin - commands.push_back(PluginCommand( - plugin_name, - "Plan building placement before you have materials.", - do_command)); - - return CR_OK; -} - -DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { - if (enable != is_enabled) { - is_enabled = enable; - DEBUG(status,out).print("%s from the API; persisting\n", - is_enabled ? "enabled" : "disabled"); - } else { - DEBUG(status,out).print("%s from the API, but already %s; no action\n", - is_enabled ? "enabled" : "disabled", - is_enabled ? "enabled" : "disabled"); - } - return CR_OK; -} - -DFhackCExport command_result plugin_shutdown (color_ostream &out) { - DEBUG(status,out).print("shutting down %s\n", plugin_name); - - return CR_OK; -} - -DFhackCExport command_result plugin_load_data (color_ostream &out) { - cycle_timestamp = 0; - config = World::GetPersistentData(CONFIG_KEY); - - if (!config.isValid()) { - DEBUG(status,out).print("no config found in this save; initializing\n"); - config = World::AddPersistentData(CONFIG_KEY); - set_config_bool(config, CONFIG_BLOCKS, true); - set_config_bool(config, CONFIG_BOULDERS, true); - set_config_bool(config, CONFIG_LOGS, true); - set_config_bool(config, CONFIG_BARS, false); - } - - DEBUG(status,out).print("loading persisted state\n"); - planned_buildings.clear(); - tasks.clear(); - vector building_configs; - World::GetPersistentData(&building_configs, BLD_CONFIG_KEY); - const size_t num_building_configs = building_configs.size(); - for (size_t idx = 0; idx < num_building_configs; ++idx) { - PlannedBuilding pb(building_configs[idx]); - registerPlannedBuilding(out, pb); - } - - return CR_OK; -} - -DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { - if (event == DFHack::SC_WORLD_UNLOADED) { - DEBUG(status,out).print("world unloaded; clearing state for %s\n", plugin_name); - planned_buildings.clear(); - tasks.clear(); - } - return CR_OK; -} - -static bool cycle_requested = false; - -DFhackCExport command_result plugin_onupdate(color_ostream &out) { - if (!Core::getInstance().isWorldLoaded()) - return CR_OK; - - if (is_enabled && - (cycle_requested || world->frame_counter - cycle_timestamp >= CYCLE_TICKS)) - do_cycle(out); - return CR_OK; -} - -static bool call_buildingplan_lua(color_ostream *out, const char *fn_name, - int nargs = 0, int nres = 0, - Lua::LuaLambda && args_lambda = Lua::DEFAULT_LUA_LAMBDA, - Lua::LuaLambda && res_lambda = Lua::DEFAULT_LUA_LAMBDA) { - DEBUG(status).print("calling buildingplan lua function: '%s'\n", fn_name); - - CoreSuspender guard; - - auto L = Lua::Core::State; - Lua::StackUnwinder top(L); - - if (!out) - out = &Core::getInstance().getConsole(); - - return Lua::CallLuaModuleFunction(*out, L, "plugins.buildingplan", fn_name, - nargs, nres, - std::forward(args_lambda), - std::forward(res_lambda)); -} - -static command_result do_command(color_ostream &out, vector ¶meters) { - CoreSuspender suspend; - - if (!Core::getInstance().isWorldLoaded()) { - out.printerr("Cannot configure %s without a loaded world.\n", plugin_name); - return CR_FAILURE; - } - - bool show_help = false; - if (!call_buildingplan_lua(&out, "parse_commandline", parameters.size(), 1, - [&](lua_State *L) { - for (const string ¶m : parameters) - Lua::Push(L, param); - }, - [&](lua_State *L) { - show_help = !lua_toboolean(L, -1); - })) { - return CR_FAILURE; - } - - return show_help ? CR_WRONG_USAGE : CR_OK; -} - -///////////////////////////////////////////////////// -// cycle logic -// - -struct BadFlags { - uint32_t whole; - - BadFlags() { - df::item_flags flags; - #define F(x) 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(in_job); - F(owned); F(in_chest); F(removed); F(encased); - F(spider_web); - #undef F - whole = flags.whole; - } -}; - -static bool itemPassesScreen(df::item * item) { - static const BadFlags bad_flags; - return !(item->flags.whole & bad_flags.whole) - && !item->isAssignedToStockpile(); -} - -static bool matchesFilters(df::item * item, df::job_item * job_item) { - // check the properties that are not checked by Job::isSuitableItem() - if (job_item->item_type > -1 && job_item->item_type != item->getType()) - return false; - - if (job_item->item_subtype > -1 && - job_item->item_subtype != item->getSubtype()) - return false; - - if (job_item->flags2.bits.building_material && !item->isBuildMat()) - return false; - - if (job_item->metal_ore > -1 && !item->isMetalOre(job_item->metal_ore)) - return false; - - if (job_item->has_tool_use > df::tool_uses::NONE - && !item->hasToolUse(job_item->has_tool_use)) - return false; - - return DFHack::Job::isSuitableItem( - job_item, item->getType(), item->getSubtype()) - && DFHack::Job::isSuitableMaterial( - job_item, item->getMaterial(), item->getMaterialIndex(), - item->getType()); -} - -static bool isJobReady(color_ostream &out, df::job * job) { - int needed_items = 0; - for (auto job_item : job->job_items) { needed_items += job_item->quantity; } - if (needed_items) { - DEBUG(cycle,out).print("building needs %d more item(s)\n", needed_items); - return false; - } - return true; -} - -static bool job_item_idx_lt(df::job_item_ref *a, df::job_item_ref *b) { - // we want the items in the opposite order of the filters - return a->job_item_idx > b->job_item_idx; -} - -// this function does not remove the job_items since their quantity fields are -// now all at 0, so there is no risk of having extra items attached. we don't -// remove them to keep the "finalize with buildingplan active" path as similar -// as possible to the "finalize with buildingplan disabled" path. -static void finalizeBuilding(color_ostream &out, df::building * bld) { - DEBUG(cycle,out).print("finalizing building %d\n", bld->id); - auto job = bld->jobs[0]; - - // sort the items so they get added to the structure in the correct order - std::sort(job->items.begin(), job->items.end(), job_item_idx_lt); - - // derive the material properties of the building and job from the first - // applicable item. if any boulders are involved, it makes the whole - // structure "rough". - bool rough = false; - for (auto attached_item : job->items) { - df::item *item = attached_item->item; - rough = rough || item->getType() == df::item_type::BOULDER; - if (bld->mat_type == -1) { - bld->mat_type = item->getMaterial(); - job->mat_type = bld->mat_type; - } - if (bld->mat_index == -1) { - bld->mat_index = item->getMaterialIndex(); - job->mat_index = bld->mat_index; - } - } - - if (bld->needsDesign()) { - auto act = (df::building_actual *)bld; - if (!act->design) - act->design = new df::building_design(); - act->design->flags.bits.rough = rough; - } - - // we're good to go! - job->flags.bits.suspend = false; - Job::checkBuildingsNow(); -} - -static df::building * popInvalidTasks(color_ostream &out, queue> & task_queue) { - while (!task_queue.empty()) { - auto & task = task_queue.front(); - auto id = task.first; - if (planned_buildings.count(id) > 0) { - auto bld = planned_buildings.at(id).getBuildingIfValidOrRemoveIfNot(out); - if (bld && bld->jobs[0]->job_items[task.second]->quantity) - return bld; - } - DEBUG(cycle,out).print("discarding invalid task: bld=%d, job_item_idx=%d\n", id, task.second); - task_queue.pop(); - } - return NULL; -} - -static void doVector(color_ostream &out, df::job_item_vector_id vector_id, - map>> & buckets) { - auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id); - auto item_vector = df::global::world->items.other[other_id]; - DEBUG(cycle,out).print("matching %zu item(s) in vector %s against %zu filter bucket(s)\n", - item_vector.size(), - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - buckets.size()); - for (auto item_it = item_vector.rbegin(); - item_it != item_vector.rend(); - ++item_it) { - auto item = *item_it; - if (!itemPassesScreen(item)) - continue; - for (auto bucket_it = buckets.begin(); bucket_it != buckets.end(); ) { - auto & task_queue = bucket_it->second; - auto bld = popInvalidTasks(out, task_queue); - if (!bld) { - DEBUG(cycle,out).print("removing empty bucket: %s/%s; %zu bucket(s) left\n", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket_it->first.c_str(), - buckets.size() - 1); - bucket_it = buckets.erase(bucket_it); - continue; - } - auto & task = task_queue.front(); - auto id = task.first; - auto job = bld->jobs[0]; - auto filter_idx = task.second; - if (matchesFilters(item, job->job_items[filter_idx]) - && DFHack::Job::attachJobItem(job, item, - df::job_item_ref::Hauled, filter_idx)) - { - MaterialInfo material; - material.decode(item); - ItemTypeInfo item_type; - item_type.decode(item); - DEBUG(cycle,out).print("attached %s %s to filter %d for %s(%d): %s/%s\n", - material.toString().c_str(), - item_type.toString().c_str(), - filter_idx, - ENUM_KEY_STR(building_type, bld->getType()).c_str(), - id, - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket_it->first.c_str()); - // keep quantity aligned with the actual number of remaining - // items so if buildingplan is turned off, the building will - // be completed with the correct number of items. - --job->job_items[filter_idx]->quantity; - task_queue.pop(); - if (isJobReady(out, job)) { - finalizeBuilding(out, bld); - planned_buildings.at(id).remove(out); - } - if (task_queue.empty()) { - DEBUG(cycle,out).print( - "removing empty item bucket: %s/%s; %zu left\n", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket_it->first.c_str(), - buckets.size() - 1); - buckets.erase(bucket_it); - } - // we found a home for this item; no need to look further - break; - } - ++bucket_it; - } - if (buckets.empty()) - break; - } -} - -struct VectorsToScanLast { - std::vector vectors; - VectorsToScanLast() { - // order is important here. we want to match boulders before wood and - // everything before bars. blocks are not listed here since we'll have - // already scanned them when we did the first pass through the buckets. - vectors.push_back(df::job_item_vector_id::BOULDER); - vectors.push_back(df::job_item_vector_id::WOOD); - vectors.push_back(df::job_item_vector_id::BAR); - } -}; - -static void do_cycle(color_ostream &out) { - static const VectorsToScanLast vectors_to_scan_last; - - // mark that we have recently run - cycle_timestamp = world->frame_counter; - cycle_requested = false; - - DEBUG(cycle,out).print("running %s cycle for %zu registered buildings\n", - plugin_name, planned_buildings.size()); - - for (auto it = tasks.begin(); it != tasks.end(); ) { - auto vector_id = it->first; - // we could make this a set, but it's only three elements - if (std::find(vectors_to_scan_last.vectors.begin(), - vectors_to_scan_last.vectors.end(), - vector_id) != vectors_to_scan_last.vectors.end()) { - ++it; - continue; - } - - auto & buckets = it->second; - doVector(out, vector_id, buckets); - if (buckets.empty()) { - DEBUG(cycle,out).print("removing empty vector: %s; %zu vector(s) left\n", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - tasks.size() - 1); - it = tasks.erase(it); - } - else - ++it; - } - for (auto vector_id : vectors_to_scan_last.vectors) { - if (tasks.count(vector_id) == 0) - continue; - auto & buckets = tasks[vector_id]; - doVector(out, vector_id, buckets); - if (buckets.empty()) { - DEBUG(cycle,out).print("removing empty vector: %s; %zu vector(s) left\n", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - tasks.size() - 1); - tasks.erase(vector_id); - } - } - DEBUG(cycle,out).print("cycle done; %zu registered building(s) left\n", - planned_buildings.size()); -} - -///////////////////////////////////////////////////// -// Lua API -// core will already be suspended when coming in through here -// - -static string getBucket(const df::job_item & ji) { - std::ostringstream ser; - - // pull out and serialize only known relevant fields. if we miss a few, then - // the filter bucket will be slighly less specific than it could be, but - // that's probably ok. we'll just end up bucketing slightly different items - // together. this is only a problem if the different filter at the front of - // the queue doesn't match any available items and blocks filters behind it - // that could be matched. - ser << ji.item_type << ':' << ji.item_subtype << ':' << ji.mat_type << ':' - << ji.mat_index << ':' << ji.flags1.whole << ':' << ji.flags2.whole - << ':' << ji.flags3.whole << ':' << ji.flags4 << ':' << ji.flags5 << ':' - << ji.metal_ore << ':' << ji.has_tool_use; - - return ser.str(); -} - -// get a list of item vectors that we should search for matches -static vector getVectorIds(color_ostream &out, df::job_item *job_item) -{ - std::vector ret; - - // if the filter already has the vector_id set to something specific, use it - if (job_item->vector_id > df::job_item_vector_id::IN_PLAY) - { - DEBUG(status,out).print("using vector_id from job_item: %s\n", - ENUM_KEY_STR(job_item_vector_id, job_item->vector_id).c_str()); - ret.push_back(job_item->vector_id); - return ret; - } - - // if the filer is for building material, refer to our global settings for - // which vectors to search - if (job_item->flags2.bits.building_material) - { - if (get_config_bool(config, CONFIG_BLOCKS)) - ret.push_back(df::job_item_vector_id::BLOCKS); - if (get_config_bool(config, CONFIG_BOULDERS)) - ret.push_back(df::job_item_vector_id::BOULDER); - if (get_config_bool(config, CONFIG_LOGS)) - ret.push_back(df::job_item_vector_id::WOOD); - if (get_config_bool(config, CONFIG_BARS)) - ret.push_back(df::job_item_vector_id::BAR); - } - - // fall back to IN_PLAY if no other vector was appropriate - if (ret.empty()) - ret.push_back(df::job_item_vector_id::IN_PLAY); - return ret; -} -static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb) { - df::building * bld = pb.getBuildingIfValidOrRemoveIfNot(out); - if (!bld) - return false; - - if (bld->jobs.size() != 1) { - DEBUG(status,out).print("unexpected number of jobs: want 1, got %zu\n", bld->jobs.size()); - return false; - } - auto job_items = bld->jobs[0]->job_items; - int num_job_items = job_items.size(); - if (num_job_items < 1) { - DEBUG(status,out).print("unexpected number of job items: want >0, got %d\n", num_job_items); - return false; - } - int32_t id = bld->id; - for (int job_item_idx = 0; job_item_idx < num_job_items; ++job_item_idx) { - auto job_item = job_items[job_item_idx]; - auto bucket = getBucket(*job_item); - auto vector_ids = getVectorIds(out, job_item); - - // if there are multiple vector_ids, schedule duplicate tasks. after - // the correct number of items are matched, the extras will get popped - // as invalid - for (auto vector_id : vector_ids) { - for (int item_num = 0; item_num < job_item->quantity; ++item_num) { - tasks[vector_id][bucket].push(std::make_pair(id, job_item_idx)); - DEBUG(status,out).print("added task: %s/%s/%d,%d; " - "%zu vector(s), %zu filter bucket(s), %zu task(s) in bucket", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket.c_str(), id, job_item_idx, tasks.size(), - tasks[vector_id].size(), tasks[vector_id][bucket].size()); - } - } - } - - // suspend jobs - for (auto job : bld->jobs) - job->flags.bits.suspend = true; - - // add the planned buildings to our register - planned_buildings.emplace(bld->id, pb); - - return true; -} - -static void printStatus(color_ostream &out) { - DEBUG(status,out).print("entering buildingplan_printStatus\n"); - out.print("buildingplan is %s\n\n", is_enabled ? "enabled" : "disabled"); - out.print(" finding materials for %zd buildings\n", planned_buildings.size()); - out.print("Current settings:\n"); - out.print(" use blocks: %s\n", get_config_bool(config, CONFIG_BLOCKS) ? "yes" : "no"); - out.print(" use boulders: %s\n", get_config_bool(config, CONFIG_BOULDERS) ? "yes" : "no"); - out.print(" use logs: %s\n", get_config_bool(config, CONFIG_LOGS) ? "yes" : "no"); - out.print(" use bars: %s\n", get_config_bool(config, CONFIG_BARS) ? "yes" : "no"); - out.print("\n"); -} - -static bool setSetting(color_ostream &out, string name, bool value) { - DEBUG(status,out).print("entering setSetting (%s -> %s)\n", name.c_str(), value ? "true" : "false"); - if (name == "blocks") - set_config_bool(config, CONFIG_BLOCKS, value); - else if (name == "boulders") - set_config_bool(config, CONFIG_BOULDERS, value); - else if (name == "logs") - set_config_bool(config, CONFIG_LOGS, value); - else if (name == "bars") - set_config_bool(config, CONFIG_BARS, value); - else { - out.printerr("unrecognized setting: '%s'\n", name.c_str()); - return false; - } - return true; -} - -static bool isPlannableBuilding(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom) { - DEBUG(status,out).print("entering isPlannableBuilding\n"); - int num_filters = 0; - if (!call_buildingplan_lua(&out, "get_num_filters", 3, 1, - [&](lua_State *L) { - Lua::Push(L, type); - Lua::Push(L, subtype); - Lua::Push(L, custom); - }, - [&](lua_State *L) { - num_filters = lua_tonumber(L, -1); - })) { - return false; - } - return num_filters >= 1; -} - -static bool isPlannedBuilding(color_ostream &out, df::building *bld) { - TRACE(status,out).print("entering isPlannedBuilding\n"); - return bld && planned_buildings.count(bld->id) > 0; -} - -static bool addPlannedBuilding(color_ostream &out, df::building *bld) { - DEBUG(status,out).print("entering addPlannedBuilding\n"); - if (!bld || planned_buildings.count(bld->id) - || !isPlannableBuilding(out, bld->getType(), bld->getSubtype(), - bld->getCustomType())) - return false; - PlannedBuilding pb(out, bld); - return registerPlannedBuilding(out, pb); -} - -static void doCycle(color_ostream &out) { - DEBUG(status,out).print("entering doCycle\n"); - do_cycle(out); -} - -static void scheduleCycle(color_ostream &out) { - DEBUG(status,out).print("entering scheduleCycle\n"); - cycle_requested = true; -} - -DFHACK_PLUGIN_LUA_FUNCTIONS { - DFHACK_LUA_FUNCTION(printStatus), - DFHACK_LUA_FUNCTION(setSetting), - DFHACK_LUA_FUNCTION(isPlannableBuilding), - DFHACK_LUA_FUNCTION(isPlannedBuilding), - DFHACK_LUA_FUNCTION(addPlannedBuilding), - DFHACK_LUA_FUNCTION(doCycle), - DFHACK_LUA_FUNCTION(scheduleCycle), - DFHACK_LUA_END -}; diff --git a/plugins/buildingplan/CMakeLists.txt b/plugins/buildingplan/CMakeLists.txt index 1d34b169a..118b2a1d1 100644 --- a/plugins/buildingplan/CMakeLists.txt +++ b/plugins/buildingplan/CMakeLists.txt @@ -2,10 +2,15 @@ project(buildingplan) set(COMMON_HDRS buildingplan.h - buildingplan-planner.h - buildingplan-rooms.h + buildingtypekey.h + defaultitemfilters.h + itemfilter.h + plannedbuilding.h ) set_source_files_properties(${COMMON_HDRS} PROPERTIES HEADER_FILE_ONLY TRUE) -dfhack_plugin(buildingplan buildingplan.cpp buildingplan-planner.cpp - buildingplan-rooms.cpp ${COMMON_HDRS} LINK_LIBRARIES lua) +dfhack_plugin(buildingplan + buildingplan.cpp buildingplan_cycle.cpp buildingtypekey.cpp + defaultitemfilters.cpp itemfilter.cpp plannedbuilding.cpp + ${COMMON_HDRS} + LINK_LIBRARIES lua) diff --git a/plugins/buildingplan/buildingplan-planner.cpp b/plugins/buildingplan/buildingplan-planner.cpp deleted file mode 100644 index 07f23150a..000000000 --- a/plugins/buildingplan/buildingplan-planner.cpp +++ /dev/null @@ -1,1074 +0,0 @@ -#include -#include // for CHAR_BIT - -#include "df/building_design.h" -#include "df/building_doorst.h" -#include "df/building_type.h" -#include "df/general_ref_building_holderst.h" -#include "df/job_item.h" -#include "df/buildreq.h" - -#include "modules/Buildings.h" -#include "modules/Gui.h" -#include "modules/Job.h" - -#include "LuaTools.h" -#include "../uicommon.h" - -#include "buildingplan.h" - -static const std::string planned_building_persistence_key_v1 = "buildingplan/constraints"; -static const std::string planned_building_persistence_key_v2 = "buildingplan/constraints2"; -static const std::string global_settings_persistence_key = "buildingplan/global"; - -/* - * ItemFilter - */ - -ItemFilter::ItemFilter() -{ - clear(); -} - -void ItemFilter::clear() -{ - min_quality = df::item_quality::Ordinary; - max_quality = df::item_quality::Masterful; - decorated_only = false; - clearMaterialMask(); - materials.clear(); -} - -bool ItemFilter::deserialize(std::string ser) -{ - clear(); - - std::vector tokens; - split_string(&tokens, ser, "/"); - if (tokens.size() != 5) - { - debug("invalid ItemFilter serialization: '%s'", ser.c_str()); - return false; - } - - if (!deserializeMaterialMask(tokens[0]) || !deserializeMaterials(tokens[1])) - return false; - - setMinQuality(atoi(tokens[2].c_str())); - setMaxQuality(atoi(tokens[3].c_str())); - decorated_only = static_cast(atoi(tokens[4].c_str())); - return true; -} - -bool ItemFilter::deserializeMaterialMask(std::string ser) -{ - if (ser.empty()) - return true; - - if (!parseJobMaterialCategory(&mat_mask, ser)) - { - debug("invalid job material category serialization: '%s'", ser.c_str()); - return false; - } - return true; -} - -bool ItemFilter::deserializeMaterials(std::string ser) -{ - if (ser.empty()) - return true; - - std::vector mat_names; - split_string(&mat_names, ser, ","); - for (auto m = mat_names.begin(); m != mat_names.end(); m++) - { - DFHack::MaterialInfo material; - if (!material.find(*m) || !material.isValid()) - { - debug("invalid material name serialization: '%s'", ser.c_str()); - return false; - } - materials.push_back(material); - } - return true; -} - -// format: mat,mask,elements/materials,list/minq/maxq/decorated -std::string ItemFilter::serialize() const -{ - std::ostringstream ser; - ser << bitfield_to_string(mat_mask, ",") << "/"; - if (!materials.empty()) - { - ser << materials[0].getToken(); - for (size_t i = 1; i < materials.size(); ++i) - ser << "," << materials[i].getToken(); - } - ser << "/" << static_cast(min_quality); - ser << "/" << static_cast(max_quality); - ser << "/" << static_cast(decorated_only); - return ser.str(); -} - -void ItemFilter::clearMaterialMask() -{ - mat_mask.whole = 0; -} - -void ItemFilter::addMaterialMask(uint32_t mask) -{ - mat_mask.whole |= mask; -} - -void ItemFilter::setMaterials(std::vector materials) -{ - this->materials = materials; -} - -static void clampItemQuality(df::item_quality *quality) -{ - if (*quality > item_quality::Artifact) - { - debug("clamping quality to Artifact"); - *quality = item_quality::Artifact; - } - if (*quality < item_quality::Ordinary) - { - debug("clamping quality to Ordinary"); - *quality = item_quality::Ordinary; - } -} - -void ItemFilter::setMinQuality(int quality) -{ - min_quality = static_cast(quality); - clampItemQuality(&min_quality); - if (max_quality < min_quality) - max_quality = min_quality; -} - -void ItemFilter::setMaxQuality(int quality) -{ - max_quality = static_cast(quality); - clampItemQuality(&max_quality); - if (max_quality < min_quality) - min_quality = max_quality; -} - -void ItemFilter::incMinQuality() { setMinQuality(min_quality + 1); } -void ItemFilter::decMinQuality() { setMinQuality(min_quality - 1); } -void ItemFilter::incMaxQuality() { setMaxQuality(max_quality + 1); } -void ItemFilter::decMaxQuality() { setMaxQuality(max_quality - 1); } - -void ItemFilter::toggleDecoratedOnly() { decorated_only = !decorated_only; } - -static std::string material_to_string_fn(const MaterialInfo &m) { return m.toString(); } - -uint32_t ItemFilter::getMaterialMask() const { return mat_mask.whole; } - -std::vector ItemFilter::getMaterials() const -{ - std::vector descriptions; - transform_(materials, descriptions, material_to_string_fn); - - if (descriptions.size() == 0) - bitfield_to_string(&descriptions, mat_mask); - - if (descriptions.size() == 0) - descriptions.push_back("any"); - - return descriptions; -} - -std::string ItemFilter::getMinQuality() const -{ - return ENUM_KEY_STR(item_quality, min_quality); -} - -std::string ItemFilter::getMaxQuality() const -{ - return ENUM_KEY_STR(item_quality, max_quality); -} - -bool ItemFilter::getDecoratedOnly() const -{ - return decorated_only; -} - -bool ItemFilter::matchesMask(DFHack::MaterialInfo &mat) const -{ - return mat_mask.whole ? mat.matches(mat_mask) : true; -} - -bool ItemFilter::matches(df::dfhack_material_category mask) const -{ - return mask.whole & mat_mask.whole; -} - -bool ItemFilter::matches(DFHack::MaterialInfo &material) const -{ - for (auto it = materials.begin(); it != materials.end(); ++it) - if (material.matches(*it)) - return true; - return false; -} - -bool ItemFilter::matches(df::item *item) const -{ - if (item->getQuality() < min_quality || item->getQuality() > max_quality) - return false; - - if (decorated_only && !item->hasImprovements()) - return false; - - auto imattype = item->getActualMaterial(); - auto imatindex = item->getActualMaterialIndex(); - auto item_mat = DFHack::MaterialInfo(imattype, imatindex); - - return (materials.size() == 0) ? matchesMask(item_mat) : matches(item_mat); -} - - -/* - * PlannedBuilding - */ - -// format: itemfilterser|itemfilterser|... -static std::string serializeFilters(const std::vector &filters) -{ - std::ostringstream ser; - if (!filters.empty()) - { - ser << filters[0].serialize(); - for (size_t i = 1; i < filters.size(); ++i) - ser << "|" << filters[i].serialize(); - } - return ser.str(); -} - -static std::vector deserializeFilters(std::string ser) -{ - std::vector isers; - split_string(&isers, ser, "|"); - std::vector ret; - for (auto & iser : isers) - { - ItemFilter filter; - if (filter.deserialize(iser)) - ret.push_back(filter); - } - return ret; -} - -static size_t getNumFilters(BuildingTypeKey key) -{ - auto L = Lua::Core::State; - color_ostream_proxy out(Core::getInstance().getConsole()); - Lua::StackUnwinder top(L); - - if (!lua_checkstack(L, 4) || !Lua::PushModulePublic( - out, L, "plugins.buildingplan", "get_num_filters")) - { - debug("failed to push the lua method on the stack"); - return 0; - } - - Lua::Push(L, std::get<0>(key)); - Lua::Push(L, std::get<1>(key)); - Lua::Push(L, std::get<2>(key)); - - if (!Lua::SafeCall(out, L, 3, 1)) - { - debug("lua call failed"); - return 0; - } - - int num_filters = lua_tonumber(L, -1); - lua_pop(L, 1); - return num_filters; -} - -PlannedBuilding::PlannedBuilding(df::building *building, const std::vector &filters) - : building(building), - building_id(building->id), - filters(filters) -{ - config = DFHack::World::AddPersistentData(planned_building_persistence_key_v2); - config.ival(0) = building_id; - config.val() = serializeFilters(filters); -} - -PlannedBuilding::PlannedBuilding(PersistentDataItem &config) - : config(config), - building(df::building::find(config.ival(0))), - building_id(config.ival(0)), - filters(deserializeFilters(config.val())) -{ - if (building) - { - if (filters.size() != - getNumFilters(toBuildingTypeKey(building))) - { - debug("invalid ItemFilter vector serialization: '%s'", - config.val().c_str()); - building = NULL; - } - } -} - -// Ensure the building still exists and is in a valid state. It can disappear -// for lots of reasons, such as running the game with the buildingplan plugin -// disabled, manually removing the building, modifying it via the API, etc. -bool PlannedBuilding::isValid() const -{ - return building && df::building::find(building_id) - && building->getBuildStage() == 0; -} - -void PlannedBuilding::remove() -{ - DFHack::World::DeletePersistentData(config); - building = NULL; -} - -df::building * PlannedBuilding::getBuilding() -{ - return building; -} - -const std::vector & PlannedBuilding::getFilters() const -{ - // if we want to be able to dynamically change the filters, we'll need to - // re-bucket the tasks in Planner. - return filters; -} - - -/* - * BuildingTypeKey - */ - -BuildingTypeKey toBuildingTypeKey( - df::building_type btype, int16_t subtype, int32_t custom) -{ - return std::make_tuple(btype, subtype, custom); -} - -BuildingTypeKey toBuildingTypeKey(df::building *bld) -{ - return std::make_tuple( - bld->getType(), bld->getSubtype(), bld->getCustomType()); -} - -BuildingTypeKey toBuildingTypeKey(df::ui_build_selector *uibs) -{ - return std::make_tuple( - uibs->building_type, uibs->building_subtype, uibs->custom_type); -} - -// rotates a size_t value left by count bits -// assumes count is not 0 or >= size_t_bits -// replace this with std::rotl when we move to C++20 -static std::size_t rotl_size_t(size_t val, uint32_t count) -{ - static const int size_t_bits = CHAR_BIT * sizeof(std::size_t); - return val << count | val >> (size_t_bits - count); -} - -std::size_t BuildingTypeKeyHash::operator() (const BuildingTypeKey & key) const -{ - // cast first param to appease gcc-4.8, which is missing the enum - // specializations for std::hash - std::size_t h1 = std::hash()(static_cast(std::get<0>(key))); - std::size_t h2 = std::hash()(std::get<1>(key)); - std::size_t h3 = std::hash()(std::get<2>(key)); - - return h1 ^ rotl_size_t(h2, 8) ^ rotl_size_t(h3, 16); -} - - -/* - * Planner - */ - -// convert v1 persistent data into v2 format -// we can remove this conversion code once v2 has been live for a while -void migrateV1ToV2() -{ - std::vector configs; - DFHack::World::GetPersistentData(&configs, planned_building_persistence_key_v1); - if (configs.empty()) - return; - - debug("migrating %zu persisted configs to new format", configs.size()); - for (auto config : configs) - { - df::building *bld = df::building::find(config.ival(1)); - if (!bld) - { - debug("buliding no longer exists; removing config"); - DFHack::World::DeletePersistentData(config); - continue; - } - - if (bld->getBuildStage() != 0 || bld->jobs.size() != 1 - || bld->jobs[0]->job_items.size() != 1) - { - debug("building in invalid state; removing config"); - DFHack::World::DeletePersistentData(config); - continue; - } - - // fix up the building so we can set the material properties later - bld->mat_type = -1; - bld->mat_index = -1; - - // the v1 filters are not initialized correctly and will match any item. - // we need to fix them up a bit. - auto filter = bld->jobs[0]->job_items[0]; - df::item_type type; - switch (bld->getType()) - { - case df::building_type::Armorstand: type = df::item_type::ARMORSTAND; break; - case df::building_type::Bed: type = df::item_type::BED; break; - case df::building_type::Chair: type = df::item_type::CHAIR; break; - case df::building_type::Coffin: type = df::item_type::COFFIN; break; - case df::building_type::Door: type = df::item_type::DOOR; break; - case df::building_type::Floodgate: type = df::item_type::FLOODGATE; break; - case df::building_type::Hatch: type = df::item_type::HATCH_COVER; break; - case df::building_type::GrateWall: type = df::item_type::GRATE; break; - case df::building_type::GrateFloor: type = df::item_type::GRATE; break; - case df::building_type::BarsVertical: type = df::item_type::BAR; break; - case df::building_type::BarsFloor: type = df::item_type::BAR; break; - case df::building_type::Cabinet: type = df::item_type::CABINET; break; - case df::building_type::Box: type = df::item_type::BOX; break; - case df::building_type::Weaponrack: type = df::item_type::WEAPONRACK; break; - case df::building_type::Statue: type = df::item_type::STATUE; break; - case df::building_type::Slab: type = df::item_type::SLAB; break; - case df::building_type::Table: type = df::item_type::TABLE; break; - case df::building_type::WindowGlass: type = df::item_type::WINDOW; break; - case df::building_type::AnimalTrap: type = df::item_type::ANIMALTRAP; break; - case df::building_type::Chain: type = df::item_type::CHAIN; break; - case df::building_type::Cage: type = df::item_type::CAGE; break; - case df::building_type::TractionBench: type = df::item_type::TRACTION_BENCH; break; - default: - debug("building has unhandled type; removing config"); - DFHack::World::DeletePersistentData(config); - continue; - } - filter->item_type = type; - filter->item_subtype = -1; - filter->mat_type = -1; - filter->mat_index = -1; - filter->flags1.whole = 0; - filter->flags2.whole = 0; - filter->flags2.bits.allow_artifact = true; - filter->flags3.whole = 0; - filter->flags4 = 0; - filter->flags5 = 0; - filter->metal_ore = -1; - filter->min_dimension = -1; - filter->has_tool_use = df::tool_uses::NONE; - filter->quantity = 1; - - std::vector tokens; - split_string(&tokens, config.val(), "/"); - if (tokens.size() != 2) - { - debug("invalid v1 format; removing config"); - DFHack::World::DeletePersistentData(config); - continue; - } - - ItemFilter item_filter; - item_filter.deserializeMaterialMask(tokens[0]); - item_filter.deserializeMaterials(tokens[1]); - item_filter.setMinQuality(config.ival(2) - 1); - item_filter.setMaxQuality(config.ival(4) - 1); - if (config.ival(3) - 1) - item_filter.toggleDecoratedOnly(); - - // create the v2 record - std::vector item_filters; - item_filters.push_back(item_filter); - PlannedBuilding pb(bld, item_filters); - - // remove the v1 record - DFHack::World::DeletePersistentData(config); - debug("v1 %s(%d) record successfully migrated", - ENUM_KEY_STR(building_type, bld->getType()).c_str(), - bld->id); - } -} - -// assumes no setting has '=' or '|' characters -static std::string serialize_settings(std::map & settings) -{ - std::ostringstream ser; - for (auto & entry : settings) - { - ser << entry.first << "=" << (entry.second ? "1" : "0") << "|"; - } - return ser.str(); -} - -static void deserialize_settings(std::map & settings, - std::string ser) -{ - std::vector tokens; - split_string(&tokens, ser, "|"); - for (auto token : tokens) - { - if (token.empty()) - continue; - - std::vector parts; - split_string(&parts, token, "="); - if (parts.size() != 2) - { - debug("invalid serialized setting format: '%s'", token.c_str()); - continue; - } - std::string key = parts[0]; - if (settings.count(key) == 0) - { - debug("unknown serialized setting: '%s", key.c_str()); - continue; - } - settings[key] = static_cast(atoi(parts[1].c_str())); - debug("deserialized setting: %s = %d", key.c_str(), settings[key]); - } -} - -static DFHack::PersistentDataItem init_global_settings( - std::map & settings) -{ - settings.clear(); - settings["blocks"] = true; - settings["boulders"] = true; - settings["logs"] = true; - settings["bars"] = false; - - // load persistent global settings if they exist; otherwise create them - std::vector items; - DFHack::World::GetPersistentData(&items, global_settings_persistence_key); - if (items.size() == 1) - { - DFHack::PersistentDataItem & config = items[0]; - deserialize_settings(settings, config.val()); - return config; - } - - debug("initializing persistent global settings"); - DFHack::PersistentDataItem config = - DFHack::World::AddPersistentData(global_settings_persistence_key); - config.val() = serialize_settings(settings); - return config; -} - -const std::map & Planner::getGlobalSettings() const -{ - return global_settings; -} - -bool Planner::setGlobalSetting(std::string name, bool value) -{ - if (global_settings.count(name) == 0) - { - debug("attempted to set invalid setting: '%s'", name.c_str()); - return false; - } - debug("global setting '%s' %d -> %d", - name.c_str(), global_settings[name], value); - global_settings[name] = value; - if (config.isValid()) - config.val() = serialize_settings(global_settings); - return true; -} - -void Planner::reset() -{ - debug("resetting Planner state"); - default_item_filters.clear(); - planned_buildings.clear(); - tasks.clear(); - - config = init_global_settings(global_settings); - - migrateV1ToV2(); - - std::vector items; - DFHack::World::GetPersistentData(&items, planned_building_persistence_key_v2); - debug("found data for %zu planned building(s)", items.size()); - - for (auto i = items.begin(); i != items.end(); i++) - { - PlannedBuilding pb(*i); - if (!pb.isValid()) - { - debug("discarding invalid planned building"); - pb.remove(); - continue; - } - - if (registerTasks(pb)) - planned_buildings.insert(std::make_pair(pb.getBuilding()->id, pb)); - } -} - -void Planner::addPlannedBuilding(df::building *bld) -{ - auto item_filters = getItemFilters(toBuildingTypeKey(bld)).get(); - // not a supported type - if (item_filters.empty()) - { - debug("failed to add building: unsupported type"); - return; - } - - // protect against multiple registrations - if (planned_buildings.count(bld->id) != 0) - { - debug("failed to add building: already registered"); - return; - } - - PlannedBuilding pb(bld, item_filters); - if (pb.isValid() && registerTasks(pb)) - { - for (auto job : bld->jobs) - job->flags.bits.suspend = true; - - planned_buildings.insert(std::make_pair(bld->id, pb)); - } - else - { - pb.remove(); - } -} - -static std::string getBucket(const df::job_item & ji, - const std::vector & item_filters) -{ - std::ostringstream ser; - - // pull out and serialize only known relevant fields. if we miss a few, then - // the filter bucket will be slighly less specific than it could be, but - // that's probably ok. we'll just end up bucketing slightly different items - // together. this is only a problem if the different filter at the front of - // the queue doesn't match any available items and blocks filters behind it - // that could be matched. - ser << ji.item_type << ':' << ji.item_subtype << ':' << ji.mat_type << ':' - << ji.mat_index << ':' << ji.flags1.whole << ':' << ji.flags2.whole - << ':' << ji.flags3.whole << ':' << ji.flags4 << ':' << ji.flags5 << ':' - << ji.metal_ore << ':' << ji.has_tool_use; - - for (auto & item_filter : item_filters) - { - ser << ':' << item_filter.serialize(); - } - - return ser.str(); -} - -// get a list of item vectors that we should search for matches -static std::vector getVectorIds(df::job_item *job_item, - const std::map & global_settings) -{ - std::vector ret; - - // if the filter already has the vector_id set to something specific, use it - if (job_item->vector_id > df::job_item_vector_id::IN_PLAY) - { - debug("using vector_id from job_item: %s", - ENUM_KEY_STR(job_item_vector_id, job_item->vector_id).c_str()); - ret.push_back(job_item->vector_id); - return ret; - } - - // if the filer is for building material, refer to our global settings for - // which vectors to search - if (job_item->flags2.bits.building_material) - { - if (global_settings.at("blocks")) - ret.push_back(df::job_item_vector_id::BLOCKS); - if (global_settings.at("boulders")) - ret.push_back(df::job_item_vector_id::BOULDER); - if (global_settings.at("logs")) - ret.push_back(df::job_item_vector_id::WOOD); - if (global_settings.at("bars")) - ret.push_back(df::job_item_vector_id::BAR); - } - - // fall back to IN_PLAY if no other vector was appropriate - if (ret.empty()) - ret.push_back(df::job_item_vector_id::IN_PLAY); - return ret; -} - -bool Planner::registerTasks(PlannedBuilding & pb) -{ - df::building * bld = pb.getBuilding(); - if (bld->jobs.size() != 1) - { - debug("unexpected number of jobs: want 1, got %zu", bld->jobs.size()); - return false; - } - auto job_items = bld->jobs[0]->job_items; - int num_job_items = job_items.size(); - if (num_job_items < 1) - { - debug("unexpected number of job items: want >0, got %d", num_job_items); - return false; - } - int32_t id = bld->id; - for (int job_item_idx = 0; job_item_idx < num_job_items; ++job_item_idx) - { - auto job_item = job_items[job_item_idx]; - auto bucket = getBucket(*job_item, pb.getFilters()); - auto vector_ids = getVectorIds(job_item, global_settings); - - // if there are multiple vector_ids, schedule duplicate tasks. after - // the correct number of items are matched, the extras will get popped - // as invalid - for (auto vector_id : vector_ids) - { - for (int item_num = 0; item_num < job_item->quantity; ++item_num) - { - tasks[vector_id][bucket].push(std::make_pair(id, job_item_idx)); - debug("added task: %s/%s/%d,%d; " - "%zu vector(s), %zu filter bucket(s), %zu task(s) in bucket", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket.c_str(), id, job_item_idx, tasks.size(), - tasks[vector_id].size(), tasks[vector_id][bucket].size()); - } - } - } - return true; -} - -PlannedBuilding * Planner::getPlannedBuilding(df::building *bld) -{ - if (!bld || planned_buildings.count(bld->id) == 0) - return NULL; - return &planned_buildings.at(bld->id); -} - -bool Planner::isPlannableBuilding(BuildingTypeKey key) -{ - return getNumFilters(key) >= 1; -} - -Planner::ItemFiltersWrapper Planner::getItemFilters(BuildingTypeKey key) -{ - static std::vector empty_vector; - static const ItemFiltersWrapper empty_ret(empty_vector); - - size_t nfilters = getNumFilters(key); - if (nfilters < 1) - return empty_ret; - while (default_item_filters[key].size() < nfilters) - default_item_filters[key].push_back(ItemFilter()); - return ItemFiltersWrapper(default_item_filters[key]); -} - -// precompute a bitmask with bad item flags -struct BadFlags -{ - uint32_t whole; - - BadFlags() - { - df::item_flags flags; - #define F(x) 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(in_job); - F(owned); F(in_chest); F(removed); F(encased); - #undef F - whole = flags.whole; - } -}; - -static bool itemPassesScreen(df::item * item) -{ - static BadFlags bad_flags; - return !(item->flags.whole & bad_flags.whole) - && !item->isAssignedToStockpile() - // TODO: make this configurable - && !(item->getType() == df::item_type::BOX && item->isBag()); -} - -static bool matchesFilters(df::item * item, - df::job_item * job_item, - const ItemFilter & item_filter) -{ - // check the properties that are not checked by Job::isSuitableItem() - if (job_item->item_type > -1 && job_item->item_type != item->getType()) - return false; - - if (job_item->item_subtype > -1 && - job_item->item_subtype != item->getSubtype()) - return false; - - if (job_item->flags2.bits.building_material && !item->isBuildMat()) - return false; - - if (job_item->metal_ore > -1 && !item->isMetalOre(job_item->metal_ore)) - return false; - - if (job_item->has_tool_use > df::tool_uses::NONE - && !item->hasToolUse(job_item->has_tool_use)) - return false; - - return DFHack::Job::isSuitableItem( - job_item, item->getType(), item->getSubtype()) - && DFHack::Job::isSuitableMaterial( - job_item, item->getMaterial(), item->getMaterialIndex(), - item->getType()) - && item_filter.matches(item); -} - -// note that this just removes the PlannedBuilding. the tasks will get dropped -// as we discover them in the tasks queues and they fail their isValid() check. -// this "lazy" task cleaning algorithm works because there is no way to -// re-register a building once it has been removed -- if it fails isValid() -// then it has either been built or desroyed. therefore there is no chance of -// duplicate tasks getting added to the tasks queues. -void Planner::unregisterBuilding(int32_t id) -{ - if (planned_buildings.count(id) > 0) - { - planned_buildings.at(id).remove(); - planned_buildings.erase(id); - } -} - -static bool isJobReady(df::job * job) -{ - int needed_items = 0; - for (auto job_item : job->job_items) { needed_items += job_item->quantity; } - if (needed_items) - { - debug("building needs %d more item(s)", needed_items); - return false; - } - return true; -} - -static bool job_item_idx_lt(df::job_item_ref *a, df::job_item_ref *b) -{ - // we want the items in the opposite order of the filters - return a->job_item_idx > b->job_item_idx; -} - -// this function does not remove the job_items since their quantity fields are -// now all at 0, so there is no risk of having extra items attached. we don't -// remove them to keep the "finalize with buildingplan active" path as similar -// as possible to the "finalize with buildingplan disabled" path. -static void finalizeBuilding(df::building * bld) -{ - debug("finalizing building %d", bld->id); - auto job = bld->jobs[0]; - - // sort the items so they get added to the structure in the correct order - std::sort(job->items.begin(), job->items.end(), job_item_idx_lt); - - // derive the material properties of the building and job from the first - // applicable item, though if any boulders are involved, it makes the whole - // structure "rough". - bool rough = false; - for (auto attached_item : job->items) - { - df::item *item = attached_item->item; - rough = rough || item->getType() == item_type::BOULDER; - if (bld->mat_type == -1) - { - bld->mat_type = item->getMaterial(); - job->mat_type = bld->mat_type; - } - if (bld->mat_index == -1) - { - bld->mat_index = item->getMaterialIndex(); - job->mat_index = bld->mat_index; - } - } - - if (bld->needsDesign()) - { - auto act = (df::building_actual *)bld; - if (!act->design) - act->design = new df::building_design(); - act->design->flags.bits.rough = rough; - } - - // we're good to go! - job->flags.bits.suspend = false; - Job::checkBuildingsNow(); -} - -void Planner::popInvalidTasks(std::queue> & task_queue) -{ - while (!task_queue.empty()) - { - auto & task = task_queue.front(); - auto id = task.first; - if (planned_buildings.count(id) > 0) - { - PlannedBuilding & pb = planned_buildings.at(id); - if (pb.isValid() && - pb.getBuilding()->jobs[0]->job_items[task.second]->quantity) - { - break; - } - } - debug("discarding invalid task: bld=%d, job_item_idx=%d", - id, task.second); - task_queue.pop(); - unregisterBuilding(id); - } -} - -void Planner::doVector(df::job_item_vector_id vector_id, - std::map>> & buckets) -{ - auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id); - auto item_vector = df::global::world->items.other[other_id]; - debug("matching %zu item(s) in vector %s against %zu filter bucket(s)", - item_vector.size(), - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - buckets.size()); - for (auto item_it = item_vector.rbegin(); - item_it != item_vector.rend(); - ++item_it) - { - auto item = *item_it; - if (!itemPassesScreen(item)) - continue; - for (auto bucket_it = buckets.begin(); bucket_it != buckets.end();) - { - auto & task_queue = bucket_it->second; - popInvalidTasks(task_queue); - if (task_queue.empty()) - { - debug("removing empty bucket: %s/%s; %zu bucket(s) left", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket_it->first.c_str(), - buckets.size() - 1); - bucket_it = buckets.erase(bucket_it); - continue; - } - auto & task = task_queue.front(); - auto id = task.first; - auto & pb = planned_buildings.at(id); - auto building = pb.getBuilding(); - auto job = building->jobs[0]; - auto filter_idx = task.second; - if (matchesFilters(item, job->job_items[filter_idx], - pb.getFilters()[filter_idx]) - && DFHack::Job::attachJobItem(job, item, - df::job_item_ref::Hauled, filter_idx)) - { - MaterialInfo material; - material.decode(item); - ItemTypeInfo item_type; - item_type.decode(item); - debug("attached %s %s to filter %d for %s(%d): %s/%s", - material.toString().c_str(), - item_type.toString().c_str(), - filter_idx, - ENUM_KEY_STR(building_type, building->getType()).c_str(), - id, - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket_it->first.c_str()); - // keep quantity aligned with the actual number of remaining - // items so if buildingplan is turned off, the building will - // be completed with the correct number of items. - --job->job_items[filter_idx]->quantity; - task_queue.pop(); - if (isJobReady(job)) - { - finalizeBuilding(building); - unregisterBuilding(id); - } - if (task_queue.empty()) - { - debug( - "removing empty item bucket: %s/%s; %zu left", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket_it->first.c_str(), - buckets.size() - 1); - buckets.erase(bucket_it); - } - // we found a home for this item; no need to look further - break; - } - ++bucket_it; - } - if (buckets.empty()) - break; - } -} - -struct VectorsToScanLast -{ - std::vector vectors; - VectorsToScanLast() - { - // order is important here. we want to match boulders before wood and - // everything before bars. blocks are not listed here since we'll have - // already scanned them when we did the first pass through the buckets. - vectors.push_back(df::job_item_vector_id::BOULDER); - vectors.push_back(df::job_item_vector_id::WOOD); - vectors.push_back(df::job_item_vector_id::BAR); - } -}; - -void Planner::doCycle() -{ - debug("running cycle for %zu registered building(s)", - planned_buildings.size()); - static const VectorsToScanLast vectors_to_scan_last; - for (auto it = tasks.begin(); it != tasks.end();) - { - auto vector_id = it->first; - // we could make this a set, but it's only three elements - if (std::find(vectors_to_scan_last.vectors.begin(), - vectors_to_scan_last.vectors.end(), - vector_id) != vectors_to_scan_last.vectors.end()) - { - ++it; - continue; - } - - auto & buckets = it->second; - doVector(vector_id, buckets); - if (buckets.empty()) - { - debug("removing empty vector: %s; %zu vector(s) left", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - tasks.size() - 1); - it = tasks.erase(it); - } - else - ++it; - } - for (auto vector_id : vectors_to_scan_last.vectors) - { - if (tasks.count(vector_id) == 0) - continue; - auto & buckets = tasks[vector_id]; - doVector(vector_id, buckets); - if (buckets.empty()) - { - debug("removing empty vector: %s; %zu vector(s) left", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - tasks.size() - 1); - tasks.erase(vector_id); - } - } - debug("cycle done; %zu registered building(s) left", - planned_buildings.size()); -} - -Planner planner; diff --git a/plugins/buildingplan/buildingplan-planner.h b/plugins/buildingplan/buildingplan-planner.h deleted file mode 100644 index 7b1615704..000000000 --- a/plugins/buildingplan/buildingplan-planner.h +++ /dev/null @@ -1,140 +0,0 @@ -#pragma once - -#include -#include - -#include "df/building.h" -#include "df/dfhack_material_category.h" -#include "df/item_quality.h" -#include "df/job_item.h" - -#include "modules/Materials.h" -#include "modules/Persistence.h" - -class ItemFilter -{ -public: - ItemFilter(); - - void clear(); - bool deserialize(std::string ser); - std::string serialize() const; - - void addMaterialMask(uint32_t mask); - void clearMaterialMask(); - void setMaterials(std::vector materials); - - void incMinQuality(); - void decMinQuality(); - void incMaxQuality(); - void decMaxQuality(); - void toggleDecoratedOnly(); - - uint32_t getMaterialMask() const; - std::vector getMaterials() const; - std::string getMinQuality() const; - std::string getMaxQuality() const; - bool getDecoratedOnly() const; - - bool matches(df::dfhack_material_category mask) const; - bool matches(DFHack::MaterialInfo &material) const; - bool matches(df::item *item) const; - -private: - // remove friend declaration when we no longer need v1 deserialization - friend void migrateV1ToV2(); - - df::dfhack_material_category mat_mask; - std::vector materials; - df::item_quality min_quality; - df::item_quality max_quality; - bool decorated_only; - - bool deserializeMaterialMask(std::string ser); - bool deserializeMaterials(std::string ser); - void setMinQuality(int quality); - void setMaxQuality(int quality); - bool matchesMask(DFHack::MaterialInfo &mat) const; -}; - -class PlannedBuilding -{ -public: - PlannedBuilding(df::building *building, const std::vector &filters); - PlannedBuilding(DFHack::PersistentDataItem &config); - - bool isValid() const; - void remove(); - - df::building * getBuilding(); - const std::vector & getFilters() const; - -private: - DFHack::PersistentDataItem config; - df::building *building; - const df::building::key_field_type building_id; - const std::vector filters; -}; - -// building type, subtype, custom -typedef std::tuple BuildingTypeKey; - -BuildingTypeKey toBuildingTypeKey( - df::building_type btype, int16_t subtype, int32_t custom); -BuildingTypeKey toBuildingTypeKey(df::building *bld); -BuildingTypeKey toBuildingTypeKey(df::ui_build_selector *uibs); - -struct BuildingTypeKeyHash -{ - std::size_t operator() (const BuildingTypeKey & key) const; -}; - -class Planner -{ -public: - class ItemFiltersWrapper - { - public: - ItemFiltersWrapper(std::vector & item_filters) - : item_filters(item_filters) { } - std::vector::reverse_iterator rbegin() const { return item_filters.rbegin(); } - std::vector::reverse_iterator rend() const { return item_filters.rend(); } - const std::vector & get() const { return item_filters; } - private: - std::vector &item_filters; - }; - - const std::map & getGlobalSettings() const; - bool setGlobalSetting(std::string name, bool value); - - void reset(); - - void addPlannedBuilding(df::building *bld); - PlannedBuilding *getPlannedBuilding(df::building *bld); - - bool isPlannableBuilding(BuildingTypeKey key); - - // returns an empty vector if the type is not supported - ItemFiltersWrapper getItemFilters(BuildingTypeKey key); - - void doCycle(); - -private: - DFHack::PersistentDataItem config; - std::map global_settings; - std::unordered_map, - BuildingTypeKeyHash> default_item_filters; - // building id -> PlannedBuilding - std::unordered_map planned_buildings; - // vector id -> filter bucket -> queue of (building id, job_item index) - std::map>>> tasks; - - bool registerTasks(PlannedBuilding &plannedBuilding); - void unregisterBuilding(int32_t id); - void popInvalidTasks(std::queue> &task_queue); - void doVector(df::job_item_vector_id vector_id, - std::map>> & buckets); -}; - -extern Planner planner; diff --git a/plugins/buildingplan/buildingplan-rooms.cpp b/plugins/buildingplan/buildingplan-rooms.cpp deleted file mode 100644 index a08c85804..000000000 --- a/plugins/buildingplan/buildingplan-rooms.cpp +++ /dev/null @@ -1,226 +0,0 @@ -#include "buildingplan.h" - -#include -#include -#include - -#include -#include -#include - -using namespace DFHack; - -bool canReserveRoom(df::building *building) -{ - if (!building) - return false; - - if (building->jobs.size() > 0 && building->jobs[0]->job_type == df::job_type::DestroyBuilding) - return false; - - return building->is_room; -} - -std::vector getUniqueNoblePositions(df::unit *unit) -{ - std::vector np; - Units::getNoblePositions(&np, unit); - for (auto iter = np.begin(); iter != np.end(); iter++) - { - if (iter->position->code == "MILITIA_CAPTAIN") - { - np.erase(iter); - break; - } - } - - return np; -} - -/* - * ReservedRoom - */ - -ReservedRoom::ReservedRoom(df::building *building, std::string noble_code) -{ - this->building = building; - config = DFHack::World::AddPersistentData("buildingplan/reservedroom"); - config.val() = noble_code; - config.ival(1) = building->id; - pos = df::coord(building->centerx, building->centery, building->z); -} - -ReservedRoom::ReservedRoom(PersistentDataItem &config, color_ostream &) -{ - this->config = config; - - building = df::building::find(config.ival(1)); - if (!building) - return; - pos = df::coord(building->centerx, building->centery, building->z); -} - -bool ReservedRoom::checkRoomAssignment() -{ - if (!isValid()) - return false; - - auto np = getOwnersNobleCode(); - bool correctOwner = false; - for (auto iter = np.begin(); iter != np.end(); iter++) - { - if (iter->position->code == getCode()) - { - correctOwner = true; - break; - } - } - - if (correctOwner) - return true; - - for (auto iter = df::global::world->units.active.begin(); iter != df::global::world->units.active.end(); iter++) - { - df::unit* unit = *iter; - if (!Units::isCitizen(unit)) - continue; - - if (!Units::isActive(unit)) - continue; - - np = getUniqueNoblePositions(unit); - for (auto iter = np.begin(); iter != np.end(); iter++) - { - if (iter->position->code == getCode()) - { - Buildings::setOwner(building, unit); - break; - } - } - } - - return true; -} - -void ReservedRoom::remove() { DFHack::World::DeletePersistentData(config); } - -bool ReservedRoom::isValid() -{ - if (!building) - return false; - - if (Buildings::findAtTile(pos) != building) - return false; - - return canReserveRoom(building); -} - -int32_t ReservedRoom::getId() -{ - if (!isValid()) - return 0; - - return building->id; -} - -std::string ReservedRoom::getCode() { return config.val(); } - -void ReservedRoom::setCode(const std::string &noble_code) { config.val() = noble_code; } - -std::vector ReservedRoom::getOwnersNobleCode() -{ - if (!building->owner) - return std::vector (); - - return getUniqueNoblePositions(building->owner); -} - -/* - * RoomMonitor - */ - -std::string RoomMonitor::getReservedNobleCode(int32_t buildingId) -{ - for (auto iter = reservedRooms.begin(); iter != reservedRooms.end(); iter++) - { - if (buildingId == iter->getId()) - return iter->getCode(); - } - - return ""; -} - -void RoomMonitor::toggleRoomForPosition(int32_t buildingId, std::string noble_code) -{ - bool found = false; - for (auto iter = reservedRooms.begin(); iter != reservedRooms.end(); iter++) - { - if (buildingId != iter->getId()) - { - continue; - } - else - { - if (noble_code == iter->getCode()) - { - iter->remove(); - reservedRooms.erase(iter); - } - else - { - iter->setCode(noble_code); - } - found = true; - break; - } - } - - if (!found) - { - ReservedRoom room(df::building::find(buildingId), noble_code); - reservedRooms.push_back(room); - } -} - -void RoomMonitor::doCycle() -{ - for (auto iter = reservedRooms.begin(); iter != reservedRooms.end();) - { - if (iter->checkRoomAssignment()) - { - ++iter; - } - else - { - iter->remove(); - iter = reservedRooms.erase(iter); - } - } -} - -void RoomMonitor::reset(color_ostream &out) -{ - reservedRooms.clear(); - std::vector items; - DFHack::World::GetPersistentData(&items, "buildingplan/reservedroom"); - - for (auto i = items.begin(); i != items.end(); i++) - { - ReservedRoom rr(*i, out); - if (rr.isValid()) - addRoom(rr); - } -} - -void RoomMonitor::addRoom(ReservedRoom &rr) -{ - for (auto iter = reservedRooms.begin(); iter != reservedRooms.end(); iter++) - { - if (iter->getId() == rr.getId()) - return; - } - - reservedRooms.push_back(rr); -} - -RoomMonitor roomMonitor; diff --git a/plugins/buildingplan/buildingplan-rooms.h b/plugins/buildingplan/buildingplan-rooms.h deleted file mode 100644 index 3880dbe06..000000000 --- a/plugins/buildingplan/buildingplan-rooms.h +++ /dev/null @@ -1,51 +0,0 @@ -#pragma once - -#include "modules/Persistence.h" -#include "modules/Units.h" - -class ReservedRoom -{ -public: - ReservedRoom(df::building *building, std::string noble_code); - - ReservedRoom(DFHack::PersistentDataItem &config, DFHack::color_ostream &out); - - bool checkRoomAssignment(); - void remove(); - bool isValid(); - - int32_t getId(); - std::string getCode(); - void setCode(const std::string &noble_code); - -private: - df::building *building; - DFHack::PersistentDataItem config; - df::coord pos; - - std::vector getOwnersNobleCode(); -}; - -class RoomMonitor -{ -public: - RoomMonitor() { } - - std::string getReservedNobleCode(int32_t buildingId); - - void toggleRoomForPosition(int32_t buildingId, std::string noble_code); - - void doCycle(); - - void reset(DFHack::color_ostream &out); - -private: - std::vector reservedRooms; - - void addRoom(ReservedRoom &rr); -}; - -bool canReserveRoom(df::building *building); -std::vector getUniqueNoblePositions(df::unit *unit); - -extern RoomMonitor roomMonitor; diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index cd4e84a6e..4fa119ebc 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -1,1168 +1,754 @@ -#include "df/construction_type.h" -#include "df/entity_position.h" -#include "df/interface_key.h" -#include "df/buildreq.h" -#include "df/viewscreen_dwarfmodest.h" - -#include "modules/Gui.h" -#include "modules/Maps.h" -#include "modules/World.h" +#include "buildingplan.h" +#include "buildingtypekey.h" +#include "defaultitemfilters.h" +#include "plannedbuilding.h" -#include "Core.h" +#include "Debug.h" #include "LuaTools.h" #include "PluginManager.h" -#include "../uicommon.h" -#include "../listcolumn.h" -#include "buildingplan.h" +#include "modules/World.h" -DFHACK_PLUGIN("buildingplan"); -#define PLUGIN_VERSION "2.0" -REQUIRE_GLOBAL(plotinfo); -REQUIRE_GLOBAL(ui_build_selector); -REQUIRE_GLOBAL(world); // used in buildingplan library +#include "df/item.h" +#include "df/job_item.h" +#include "df/world.h" -#define MAX_MASK 10 -#define MAX_MATERIAL 21 +using std::map; +using std::string; +using std::unordered_map; +using std::vector; -bool show_help = false; -bool quickfort_mode = false; -bool all_enabled = false; -bool in_dummy_screen = false; -std::unordered_map planmode_enabled; +using namespace DFHack; -bool show_debugging = false; +DFHACK_PLUGIN("buildingplan"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); -void debug(const char *fmt, ...) -{ - if (!show_debugging) - return; +REQUIRE_GLOBAL(world); - color_ostream_proxy out(Core::getInstance().getConsole()); - out.print("DEBUG(buildingplan): "); - va_list args; - va_start(args, fmt); - out.vprint(fmt, args); - va_end(args); - out.print("\n"); +namespace DFHack { + DBG_DECLARE(buildingplan, status, DebugCategory::LINFO); + DBG_DECLARE(buildingplan, cycle, DebugCategory::LINFO); } -class ViewscreenChooseMaterial : public dfhack_viewscreen -{ -public: - ViewscreenChooseMaterial(ItemFilter &filter); - - void feed(set *input); - - void render(); - - std::string getFocusString() { return "buildingplan_choosemat"; } - -private: - ListColumn masks_column; - ListColumn materials_column; - int selected_column; - ItemFilter &filter; - - void addMaskEntry(df::dfhack_material_category &mask, const std::string &text) - { - auto entry = ListEntry(pad_string(text, MAX_MASK, false), mask); - if (filter.matches(mask)) - entry.selected = true; - - masks_column.add(entry); - } - - void populateMasks() - { - masks_column.clear(); - df::dfhack_material_category mask; +static const string CONFIG_KEY = string(plugin_name) + "/config"; +const string FILTER_CONFIG_KEY = string(plugin_name) + "/filter"; +const string BLD_CONFIG_KEY = string(plugin_name) + "/building"; - mask.whole = 0; - mask.bits.stone = true; - addMaskEntry(mask, "Stone"); +int get_config_val(PersistentDataItem &c, int index) { + if (!c.isValid()) + return -1; + return c.ival(index); +} +bool get_config_bool(PersistentDataItem &c, int index) { + return get_config_val(c, index) == 1; +} +void set_config_val(PersistentDataItem &c, int index, int value) { + if (c.isValid()) + c.ival(index) = value; +} +void set_config_bool(PersistentDataItem &c, int index, bool value) { + set_config_val(c, index, value ? 1 : 0); +} - mask.whole = 0; - mask.bits.wood = true; - addMaskEntry(mask, "Wood"); +static PersistentDataItem config; +// for use in counting available materials for the UI +static unordered_map, BuildingTypeKeyHash> job_item_cache; +static unordered_map cur_heat_safety; +static unordered_map cur_item_filters; +// building id -> PlannedBuilding +static unordered_map planned_buildings; +// vector id -> filter bucket -> queue of (building id, job_item index) +static Tasks tasks; + +// note that this just removes the PlannedBuilding. the tasks will get dropped +// as we discover them in the tasks queues and they fail to be found in planned_buildings. +// this "lazy" task cleaning algorithm works because there is no way to +// re-register a building once it has been removed -- if it has been booted out of +// planned_buildings, then it has either been built or desroyed. therefore there is +// no chance of duplicate tasks getting added to the tasks queues. +void PlannedBuilding::remove(color_ostream &out) { + DEBUG(status,out).print("removing persistent data for building %d\n", id); + World::DeletePersistentData(bld_config); + if (planned_buildings.count(id) > 0) + planned_buildings.erase(id); +} - mask.whole = 0; - mask.bits.metal = true; - addMaskEntry(mask, "Metal"); +static const int32_t CYCLE_TICKS = 600; // twice per game day +static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle - mask.whole = 0; - mask.bits.soap = true; - addMaskEntry(mask, "Soap"); +static bool call_buildingplan_lua(color_ostream *out, const char *fn_name, + int nargs = 0, int nres = 0, + Lua::LuaLambda && args_lambda = Lua::DEFAULT_LUA_LAMBDA, + Lua::LuaLambda && res_lambda = Lua::DEFAULT_LUA_LAMBDA) { + DEBUG(status).print("calling buildingplan lua function: '%s'\n", fn_name); - masks_column.filterDisplay(); - } + CoreSuspender guard; - void populateMaterials() - { - materials_column.clear(); - df::dfhack_material_category selected_category; - std::vector selected_masks = masks_column.getSelectedElems(); - if (selected_masks.size() == 1) - selected_category = selected_masks[0]; - else if (selected_masks.size() > 1) - return; - - df::world_raws &raws = world->raws; - for (int i = 1; i < DFHack::MaterialInfo::NUM_BUILTIN; i++) - { - auto obj = raws.mat_table.builtin[i]; - if (obj) - { - MaterialInfo material; - material.decode(i, -1); - addMaterialEntry(selected_category, material, material.toString()); - } - } + auto L = Lua::Core::State; + Lua::StackUnwinder top(L); - for (size_t i = 0; i < raws.inorganics.size(); i++) - { - MaterialInfo material; - material.decode(0, i); - addMaterialEntry(selected_category, material, material.toString()); - } + if (!out) + out = &Core::getInstance().getConsole(); - decltype(selected_category) wood_flag; - wood_flag.bits.wood = true; - if (!selected_category.whole || selected_category.bits.wood) - { - for (size_t i = 0; i < raws.plants.all.size(); i++) - { - df::plant_raw *p = raws.plants.all[i]; - for (size_t j = 0; p->material.size() > 1 && j < p->material.size(); j++) - { - if (p->material[j]->id != "WOOD") - continue; - - MaterialInfo material; - material.decode(DFHack::MaterialInfo::PLANT_BASE+j, i); - auto name = material.toString(); - ListEntry entry(pad_string(name, MAX_MATERIAL, false), material); - if (filter.matches(material)) - entry.selected = true; - - materials_column.add(entry); - } - } - } - materials_column.sort(); - } + return Lua::CallLuaModuleFunction(*out, L, "plugins.buildingplan", fn_name, + nargs, nres, + std::forward(args_lambda), + std::forward(res_lambda)); +} - void addMaterialEntry(df::dfhack_material_category &selected_category, - MaterialInfo &material, std::string name) - { - if (!selected_category.whole || material.matches(selected_category)) - { - ListEntry entry(pad_string(name, MAX_MATERIAL, false), material); - if (filter.matches(material)) - entry.selected = true; +static int get_num_filters(color_ostream &out, BuildingTypeKey key) { + int num_filters = 0; + if (!call_buildingplan_lua(&out, "get_num_filters", 3, 1, + [&](lua_State *L) { + Lua::Push(L, std::get<0>(key)); + Lua::Push(L, std::get<1>(key)); + Lua::Push(L, std::get<2>(key)); + }, + [&](lua_State *L) { + num_filters = lua_tonumber(L, -1); + })) { + return 0; + } + return num_filters; +} - materials_column.add(entry); +static const vector & get_job_items(color_ostream &out, BuildingTypeKey key) { + if (job_item_cache.count(key)) + return job_item_cache[key]; + const int num_filters = get_num_filters(out, key); + auto &jitems = job_item_cache[key]; + for (int index = 0; index < num_filters; ++index) { + bool failed = false; + if (!call_buildingplan_lua(&out, "get_job_item", 4, 1, + [&](lua_State *L) { + Lua::Push(L, std::get<0>(key)); + Lua::Push(L, std::get<1>(key)); + Lua::Push(L, std::get<2>(key)); + Lua::Push(L, index+1); + }, + [&](lua_State *L) { + df::job_item *jitem = Lua::GetDFObject(L, -1); + DEBUG(status,out).print("retrieving job_item for (%d, %d, %d) index=%d: %p\n", + std::get<0>(key), std::get<1>(key), std::get<2>(key), index, jitem); + if (!jitem) + failed = true; + else + jitems.emplace_back(jitem); + }) || failed) { + jitems.clear(); + break; } } - - void validateColumn() - { - set_to_limit(selected_column, 1); - } - - void resize(int32_t x, int32_t y) - { - dfhack_viewscreen::resize(x, y); - masks_column.resize(); - materials_column.resize(); - } -}; - -const DFHack::MaterialInfo &material_info_identity_fn(const DFHack::MaterialInfo &m) { return m; } - -ViewscreenChooseMaterial::ViewscreenChooseMaterial(ItemFilter &filter) - : filter(filter) -{ - selected_column = 0; - masks_column.setTitle("Type"); - masks_column.multiselect = true; - masks_column.allow_search = false; - masks_column.left_margin = 2; - materials_column.left_margin = MAX_MASK + 3; - materials_column.setTitle("Material"); - materials_column.multiselect = true; - - masks_column.changeHighlight(0); - - populateMasks(); - populateMaterials(); - - masks_column.selectDefaultEntry(); - materials_column.selectDefaultEntry(); - materials_column.changeHighlight(0); + return jitems; } -void ViewscreenChooseMaterial::feed(set *input) -{ - bool key_processed = false; - switch (selected_column) - { - case 0: - key_processed = masks_column.feed(input); - if (input->count(interface_key::SELECT)) - populateMaterials(); // Redo materials lists based on category selection - break; - case 1: - key_processed = materials_column.feed(input); - break; - } - - if (key_processed) - return; - - if (input->count(interface_key::LEAVESCREEN)) - { - input->clear(); - Screen::dismiss(this); - return; - } - if (input->count(interface_key::CUSTOM_SHIFT_C)) - { - filter.clear(); - masks_column.clearSelection(); - materials_column.clearSelection(); - populateMaterials(); - } - else if (input->count(interface_key::SEC_SELECT)) - { - // Convert list selections to material filters - filter.clearMaterialMask(); - - // Category masks - auto masks = masks_column.getSelectedElems(); - for (auto it = masks.begin(); it != masks.end(); ++it) - filter.addMaterialMask(it->whole); - - // Specific materials - auto materials = materials_column.getSelectedElems(); - std::vector materialInfos; - transform_(materials, materialInfos, material_info_identity_fn); - filter.setMaterials(materialInfos); - - Screen::dismiss(this); - } - else if (input->count(interface_key::STANDARDSCROLL_LEFT)) - { - --selected_column; - validateColumn(); - } - else if (input->count(interface_key::STANDARDSCROLL_RIGHT)) - { - selected_column++; - validateColumn(); - } - else if (enabler->tracking_on && enabler->mouse_lbut) - { - if (masks_column.setHighlightByMouse()) - selected_column = 0; - else if (materials_column.setHighlightByMouse()) - selected_column = 1; +static HeatSafety get_heat_safety_filter(const BuildingTypeKey &key) { + if (cur_heat_safety.count(key)) + return cur_heat_safety.at(key); + return HEAT_SAFETY_ANY; +} - enabler->mouse_lbut = enabler->mouse_rbut = 0; - } +static DefaultItemFilters & get_item_filters(color_ostream &out, const BuildingTypeKey &key) { + if (cur_item_filters.count(key)) + return cur_item_filters.at(key); + cur_item_filters.emplace(key, DefaultItemFilters(out, key, get_job_items(out, key))); + return cur_item_filters.at(key); } -void ViewscreenChooseMaterial::render() -{ - if (Screen::isDismissed(this)) - return; +static command_result do_command(color_ostream &out, vector ¶meters); +void buildingplan_cycle(color_ostream &out, Tasks &tasks, + unordered_map &planned_buildings); - dfhack_viewscreen::render(); +static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb); - Screen::clear(); - Screen::drawBorder(" Building Material "); +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { + DEBUG(status,out).print("initializing %s\n", plugin_name); - masks_column.display(selected_column == 0); - materials_column.display(selected_column == 1); + // provide a configuration interface for the plugin + commands.push_back(PluginCommand( + plugin_name, + "Plan building placement before you have materials.", + do_command)); - int32_t y = gps->dimy - 3; - int32_t x = 2; - OutputHotkeyString(x, y, "Toggle", interface_key::SELECT); - x += 3; - OutputHotkeyString(x, y, "Save", interface_key::SEC_SELECT); - x += 3; - OutputHotkeyString(x, y, "Clear", interface_key::CUSTOM_SHIFT_C); - x += 3; - OutputHotkeyString(x, y, "Cancel", interface_key::LEAVESCREEN); + return CR_OK; } -//START Viewscreen Hook -static bool is_planmode_enabled(BuildingTypeKey key) -{ - return planmode_enabled[key] || quickfort_mode || all_enabled; +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { + if (enable != is_enabled) { + is_enabled = enable; + DEBUG(status,out).print("%s from the API; persisting\n", + is_enabled ? "enabled" : "disabled"); + } else { + DEBUG(status,out).print("%s from the API, but already %s; no action\n", + is_enabled ? "enabled" : "disabled", + is_enabled ? "enabled" : "disabled"); + } + return CR_OK; } -static std::string get_item_label(const BuildingTypeKey &key, int item_idx) -{ - auto L = Lua::Core::State; - color_ostream_proxy out(Core::getInstance().getConsole()); - Lua::StackUnwinder top(L); - - if (!lua_checkstack(L, 5) || - !Lua::PushModulePublic( - out, L, "plugins.buildingplan", "get_item_label")) - return "Failed push"; +DFhackCExport command_result plugin_shutdown (color_ostream &out) { + DEBUG(status,out).print("shutting down %s\n", plugin_name); - Lua::Push(L, std::get<0>(key)); - Lua::Push(L, std::get<1>(key)); - Lua::Push(L, std::get<2>(key)); - Lua::Push(L, item_idx); - - if (!Lua::SafeCall(out, L, 4, 1)) - return "Failed call"; - - const char *s = lua_tostring(L, -1); - if (!s) - return "No string"; - - return s; + return CR_OK; } -static bool item_can_be_improved(const BuildingTypeKey &key, int item_idx) -{ - auto L = Lua::Core::State; - color_ostream_proxy out(Core::getInstance().getConsole()); - Lua::StackUnwinder top(L); - - if (!lua_checkstack(L, 5) || - !Lua::PushModulePublic( - out, L, "plugins.buildingplan", "item_can_be_improved")) - return false; +static void validate_config(color_ostream &out, bool verbose = false) { + if (get_config_bool(config, CONFIG_BLOCKS) + || get_config_bool(config, CONFIG_BOULDERS) + || get_config_bool(config, CONFIG_LOGS) + || get_config_bool(config, CONFIG_BARS)) + return; - Lua::Push(L, std::get<0>(key)); - Lua::Push(L, std::get<1>(key)); - Lua::Push(L, std::get<2>(key)); - Lua::Push(L, item_idx); + if (verbose) + out.printerr("all contruction materials disabled; resetting config\n"); - if (!Lua::SafeCall(out, L, 4, 1)) - return false; - - return lua_toboolean(L, -1); + set_config_bool(config, CONFIG_BLOCKS, true); + set_config_bool(config, CONFIG_BOULDERS, true); + set_config_bool(config, CONFIG_LOGS, true); + set_config_bool(config, CONFIG_BARS, false); } -static bool construct_planned_building() -{ - auto L = Lua::Core::State; - color_ostream_proxy out(Core::getInstance().getConsole()); - - CoreSuspendClaimer suspend; - Lua::StackUnwinder top(L); - - if (!(lua_checkstack(L, 1) && - Lua::PushModulePublic(out, L, "plugins.buildingplan", - "construct_buildings_from_ui_state") && - Lua::SafeCall(out, L, 0, 1))) - { - return false; - } - - // register all returned buildings with planner - lua_pushnil(L); - while (lua_next(L, -2) != 0) - { - auto bld = Lua::GetDFObject(L, -1); - if (!bld) - { - out.printerr( - "buildingplan: construct_buildings_from_ui_state() failed\n"); - return false; +static void clear_state(color_ostream &out) { + call_buildingplan_lua(&out, "signal_reset"); + call_buildingplan_lua(&out, "reload_cursors"); + planned_buildings.clear(); + tasks.clear(); + cur_heat_safety.clear(); + cur_item_filters.clear(); + for (auto &entry : job_item_cache ) { + for (auto &jitem : entry.second) { + delete jitem; } - - planner.addPlannedBuilding(bld); - lua_pop(L, 1); } - - return true; + job_item_cache.clear(); } -static void show_global_settings_dialog() -{ - auto L = Lua::Core::State; - color_ostream_proxy out(Core::getInstance().getConsole()); - Lua::StackUnwinder top(L); +DFhackCExport command_result plugin_load_data (color_ostream &out) { + cycle_timestamp = 0; + config = World::GetPersistentData(CONFIG_KEY); - if (!lua_checkstack(L, 2) || - !Lua::PushModulePublic( - out, L, "plugins.buildingplan", "show_global_settings_dialog")) - { - debug("Failed to push the module"); - return; + if (!config.isValid()) { + DEBUG(status,out).print("no config found in this save; initializing\n"); + config = World::AddPersistentData(CONFIG_KEY); } + validate_config(out); - lua_newtable(L); - int ctable = lua_gettop(L); - Lua::SetField(L, quickfort_mode, ctable, "quickfort_mode"); - Lua::SetField(L, all_enabled, ctable, "all_enabled"); + DEBUG(status,out).print("loading persisted state\n"); + clear_state(out); - for (auto & setting : planner.getGlobalSettings()) - { - Lua::SetField(L, setting.second, ctable, setting.first.c_str()); + vector filter_configs; + World::GetPersistentData(&filter_configs, FILTER_CONFIG_KEY); + for (auto &cfg : filter_configs) { + BuildingTypeKey key = DefaultItemFilters::getKey(cfg); + cur_item_filters.emplace(key, DefaultItemFilters(out, cfg, get_job_items(out, key))); } - if (!Lua::SafeCall(out, L, 1, 0)) - { - debug("Failed call to show_global_settings_dialog"); - return; - } -} - -static bool is_automaterial_enabled() -{ - auto L = Lua::Core::State; - color_ostream_proxy out(Core::getInstance().getConsole()); - Lua::StackUnwinder top(L); - - if (!(lua_checkstack(L, 1) && - Lua::PushModulePublic(out, L, "plugins.automaterial", "isEnabled") && - Lua::SafeCall(out, L, 0, 1))) - { - return false; + vector building_configs; + World::GetPersistentData(&building_configs, BLD_CONFIG_KEY); + const size_t num_building_configs = building_configs.size(); + for (size_t idx = 0; idx < num_building_configs; ++idx) { + PlannedBuilding pb(out, building_configs[idx]); + df::building *bld = df::building::find(pb.id); + if (!bld) { + INFO(status).print("building %d no longer exists; skipping\n", pb.id); + pb.remove(out); + continue; + } + BuildingTypeKey key(bld->getType(), bld->getSubtype(), bld->getCustomType()); + if (pb.item_filters.size() != get_item_filters(out, key).getItemFilters().size()) { + WARN(status).print("loaded state for building %d doesn't match world\n", pb.id); + pb.remove(out); + continue; + } + registerPlannedBuilding(out, pb); } - return lua_toboolean(L, -1); -} - -static bool is_automaterial_managed(df::building_type type, int16_t subtype) -{ - return is_automaterial_enabled() - && type == df::building_type::Construction - && subtype < df::construction_type::TrackN; + return CR_OK; } -struct buildingplan_query_hook : public df::viewscreen_dwarfmodest -{ - typedef df::viewscreen_dwarfmodest interpose_base; - - // no non-static fields allowed (according to VTableInterpose.h) - static df::building *bld; - static PlannedBuilding *pb; - static int filter_count; - static int filter_idx; - - // logic is reversed since we're starting at the last filter - bool hasNextFilter() const { return filter_idx > 0; } - bool hasPrevFilter() const { return filter_idx + 1 < filter_count; } - - bool isInPlannedBuildingQueryMode() - { - return (plotinfo->main.mode == df::ui_sidebar_mode::QueryBuilding || - plotinfo->main.mode == df::ui_sidebar_mode::BuildingItems) && - planner.getPlannedBuilding(world->selected_building); +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { + if (event == SC_WORLD_UNLOADED) { + DEBUG(status,out).print("world unloaded; clearing state for %s\n", plugin_name); + clear_state(out); } + return CR_OK; +} - // reinit static fields when selected building changes - void initStatics() - { - df::building *cur_bld = world->selected_building; - if (bld != cur_bld) - { - bld = cur_bld; - pb = planner.getPlannedBuilding(bld); - filter_count = pb->getFilters().size(); - filter_idx = filter_count - 1; - } - } +static bool cycle_requested = false; - static void invalidateStatics() - { - bld = NULL; - } +static void do_cycle(color_ostream &out) { + // mark that we have recently run + cycle_timestamp = world->frame_counter; + cycle_requested = false; - bool handleInput(set *input) - { - if (!isInPlannedBuildingQueryMode() || Gui::inRenameBuilding()) - return false; - - initStatics(); - - if (input->count(interface_key::SUSPENDBUILDING)) - return true; // Don't unsuspend planned buildings - if (input->count(interface_key::DESTROYBUILDING)) - { - // remove persistent data - pb->remove(); - // still allow the building to be removed - return false; - } + buildingplan_cycle(out, tasks, planned_buildings); + call_buildingplan_lua(&out, "signal_reset"); +} - // ctrl+Right - if (input->count(interface_key::A_MOVE_E_DOWN) && hasNextFilter()) - --filter_idx; - // ctrl+Left - else if (input->count(interface_key::A_MOVE_W_DOWN) && hasPrevFilter()) - ++filter_idx; - else - return false; - return true; - } +DFhackCExport command_result plugin_onupdate(color_ostream &out) { + if (!Core::getInstance().isWorldLoaded()) + return CR_OK; - DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) - { - if (!handleInput(input)) - INTERPOSE_NEXT(feed)(input); - } + if (is_enabled && + (cycle_requested || world->frame_counter - cycle_timestamp >= CYCLE_TICKS)) + do_cycle(out); + return CR_OK; +} - static bool is_filter_satisfied(df::building *bld, int filter_idx) - { - if (!bld - || bld->jobs.size() < 1 - || int(bld->jobs[0]->job_items.size()) <= filter_idx) - return false; +static command_result do_command(color_ostream &out, vector ¶meters) { + CoreSuspender suspend; - // if all items for this filter are attached, the quantity will be 0 - return bld->jobs[0]->job_items[filter_idx]->quantity == 0; + if (!Core::getInstance().isWorldLoaded()) { + out.printerr("Cannot configure %s without a loaded world.\n", plugin_name); + return CR_FAILURE; } - DEFINE_VMETHOD_INTERPOSE(void, render, ()) - { - INTERPOSE_NEXT(render)(); - - if (!isInPlannedBuildingQueryMode()) - return; - - initStatics(); - - // Hide suspend toggle option - auto dims = Gui::getDwarfmodeViewDims(); - int left_margin = dims.menu_x1 + 1; - int x = left_margin; - int y = 20; - Screen::Pen pen(' ', COLOR_BLACK); - Screen::fillRect(pen, x, y, dims.menu_x2, y); - - bool attached = is_filter_satisfied(pb->getBuilding(), filter_idx); - - auto & filter = pb->getFilters()[filter_idx]; - y = 24; - std::string item_label = - stl_sprintf("Item %d of %d (%s)", filter_count - filter_idx, filter_count, attached ? "attached" : "pending"); - OutputString(COLOR_WHITE, x, y, "Planned Building Filter", true, left_margin + 1); - OutputString(COLOR_WHITE, x, y, item_label.c_str(), true, left_margin + 1); - OutputString(COLOR_WHITE, x, y, get_item_label(toBuildingTypeKey(bld), filter_idx).c_str(), true, left_margin); - ++y; - if (item_can_be_improved(toBuildingTypeKey(bld), filter_idx)) - { - OutputString(COLOR_BROWN, x, y, "Min Quality: ", false, left_margin); - OutputString(COLOR_BLUE, x, y, filter.getMinQuality(), true, left_margin); - OutputString(COLOR_BROWN, x, y, "Max Quality: ", false, left_margin); - OutputString(COLOR_BLUE, x, y, filter.getMaxQuality(), true, left_margin); - if (filter.getDecoratedOnly()) - OutputString(COLOR_BLUE, x, y, "Decorated Only", true, left_margin); - } - - OutputString(COLOR_BROWN, x, y, "Materials:", true, left_margin); - auto filters = filter.getMaterials(); - for (auto it = filters.begin(); it != filters.end(); ++it) - OutputString(COLOR_BLUE, x, y, "*" + *it, true, left_margin); - - ++y; - if (hasPrevFilter()) - OutputHotkeyString(x, y, "Prev Item", "Ctrl+Left", true, left_margin, COLOR_WHITE, COLOR_LIGHTRED); - if (hasNextFilter()) - OutputHotkeyString(x, y, "Next Item", "Ctrl+Right", true, left_margin, COLOR_WHITE, COLOR_LIGHTRED); + bool show_help = false; + if (!call_buildingplan_lua(&out, "parse_commandline", parameters.size(), 1, + [&](lua_State *L) { + for (const string ¶m : parameters) + Lua::Push(L, param); + }, + [&](lua_State *L) { + show_help = !lua_toboolean(L, -1); + })) { + return CR_FAILURE; } -}; - -df::building * buildingplan_query_hook::bld; -PlannedBuilding * buildingplan_query_hook::pb; -int buildingplan_query_hook::filter_count; -int buildingplan_query_hook::filter_idx; - -struct buildingplan_place_hook : public df::viewscreen_dwarfmodest -{ - typedef df::viewscreen_dwarfmodest interpose_base; - // no non-static fields allowed (according to VTableInterpose.h) - static BuildingTypeKey key; - static std::vector::reverse_iterator filter_rbegin; - static std::vector::reverse_iterator filter_rend; - static std::vector::reverse_iterator filter; - static int filter_count; - static int filter_idx; + return show_help ? CR_WRONG_USAGE : CR_OK; +} - bool hasNextFilter() const { return filter + 1 != filter_rend; } - bool hasPrevFilter() const { return filter != filter_rbegin; } +///////////////////////////////////////////////////// +// Lua API +// core will already be suspended when coming in through here +// + +static string getBucket(const df::job_item & ji) { + std::ostringstream ser; + + // pull out and serialize only known relevant fields. if we miss a few, then + // the filter bucket will be slighly less specific than it could be, but + // that's probably ok. we'll just end up bucketing slightly different items + // together. this is only a problem if the different filter at the front of + // the queue doesn't match any available items and blocks filters behind it + // that could be matched. + ser << ji.item_type << ':' << ji.item_subtype << ':' << ji.mat_type << ':' + << ji.mat_index << ':' << ji.flags1.whole << ':' << ji.flags2.whole + << ':' << ji.flags3.whole << ':' << ji.flags4 << ':' << ji.flags5 << ':' + << ji.metal_ore << ':' << ji.has_tool_use; + + return ser.str(); +} - bool isInPlannedBuildingPlacementMode() - { - return plotinfo->main.mode == ui_sidebar_mode::Build && - df::global::ui_build_selector && - df::global::ui_build_selector->stage < 2 && - planner.isPlannableBuilding(toBuildingTypeKey(ui_build_selector)); - } +// get a list of item vectors that we should search for matches +vector getVectorIds(color_ostream &out, const df::job_item *job_item) { + std::vector ret; - // reinit static fields when selected building type changes - void initStatics() + // if the filter already has the vector_id set to something specific, use it + if (job_item->vector_id > df::job_item_vector_id::IN_PLAY) { - BuildingTypeKey cur_key = toBuildingTypeKey(ui_build_selector); - if (key != cur_key) - { - key = cur_key; - auto wrapper = planner.getItemFilters(key); - filter_rbegin = wrapper.rbegin(); - filter_rend = wrapper.rend(); - filter = filter_rbegin; - filter_count = wrapper.get().size(); - filter_idx = filter_count - 1; - } + DEBUG(status,out).print("using vector_id from job_item: %s\n", + ENUM_KEY_STR(job_item_vector_id, job_item->vector_id).c_str()); + ret.push_back(job_item->vector_id); + return ret; } - static void invalidateStatics() + // if the filer is for building material, refer to our global settings for + // which vectors to search + if (job_item->flags2.bits.building_material) { - key = BuildingTypeKey(); + if (get_config_bool(config, CONFIG_BLOCKS)) + ret.push_back(df::job_item_vector_id::BLOCKS); + if (get_config_bool(config, CONFIG_BOULDERS)) + ret.push_back(df::job_item_vector_id::BOULDER); + if (get_config_bool(config, CONFIG_LOGS)) + ret.push_back(df::job_item_vector_id::WOOD); + if (get_config_bool(config, CONFIG_BARS)) + ret.push_back(df::job_item_vector_id::BAR); } - bool handleInput(set *input) - { - if (!isInPlannedBuildingPlacementMode()) - { - show_help = false; - return false; - } - - initStatics(); - - if (in_dummy_screen) - { - if (input->count(interface_key::SELECT) || input->count(interface_key::SEC_SELECT) - || input->count(interface_key::LEAVESCREEN)) - { - in_dummy_screen = false; - // pass LEAVESCREEN up to parent view - input->clear(); - input->insert(interface_key::LEAVESCREEN); - return false; - } - return true; - } - - if (input->count(interface_key::CUSTOM_P) || - input->count(interface_key::CUSTOM_G) || - input->count(interface_key::CUSTOM_D) || - input->count(interface_key::CUSTOM_Q) || - input->count(interface_key::CUSTOM_W) || - input->count(interface_key::CUSTOM_A) || - input->count(interface_key::CUSTOM_S) || - input->count(interface_key::CUSTOM_M)) - { - show_help = true; - } - - if (!quickfort_mode && !all_enabled - && input->count(interface_key::CUSTOM_SHIFT_P)) - { - planmode_enabled[key] = !planmode_enabled[key]; - if (!is_planmode_enabled(key)) - Gui::refreshSidebar(); - return true; - } - if (input->count(interface_key::CUSTOM_SHIFT_G)) - { - show_global_settings_dialog(); - return true; - } - - if (!is_planmode_enabled(key)) - return false; - - // if automaterial is enabled, let it handle building allocation and - // registration with planner - if (input->count(interface_key::SELECT) && - !is_automaterial_managed(ui_build_selector->building_type, - ui_build_selector->building_subtype)) - { - if (ui_build_selector->errors.size() == 0 && construct_planned_building()) - { - Gui::refreshSidebar(); - if (quickfort_mode) - in_dummy_screen = true; - } - return true; - } - - - - if (input->count(interface_key::CUSTOM_SHIFT_M)) - Screen::show(dts::make_unique(*filter), plugin_self); + // fall back to IN_PLAY if no other vector was appropriate + if (ret.empty()) + ret.push_back(df::job_item_vector_id::IN_PLAY); + return ret; +} - if (item_can_be_improved(key, filter_idx)) - { - if (input->count(interface_key::CUSTOM_SHIFT_Q)) - filter->decMinQuality(); - else if (input->count(interface_key::CUSTOM_SHIFT_W)) - filter->incMinQuality(); - else if (input->count(interface_key::CUSTOM_SHIFT_A)) - filter->decMaxQuality(); - else if (input->count(interface_key::CUSTOM_SHIFT_S)) - filter->incMaxQuality(); - else if (input->count(interface_key::CUSTOM_SHIFT_D)) - filter->toggleDecoratedOnly(); - } +static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb) { + df::building * bld = pb.getBuildingIfValidOrRemoveIfNot(out); + if (!bld) + return false; - // ctrl+Right - if (input->count(interface_key::A_MOVE_E_DOWN) && hasNextFilter()) - { - ++filter; - --filter_idx; - } - // ctrl+Left - else if (input->count(interface_key::A_MOVE_W_DOWN) && hasPrevFilter()) - { - --filter; - ++filter_idx; - } - else - return false; - return true; + if (bld->jobs.size() != 1) { + DEBUG(status,out).print("unexpected number of jobs: want 1, got %zu\n", bld->jobs.size()); + return false; } - DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) - { - if (!handleInput(input)) - INTERPOSE_NEXT(feed)(input); + auto job_items = bld->jobs[0]->job_items; + if (isJobReady(out, job_items)) { + // all items are already attached + finalizeBuilding(out, bld); + return true; } - DEFINE_VMETHOD_INTERPOSE(void, render, ()) - { - initStatics(); - - bool plannable = isInPlannedBuildingPlacementMode(); - if (plannable && is_planmode_enabled(key)) - { - if (ui_build_selector->stage < 1) - // No materials but turn on cursor - ui_build_selector->stage = 1; - - for (auto iter = ui_build_selector->errors.begin(); - iter != ui_build_selector->errors.end();) - { - // FIXME Hide bags - if (((*iter)->find("Needs") != string::npos - && **iter != "Needs adjacent wall") - || (*iter)->find("No access") != string::npos) - iter = ui_build_selector->errors.erase(iter); - else - ++iter; + int num_job_items = job_items.size(); + int32_t id = bld->id; + for (int job_item_idx = 0; job_item_idx < num_job_items; ++job_item_idx) { + auto job_item = job_items[job_item_idx]; + auto bucket = getBucket(*job_item); + + // if there are multiple vector_ids, schedule duplicate tasks. after + // the correct number of items are matched, the extras will get popped + // as invalid + for (auto vector_id : pb.vector_ids[job_item_idx]) { + for (int item_num = 0; item_num < job_item->quantity; ++item_num) { + tasks[vector_id][bucket].emplace_back(id, job_item_idx); + DEBUG(status,out).print("added task: %s/%s/%d,%d; " + "%zu vector(s), %zu filter bucket(s), %zu task(s) in bucket", + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + bucket.c_str(), id, job_item_idx, tasks.size(), + tasks[vector_id].size(), tasks[vector_id][bucket].size()); } } - - INTERPOSE_NEXT(render)(); - - if (!plannable) - return; - - auto dims = Gui::getDwarfmodeViewDims(); - int left_margin = dims.menu_x1 + 1; - int x = left_margin; - - if (in_dummy_screen) - { - Screen::Pen pen(' ',COLOR_BLACK); - int y = dims.y1 + 1; - Screen::fillRect(pen, x, y, dims.menu_x2, y + 20); - - ++y; - - OutputString(COLOR_BROWN, x, y, - "Placeholder for legacy Quickfort. This screen is not required for DFHack native quickfort.", - true, left_margin); - OutputString(COLOR_WHITE, x, y, "Enter, Shift-Enter or Esc", true, left_margin); - return; - } - - int y = 23; - - if (is_automaterial_managed(ui_build_selector->building_type, - ui_build_selector->building_subtype)) - { - // avoid conflict with the automaterial plugin UI - y = 36; - } - - if (show_help) - { - OutputString(COLOR_BROWN, x, y, "Note: "); - OutputString(COLOR_WHITE, x, y, "Use Shift-Keys here", true, left_margin); - } - - OutputHotkeyString(x, y, "Planning Mode", interface_key::CUSTOM_SHIFT_P); - OutputString(COLOR_WHITE, x, y, ": "); - if (quickfort_mode) - OutputString(COLOR_YELLOW, x, y, "Quickfort", true, left_margin); - else if (all_enabled) - OutputString(COLOR_YELLOW, x, y, "All", true, left_margin); - else if (planmode_enabled[key]) - OutputString(COLOR_GREEN, x, y, "On", true, left_margin); - else - OutputString(COLOR_GREY, x, y, "Off", true, left_margin); - OutputHotkeyString(x, y, "Global Settings", interface_key::CUSTOM_SHIFT_G, - true, left_margin, COLOR_WHITE, COLOR_LIGHTRED); - - if (!is_planmode_enabled(key)) - return; - - y += 2; - std::string title = - stl_sprintf("Filter for Item %d of %d:", - filter_count - filter_idx, filter_count); - OutputString(COLOR_WHITE, x, y, title.c_str(), true, left_margin + 1); - OutputString(COLOR_WHITE, x, y, get_item_label(key, filter_idx).c_str(), true, left_margin); - - if (item_can_be_improved(key, filter_idx)) - { - OutputHotkeyString(x, y, "Min Quality: ", "QW", false, 0, COLOR_WHITE, COLOR_LIGHTRED); - OutputString(COLOR_BROWN, x, y, filter->getMinQuality(), true, left_margin); - - OutputHotkeyString(x, y, "Max Quality: ", "AS", false, 0, COLOR_WHITE, COLOR_LIGHTRED); - OutputString(COLOR_BROWN, x, y, filter->getMaxQuality(), true, left_margin); - - OutputToggleString(x, y, "Decorated Only", interface_key::CUSTOM_SHIFT_D, - filter->getDecoratedOnly(), true, left_margin, COLOR_WHITE, COLOR_LIGHTRED); - } - - OutputHotkeyString(x, y, "Material Filter:", interface_key::CUSTOM_SHIFT_M, true, - left_margin, COLOR_WHITE, COLOR_LIGHTRED); - auto filter_descriptions = filter->getMaterials(); - for (auto it = filter_descriptions.begin(); - it != filter_descriptions.end(); ++it) - OutputString(COLOR_BROWN, x, y, " *" + *it, true, left_margin); - - y += 2; - if (hasPrevFilter()) - OutputHotkeyString(x, y, "Prev Item", "Ctrl+Left", true, - left_margin, COLOR_WHITE, COLOR_LIGHTRED); - if (hasNextFilter()) - OutputHotkeyString(x, y, "Next Item", "Ctrl+Right", true, - left_margin, COLOR_WHITE, COLOR_LIGHTRED); } -}; - -BuildingTypeKey buildingplan_place_hook::key; -std::vector::reverse_iterator buildingplan_place_hook::filter_rbegin; -std::vector::reverse_iterator buildingplan_place_hook::filter_rend; -std::vector::reverse_iterator buildingplan_place_hook::filter; -int buildingplan_place_hook::filter_count; -int buildingplan_place_hook::filter_idx; -struct buildingplan_room_hook : public df::viewscreen_dwarfmodest -{ - typedef df::viewscreen_dwarfmodest interpose_base; + // suspend jobs + for (auto job : bld->jobs) + job->flags.bits.suspend = true; - std::vector getNoblePositionOfSelectedBuildingOwner() - { - std::vector np; - if (plotinfo->main.mode != df::ui_sidebar_mode::QueryBuilding || - !world->selected_building || - !world->selected_building->owner) - { - return np; - } + // add the planned buildings to our register + planned_buildings.emplace(bld->id, pb); - switch (world->selected_building->getType()) - { - case building_type::Bed: - case building_type::Chair: - case building_type::Table: - break; - default: - return np; - } + return true; +} - return getUniqueNoblePositions(world->selected_building->owner); - } +static string get_desc_string(color_ostream &out, df::job_item *jitem, + const vector &vec_ids) { + vector descs; + for (auto &vec_id : vec_ids) { + df::job_item jitem_copy = *jitem; + jitem_copy.vector_id = vec_id; + call_buildingplan_lua(&out, "get_desc", 1, 1, + [&](lua_State *L) { Lua::Push(L, &jitem_copy); }, + [&](lua_State *L) { + descs.emplace_back(lua_tostring(L, -1)); }); + } + return join_strings(" or ", descs); +} - bool isInNobleRoomQueryMode() - { - if (getNoblePositionOfSelectedBuildingOwner().size() > 0) - return canReserveRoom(world->selected_building); - else - return false; - } +static void printStatus(color_ostream &out) { + DEBUG(status,out).print("entering buildingplan_printStatus\n"); + out.print("buildingplan is %s\n\n", is_enabled ? "enabled" : "disabled"); + out.print("Current settings:\n"); + out.print(" use blocks: %s\n", get_config_bool(config, CONFIG_BLOCKS) ? "yes" : "no"); + out.print(" use boulders: %s\n", get_config_bool(config, CONFIG_BOULDERS) ? "yes" : "no"); + out.print(" use logs: %s\n", get_config_bool(config, CONFIG_LOGS) ? "yes" : "no"); + out.print(" use bars: %s\n", get_config_bool(config, CONFIG_BARS) ? "yes" : "no"); + out.print("\n"); - bool handleInput(set *input) - { - if (!isInNobleRoomQueryMode()) - return false; - - if (Gui::inRenameBuilding()) - return false; - auto np = getNoblePositionOfSelectedBuildingOwner(); - df::interface_key last_token = get_string_key(input); - if (last_token >= Screen::charToKey('1') - && last_token <= Screen::charToKey('9')) - { - size_t index = last_token - Screen::charToKey('1'); - if (index >= np.size()) - return false; - roomMonitor.toggleRoomForPosition(world->selected_building->id, np.at(index).position->code); - return true; + map counts; + int32_t total = 0; + for (auto &entry : planned_buildings) { + auto &pb = entry.second; + auto bld = pb.getBuildingIfValidOrRemoveIfNot(out); + if (!bld || bld->jobs.size() != 1) + continue; + auto &job_items = bld->jobs[0]->job_items; + if (job_items.size() != pb.vector_ids.size()) + continue; + int job_item_idx = 0; + for (auto &vec_ids : pb.vector_ids) { + auto &jitem = job_items[job_item_idx++]; + int32_t quantity = jitem->quantity; + if (quantity) { + counts[get_desc_string(out, jitem, vec_ids)] += quantity; + total += quantity; + } } - - return false; } - DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) - { - if (!handleInput(input)) - INTERPOSE_NEXT(feed)(input); + if (planned_buildings.size()) { + out.print("Waiting for %d item(s) to be produced for %zd building(s):\n", + total, planned_buildings.size()); + for (auto &count : counts) + out.print(" %3d %s%s\n", count.second, count.first.c_str(), count.second == 1 ? "" : "s"); + } else { + out.print("Currently no planned buildings\n"); } + out.print("\n"); +} - DEFINE_VMETHOD_INTERPOSE(void, render, ()) - { - INTERPOSE_NEXT(render)(); - - if (!isInNobleRoomQueryMode()) - return; - - auto np = getNoblePositionOfSelectedBuildingOwner(); - auto dims = Gui::getDwarfmodeViewDims(); - int left_margin = dims.menu_x1 + 1; - int x = left_margin; - int y = 24; - OutputString(COLOR_BROWN, x, y, "DFHack", true, left_margin); - OutputString(COLOR_WHITE, x, y, "Auto-allocate to:", true, left_margin); - for (size_t i = 0; i < np.size() && i < 9; i++) - { - bool enabled = - roomMonitor.getReservedNobleCode(world->selected_building->id) - == np[i].position->code; - OutputToggleString(x, y, np[i].position->name[0].c_str(), - int_to_string(i+1).c_str(), enabled, true, left_margin); - } +static bool setSetting(color_ostream &out, string name, bool value) { + DEBUG(status,out).print("entering setSetting (%s -> %s)\n", name.c_str(), value ? "true" : "false"); + if (name == "blocks") + set_config_bool(config, CONFIG_BLOCKS, value); + else if (name == "boulders") + set_config_bool(config, CONFIG_BOULDERS, value); + else if (name == "logs") + set_config_bool(config, CONFIG_LOGS, value); + else if (name == "bars") + set_config_bool(config, CONFIG_BARS, value); + else { + out.printerr("unrecognized setting: '%s'\n", name.c_str()); + return false; } -}; - -IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_query_hook, feed); -IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_place_hook, feed); -IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_room_hook, feed); -IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_query_hook, render); -IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_place_hook, render); -IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_room_hook, render); -DFHACK_PLUGIN_IS_ENABLED(is_enabled); - -static bool setSetting(std::string name, bool value); - -static bool isTrue(std::string val) -{ - val = toLower(val); - return val == "on" || val == "true" || val == "y" || val == "yes" - || val == "1"; + validate_config(out, true); + call_buildingplan_lua(&out, "signal_reset"); + return true; } -static command_result buildingplan_cmd(color_ostream &out, vector & parameters) -{ - if (parameters.empty()) - return CR_OK; - - std::string cmd = toLower(parameters[0]); +static bool isPlannableBuilding(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom) { + DEBUG(status,out).print("entering isPlannableBuilding\n"); + return get_num_filters(out, BuildingTypeKey(type, subtype, custom)) >= 1; +} - if (cmd.size() >= 1 && cmd[0] == 'v') - { - out.print("buildingplan version: %s\n", PLUGIN_VERSION); - } - else if (parameters.size() >= 2 && cmd == "debug") - { - show_debugging = isTrue(parameters[1]); - out.print("buildingplan debugging: %s\n", - show_debugging ? "enabled" : "disabled"); - } - else if (cmd == "set") - { - if (!is_enabled) - { - out.printerr( - "ERROR: buildingplan must be enabled before you can" - " read or set buildingplan global settings."); - return CR_FAILURE; - } +static bool isPlannedBuilding(color_ostream &out, df::building *bld) { + TRACE(status,out).print("entering isPlannedBuilding\n"); + return bld && planned_buildings.count(bld->id); +} - if (!DFHack::Core::getInstance().isMapLoaded()) - { - out.printerr( - "ERROR: A map must be loaded before you can read or set" - "buildingplan global settings. Try adding your" - "'buildingplan set' commands to the onMapLoad.init file.\n"); - return CR_FAILURE; - } +static bool addPlannedBuilding(color_ostream &out, df::building *bld) { + DEBUG(status,out).print("entering addPlannedBuilding\n"); + if (!bld || planned_buildings.count(bld->id) + || !isPlannableBuilding(out, bld->getType(), bld->getSubtype(), + bld->getCustomType())) + return false; + BuildingTypeKey key(bld->getType(), bld->getSubtype(), bld->getCustomType()); + PlannedBuilding pb(out, bld, get_heat_safety_filter(key), get_item_filters(out, key).getItemFilters()); + return registerPlannedBuilding(out, pb); +} - if (parameters.size() == 1) - { - // display current settings - out.print("active settings:\n"); +static void doCycle(color_ostream &out) { + DEBUG(status,out).print("entering doCycle\n"); + do_cycle(out); +} - out.print(" all_enabled = %s\n", all_enabled ? "true" : "false"); - for (auto & setting : planner.getGlobalSettings()) - { - out.print(" %s = %s\n", setting.first.c_str(), - setting.second ? "true" : "false"); - } +static void scheduleCycle(color_ostream &out) { + DEBUG(status,out).print("entering scheduleCycle\n"); + cycle_requested = true; +} - out.print(" quickfort_mode = %s\n", - quickfort_mode ? "true" : "false"); - } - else if (parameters.size() == 3) - { - // set a setting - std::string setting = toLower(parameters[1]); - bool val = isTrue(parameters[2]); - if (!setSetting(setting, val)) - { - out.printerr("ERROR: invalid parameter: '%s'\n", - parameters[1].c_str()); +static int scanAvailableItems(color_ostream &out, df::building_type type, int16_t subtype, + int32_t custom, int index, vector *item_ids = NULL) { + DEBUG(status,out).print( + "entering countAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", + type, subtype, custom, index); + BuildingTypeKey key(type, subtype, custom); + HeatSafety heat = get_heat_safety_filter(key); + auto &job_items = get_job_items(out, key); + if (index < 0 || job_items.size() <= (size_t)index) + return 0; + auto &item_filters = get_item_filters(out, key).getItemFilters(); + + auto &jitem = job_items[index]; + auto vector_ids = getVectorIds(out, jitem); + + int count = 0; + for (auto vector_id : vector_ids) { + auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id); + for (auto &item : df::global::world->items.other[other_id]) { + if (itemPassesScreen(item) && matchesFilters(item, jitem, heat, item_filters[index])) { + if (item_ids) + item_ids->emplace_back(item->id); + ++count; } } - else - { - out.printerr("ERROR: invalid syntax\n"); - } } - return CR_OK; + DEBUG(status,out).print("found matches %d\n", count); + return count; } -DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) -{ - if (!gps) - return CR_FAILURE; - - if (enable != is_enabled) - { - if (DFHack::Core::getInstance().isMapLoaded()) - planner.reset(); +static int getAvailableItems(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + df::building_type type = (df::building_type)luaL_checkint(L, 1); + int16_t subtype = luaL_checkint(L, 2); + int32_t custom = luaL_checkint(L, 3); + int index = luaL_checkint(L, 4); + DEBUG(status,*out).print( + "entering getAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", + type, subtype, custom, index); + vector item_ids; + scanAvailableItems(*out, type, subtype, custom, index, &item_ids); + Lua::PushVector(L, item_ids); + return 1; +} - if (!INTERPOSE_HOOK(buildingplan_query_hook, feed).apply(enable) || - !INTERPOSE_HOOK(buildingplan_place_hook, feed).apply(enable) || - !INTERPOSE_HOOK(buildingplan_room_hook, feed).apply(enable) || - !INTERPOSE_HOOK(buildingplan_query_hook, render).apply(enable) || - !INTERPOSE_HOOK(buildingplan_place_hook, render).apply(enable) || - !INTERPOSE_HOOK(buildingplan_room_hook, render).apply(enable)) - return CR_FAILURE; +static int countAvailableItems(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { + DEBUG(status,out).print( + "entering countAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", + type, subtype, custom, index); + return scanAvailableItems(out, type, subtype, custom, index); +} - is_enabled = enable; +static bool hasFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { + TRACE(status,out).print("entering hasFilter\n"); + BuildingTypeKey key(type, subtype, custom); + auto &filters = get_item_filters(out, key); + for (auto &filter : filters.getItemFilters()) { + if (filter.isEmpty()) + return true; } - - return CR_OK; + return false; } -DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) -{ - commands.push_back( - PluginCommand("buildingplan", - "Plan building construction before you have materials.", - buildingplan_cmd)); - - return CR_OK; +static void setMaterialFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index, string filter) { + DEBUG(status,out).print("entering setMaterialFilter\n"); + call_buildingplan_lua(&out, "signal_reset"); } -DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) -{ - switch (event) { - case SC_MAP_LOADED: - buildingplan_place_hook::invalidateStatics(); - buildingplan_query_hook::invalidateStatics(); - planner.reset(); - roomMonitor.reset(out); - break; - default: - break; - } +static int getMaterialFilter(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + df::building_type type = (df::building_type)luaL_checkint(L, 1); + int16_t subtype = luaL_checkint(L, 2); + int32_t custom = luaL_checkint(L, 3); + int index = luaL_checkint(L, 4); + DEBUG(status,*out).print( + "entering getMaterialFilter building_type=%d subtype=%d custom=%d index=%d\n", + type, subtype, custom, index); + vector filter; + Lua::PushVector(L, filter); + return 1; +} - return CR_OK; +static void setHeatSafetyFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int heat) { + DEBUG(status,out).print("entering setHeatSafetyFilter\n"); + BuildingTypeKey key(type, subtype, custom); + if (heat == HEAT_SAFETY_FIRE || heat == HEAT_SAFETY_MAGMA) + cur_heat_safety[key] = (HeatSafety)heat; + else + cur_heat_safety.erase(key); + call_buildingplan_lua(&out, "signal_reset"); } -static bool is_paused() -{ - return World::ReadPauseState() || - plotinfo->main.mode > df::ui_sidebar_mode::Squads || - !strict_virtual_cast(Gui::getCurViewscreen(true)); +static int getHeatSafetyFilter(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + df::building_type type = (df::building_type)luaL_checkint(L, 1); + int16_t subtype = luaL_checkint(L, 2); + int32_t custom = luaL_checkint(L, 3); + DEBUG(status,*out).print( + "entering getHeatSafetyFilter building_type=%d subtype=%d custom=%d\n", + type, subtype, custom); + BuildingTypeKey key(type, subtype, custom); + HeatSafety heat = get_heat_safety_filter(key); + Lua::Push(L, heat); + return 1; } -static bool cycle_requested = false; +static bool validate_pb(color_ostream &out, df::building *bld, int index) { + if (!isPlannedBuilding(out, bld) || bld->jobs.size() != 1) + return false; -#define DAY_TICKS 1200 -DFhackCExport command_result plugin_onupdate(color_ostream &) -{ - if (Maps::IsValid() && !is_paused() - && (cycle_requested || world->frame_counter % (DAY_TICKS/2) == 0)) - { - planner.doCycle(); - roomMonitor.doCycle(); - cycle_requested = false; - } + auto &job_items = bld->jobs[0]->job_items; + if ((int)job_items.size() <= index) + return false; - return CR_OK; -} + PlannedBuilding &pb = planned_buildings.at(bld->id); + if ((int)pb.vector_ids.size() <= index) + return false; -DFhackCExport command_result plugin_shutdown(color_ostream &) -{ - return CR_OK; + return true; } -// Lua API section +static string getDescString(color_ostream &out, df::building *bld, int index) { + DEBUG(status,out).print("entering getDescString\n"); + if (!validate_pb(out, bld, index)) + return 0; -static bool isPlanModeEnabled(df::building_type type, - int16_t subtype, - int32_t custom) { - return is_planmode_enabled(toBuildingTypeKey(type, subtype, custom)); + PlannedBuilding &pb = planned_buildings.at(bld->id); + auto &jitem = bld->jobs[0]->job_items[index]; + return get_desc_string(out, jitem, pb.vector_ids[index]); } -static bool isPlannableBuilding(df::building_type type, - int16_t subtype, - int32_t custom) { - return planner.isPlannableBuilding( - toBuildingTypeKey(type, subtype, custom)); -} +static int getQueuePosition(color_ostream &out, df::building *bld, int index) { + DEBUG(status,out).print("entering getQueuePosition\n"); + if (!validate_pb(out, bld, index)) + return 0; -static bool isPlannedBuilding(df::building *bld) { - return !!planner.getPlannedBuilding(bld); -} + PlannedBuilding &pb = planned_buildings.at(bld->id); + auto &job_item = bld->jobs[0]->job_items[index]; -static void addPlannedBuilding(df::building *bld) { - planner.addPlannedBuilding(bld); -} + if (job_item->quantity <= 0) + return 0; -static void doCycle() { - planner.doCycle(); -} + int min_pos = -1; + for (auto &vec_id : pb.vector_ids[index]) { + if (!tasks.count(vec_id)) + continue; + auto &buckets = tasks.at(vec_id); + string bucket_id = getBucket(*job_item); + if (!buckets.count(bucket_id)) + continue; + int bucket_pos = -1; + for (auto &task : buckets.at(bucket_id)) { + ++bucket_pos; + if (bld->id == task.first && index == task.second) + break; + } + if (bucket_pos++ >= 0) + min_pos = min_pos < 0 ? bucket_pos : std::min(min_pos, bucket_pos); + } -static void scheduleCycle() { - cycle_requested = true; + return min_pos < 0 ? 0 : min_pos; } -static bool setSetting(std::string name, bool value) { - if (name == "quickfort_mode") - { - debug("setting quickfort_mode %d -> %d", quickfort_mode, value); - quickfort_mode = value; - return true; - } - if (name == "all_enabled") - { - debug("setting all_enabled %d -> %d", all_enabled, value); - all_enabled = value; - return true; +static void makeTopPriority(color_ostream &out, df::building *bld) { + DEBUG(status,out).print("entering makeTopPriority\n"); + if (!validate_pb(out, bld, 0)) + return; + + PlannedBuilding &pb = planned_buildings.at(bld->id); + auto &job_items = bld->jobs[0]->job_items; + + for (int index = 0; index < (int)job_items.size(); ++index) { + for (auto &vec_id : pb.vector_ids[index]) { + if (!tasks.count(vec_id)) + continue; + auto &buckets = tasks.at(vec_id); + string bucket_id = getBucket(*job_items[index]); + if (!buckets.count(bucket_id)) + continue; + auto &bucket = buckets.at(bucket_id); + for (auto taskit = bucket.begin(); taskit != bucket.end(); ++taskit) { + if (bld->id == taskit->first && index == taskit->second) { + auto task_bld_id = taskit->first; + auto task_job_item_idx = taskit->second; + bucket.erase(taskit); + bucket.emplace_front(task_bld_id, task_job_item_idx); + break; + } + } + } } - return planner.setGlobalSetting(name, value); } DFHACK_PLUGIN_LUA_FUNCTIONS { - DFHACK_LUA_FUNCTION(isPlanModeEnabled), + DFHACK_LUA_FUNCTION(printStatus), + DFHACK_LUA_FUNCTION(setSetting), DFHACK_LUA_FUNCTION(isPlannableBuilding), DFHACK_LUA_FUNCTION(isPlannedBuilding), DFHACK_LUA_FUNCTION(addPlannedBuilding), DFHACK_LUA_FUNCTION(doCycle), DFHACK_LUA_FUNCTION(scheduleCycle), - DFHACK_LUA_FUNCTION(setSetting), + DFHACK_LUA_FUNCTION(countAvailableItems), + DFHACK_LUA_FUNCTION(hasFilter), + DFHACK_LUA_FUNCTION(setMaterialFilter), + DFHACK_LUA_FUNCTION(setHeatSafetyFilter), + DFHACK_LUA_FUNCTION(getDescString), + DFHACK_LUA_FUNCTION(getQueuePosition), + DFHACK_LUA_FUNCTION(makeTopPriority), + DFHACK_LUA_END +}; + +DFHACK_PLUGIN_LUA_COMMANDS { + DFHACK_LUA_COMMAND(getAvailableItems), + DFHACK_LUA_COMMAND(getMaterialFilter), + DFHACK_LUA_COMMAND(getHeatSafetyFilter), DFHACK_LUA_END }; diff --git a/plugins/buildingplan/buildingplan.h b/plugins/buildingplan/buildingplan.h index e906ef1a7..eef9808e6 100644 --- a/plugins/buildingplan/buildingplan.h +++ b/plugins/buildingplan/buildingplan.h @@ -1,8 +1,52 @@ #pragma once -#include "buildingplan-planner.h" -#include "buildingplan-rooms.h" +#include "itemfilter.h" -void debug(const char *fmt, ...) Wformat(printf,1,2); +#include "modules/Persistence.h" -extern bool show_debugging; +#include "df/building.h" +#include "df/job_item.h" +#include "df/job_item_vector_id.h" + +#include + +typedef std::deque> Bucket; +typedef std::map> Tasks; + +extern const std::string FILTER_CONFIG_KEY; +extern const std::string BLD_CONFIG_KEY; + +enum ConfigValues { + CONFIG_BLOCKS = 1, + CONFIG_BOULDERS = 2, + CONFIG_LOGS = 3, + CONFIG_BARS = 4, +}; + +enum FilterConfigValues { + FILTER_CONFIG_TYPE = 0, + FILTER_CONFIG_SUBTYPE = 1, + FILTER_CONFIG_CUSTOM = 2, +}; + +enum BuildingConfigValues { + BLD_CONFIG_ID = 0, + BLD_CONFIG_HEAT = 1, +}; + +enum HeatSafety { + HEAT_SAFETY_ANY = 0, + HEAT_SAFETY_FIRE = 1, + HEAT_SAFETY_MAGMA = 2, +}; + +int get_config_val(DFHack::PersistentDataItem &c, int index); +bool get_config_bool(DFHack::PersistentDataItem &c, int index); +void set_config_val(DFHack::PersistentDataItem &c, int index, int value); +void set_config_bool(DFHack::PersistentDataItem &c, int index, bool value); + +std::vector getVectorIds(DFHack::color_ostream &out, const df::job_item *job_item); +bool itemPassesScreen(df::item * item); +bool matchesFilters(df::item * item, const df::job_item * job_item, HeatSafety heat, const ItemFilter &item_filter); +bool isJobReady(DFHack::color_ostream &out, const std::vector &jitems); +void finalizeBuilding(DFHack::color_ostream &out, df::building *bld); diff --git a/plugins/buildingplan/buildingplan_cycle.cpp b/plugins/buildingplan/buildingplan_cycle.cpp new file mode 100644 index 000000000..655dc8c1a --- /dev/null +++ b/plugins/buildingplan/buildingplan_cycle.cpp @@ -0,0 +1,284 @@ +#include "plannedbuilding.h" +#include "buildingplan.h" + +#include "Debug.h" + +#include "modules/Items.h" +#include "modules/Job.h" +#include "modules/Materials.h" + +#include "df/building_design.h" +#include "df/item.h" +#include "df/job.h" +#include "df/world.h" + +#include + +using std::map; +using std::string; +using std::unordered_map; + +namespace DFHack { + DBG_EXTERN(buildingplan, cycle); +} + +using namespace DFHack; + +struct BadFlags { + uint32_t whole; + + BadFlags() { + df::item_flags flags; + #define F(x) 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(in_job); + F(owned); F(in_chest); F(removed); F(encased); + F(spider_web); + #undef F + whole = flags.whole; + } +}; + +bool itemPassesScreen(df::item * item) { + static const BadFlags bad_flags; + return !(item->flags.whole & bad_flags.whole) + && !item->isAssignedToStockpile(); +} + +bool matchesFilters(df::item * item, const df::job_item * job_item, HeatSafety heat, const ItemFilter &item_filter) { + // check the properties that are not checked by Job::isSuitableItem() + if (job_item->item_type > -1 && job_item->item_type != item->getType()) + return false; + + if (job_item->item_subtype > -1 && + job_item->item_subtype != item->getSubtype()) + return false; + + if (job_item->flags2.bits.building_material && !item->isBuildMat()) + return false; + + if (job_item->metal_ore > -1 && !item->isMetalOre(job_item->metal_ore)) + return false; + + if (job_item->has_tool_use > df::tool_uses::NONE + && !item->hasToolUse(job_item->has_tool_use)) + return false; + + df::job_item jitem = *job_item; + if (heat == HEAT_SAFETY_MAGMA) { + jitem.flags2.bits.magma_safe = true; + jitem.flags2.bits.fire_safe = false; + } else if (heat == HEAT_SAFETY_FIRE && !jitem.flags2.bits.magma_safe) + jitem.flags2.bits.fire_safe = true; + + return Job::isSuitableItem( + &jitem, item->getType(), item->getSubtype()) + && Job::isSuitableMaterial( + &jitem, item->getMaterial(), item->getMaterialIndex(), + item->getType()) + && item_filter.matches(item); +} + +bool isJobReady(color_ostream &out, const std::vector &jitems) { + int needed_items = 0; + for (auto job_item : jitems) { needed_items += job_item->quantity; } + if (needed_items) { + DEBUG(cycle,out).print("building needs %d more item(s)\n", needed_items); + return false; + } + return true; +} + +static bool job_item_idx_lt(df::job_item_ref *a, df::job_item_ref *b) { + // we want the items in the opposite order of the filters + return a->job_item_idx > b->job_item_idx; +} + +// this function does not remove the job_items since their quantity fields are +// now all at 0, so there is no risk of having extra items attached. we don't +// remove them to keep the "finalize with buildingplan active" path as similar +// as possible to the "finalize with buildingplan disabled" path. +void finalizeBuilding(color_ostream &out, df::building *bld) { + DEBUG(cycle,out).print("finalizing building %d\n", bld->id); + auto job = bld->jobs[0]; + + // sort the items so they get added to the structure in the correct order + std::sort(job->items.begin(), job->items.end(), job_item_idx_lt); + + // derive the material properties of the building and job from the first + // applicable item. if any boulders are involved, it makes the whole + // structure "rough". + bool rough = false; + for (auto attached_item : job->items) { + df::item *item = attached_item->item; + rough = rough || item->getType() == df::item_type::BOULDER; + if (bld->mat_type == -1) { + bld->mat_type = item->getMaterial(); + job->mat_type = bld->mat_type; + } + if (bld->mat_index == -1) { + bld->mat_index = item->getMaterialIndex(); + job->mat_index = bld->mat_index; + } + } + + if (bld->needsDesign()) { + auto act = (df::building_actual *)bld; + if (!act->design) + act->design = new df::building_design(); + act->design->flags.bits.rough = rough; + } + + // we're good to go! + job->flags.bits.suspend = false; + Job::checkBuildingsNow(); +} + +static df::building * popInvalidTasks(color_ostream &out, Bucket &task_queue, + unordered_map &planned_buildings) { + while (!task_queue.empty()) { + auto & task = task_queue.front(); + auto id = task.first; + if (planned_buildings.count(id) > 0) { + auto bld = planned_buildings.at(id).getBuildingIfValidOrRemoveIfNot(out); + if (bld && bld->jobs[0]->job_items[task.second]->quantity) + return bld; + } + DEBUG(cycle,out).print("discarding invalid task: bld=%d, job_item_idx=%d\n", id, task.second); + task_queue.pop_front(); + } + return NULL; +} + +static void doVector(color_ostream &out, df::job_item_vector_id vector_id, + map &buckets, + unordered_map &planned_buildings) { + auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id); + auto item_vector = df::global::world->items.other[other_id]; + DEBUG(cycle,out).print("matching %zu item(s) in vector %s against %zu filter bucket(s)\n", + item_vector.size(), + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + buckets.size()); + for (auto item_it = item_vector.rbegin(); + item_it != item_vector.rend(); + ++item_it) { + auto item = *item_it; + if (!itemPassesScreen(item)) + continue; + for (auto bucket_it = buckets.begin(); bucket_it != buckets.end(); ) { + auto & task_queue = bucket_it->second; + auto bld = popInvalidTasks(out, task_queue, planned_buildings); + if (!bld) { + DEBUG(cycle,out).print("removing empty bucket: %s/%s; %zu bucket(s) left\n", + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + bucket_it->first.c_str(), + buckets.size() - 1); + bucket_it = buckets.erase(bucket_it); + continue; + } + auto & task = task_queue.front(); + auto id = task.first; + auto job = bld->jobs[0]; + auto filter_idx = task.second; + auto &pb = planned_buildings.at(id); + if (matchesFilters(item, job->job_items[filter_idx], pb.heat_safety, + pb.item_filters[filter_idx]) + && Job::attachJobItem(job, item, + df::job_item_ref::Hauled, filter_idx)) + { + MaterialInfo material; + material.decode(item); + ItemTypeInfo item_type; + item_type.decode(item); + DEBUG(cycle,out).print("attached %s %s to filter %d for %s(%d): %s/%s\n", + material.toString().c_str(), + item_type.toString().c_str(), + filter_idx, + ENUM_KEY_STR(building_type, bld->getType()).c_str(), + id, + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + bucket_it->first.c_str()); + // keep quantity aligned with the actual number of remaining + // items so if buildingplan is turned off, the building will + // be completed with the correct number of items. + --job->job_items[filter_idx]->quantity; + task_queue.pop_front(); + if (isJobReady(out, job->job_items)) { + finalizeBuilding(out, bld); + planned_buildings.at(id).remove(out); + } + if (task_queue.empty()) { + DEBUG(cycle,out).print( + "removing empty item bucket: %s/%s; %zu left\n", + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + bucket_it->first.c_str(), + buckets.size() - 1); + buckets.erase(bucket_it); + } + // we found a home for this item; no need to look further + break; + } + ++bucket_it; + } + if (buckets.empty()) + break; + } +} + +struct VectorsToScanLast { + std::vector vectors; + VectorsToScanLast() { + // order is important here. we want to match boulders before wood and + // everything before bars. blocks are not listed here since we'll have + // already scanned them when we did the first pass through the buckets. + vectors.push_back(df::job_item_vector_id::BOULDER); + vectors.push_back(df::job_item_vector_id::WOOD); + vectors.push_back(df::job_item_vector_id::BAR); + } +}; + +void buildingplan_cycle(color_ostream &out, Tasks &tasks, + unordered_map &planned_buildings) { + static const VectorsToScanLast vectors_to_scan_last; + + DEBUG(cycle,out).print( + "running buildingplan cycle for %zu registered buildings\n", + planned_buildings.size()); + + for (auto it = tasks.begin(); it != tasks.end(); ) { + auto vector_id = it->first; + // we could make this a set, but it's only three elements + if (std::find(vectors_to_scan_last.vectors.begin(), + vectors_to_scan_last.vectors.end(), + vector_id) != vectors_to_scan_last.vectors.end()) { + ++it; + continue; + } + + auto & buckets = it->second; + doVector(out, vector_id, buckets, planned_buildings); + if (buckets.empty()) { + DEBUG(cycle,out).print("removing empty vector: %s; %zu vector(s) left\n", + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + tasks.size() - 1); + it = tasks.erase(it); + } + else + ++it; + } + for (auto vector_id : vectors_to_scan_last.vectors) { + if (tasks.count(vector_id) == 0) + continue; + auto & buckets = tasks[vector_id]; + doVector(out, vector_id, buckets, planned_buildings); + if (buckets.empty()) { + DEBUG(cycle,out).print("removing empty vector: %s; %zu vector(s) left\n", + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + tasks.size() - 1); + tasks.erase(vector_id); + } + } + DEBUG(cycle,out).print("cycle done; %zu registered building(s) left\n", + planned_buildings.size()); +} diff --git a/plugins/buildingplan/buildingtypekey.cpp b/plugins/buildingplan/buildingtypekey.cpp new file mode 100644 index 000000000..664fdf27d --- /dev/null +++ b/plugins/buildingplan/buildingtypekey.cpp @@ -0,0 +1,59 @@ +#include "buildingplan.h" +#include "buildingtypekey.h" + +#include "Debug.h" +#include "MiscUtils.h" + +using std::string; +using std::vector; + +namespace DFHack { + DBG_EXTERN(buildingplan, status); +} + +using namespace DFHack; + +// building type, subtype, custom +BuildingTypeKey::BuildingTypeKey(df::building_type type, int16_t subtype, int32_t custom) + : tuple(type, subtype, custom) { } + +static BuildingTypeKey deserialize(color_ostream &out, const std::string &serialized) { + vector key_parts; + split_string(&key_parts, serialized, ","); + if (key_parts.size() != 3) { + WARN(status,out).print("invalid key_str: '%s'\n", serialized.c_str()); + return BuildingTypeKey(df::building_type::NONE, -1, -1); + } + return BuildingTypeKey((df::building_type)string_to_int(key_parts[0]), + string_to_int(key_parts[1]), string_to_int(key_parts[2])); +} + +BuildingTypeKey::BuildingTypeKey(color_ostream &out, const std::string &serialized) + :tuple(deserialize(out, serialized)) { } + +string BuildingTypeKey::serialize() const { + std::ostringstream ser; + ser << std::get<0>(*this) << ","; + ser << std::get<1>(*this) << ","; + ser << std::get<2>(*this); + return ser.str(); +} + +// rotates a size_t value left by count bits +// assumes count is not 0 or >= size_t_bits +// replace this with std::rotl when we move to C++20 +static std::size_t rotl_size_t(size_t val, uint32_t count) +{ + static const int size_t_bits = CHAR_BIT * sizeof(std::size_t); + return val << count | val >> (size_t_bits - count); +} + +std::size_t BuildingTypeKeyHash::operator() (const BuildingTypeKey & key) const { + // cast first param to appease gcc-4.8, which is missing the enum + // specializations for std::hash + std::size_t h1 = std::hash()(static_cast(std::get<0>(key))); + std::size_t h2 = std::hash()(std::get<1>(key)); + std::size_t h3 = std::hash()(std::get<2>(key)); + + return h1 ^ rotl_size_t(h2, 8) ^ rotl_size_t(h3, 16); +} diff --git a/plugins/buildingplan/buildingtypekey.h b/plugins/buildingplan/buildingtypekey.h new file mode 100644 index 000000000..81bb043c5 --- /dev/null +++ b/plugins/buildingplan/buildingtypekey.h @@ -0,0 +1,22 @@ +#pragma once + +#include "df/building_type.h" + +#include +#include + +namespace DFHack { + class color_ostream; +} + +// building type, subtype, custom +struct BuildingTypeKey : public std::tuple { + BuildingTypeKey(df::building_type type, int16_t subtype, int32_t custom); + BuildingTypeKey(DFHack::color_ostream &out, const std::string & serialized); + + std::string serialize() const; +}; + +struct BuildingTypeKeyHash { + std::size_t operator() (const BuildingTypeKey & key) const; +}; diff --git a/plugins/buildingplan/defaultitemfilters.cpp b/plugins/buildingplan/defaultitemfilters.cpp new file mode 100644 index 000000000..36d074363 --- /dev/null +++ b/plugins/buildingplan/defaultitemfilters.cpp @@ -0,0 +1,60 @@ +#include "defaultitemfilters.h" + +#include "Debug.h" +#include "MiscUtils.h" + +#include "modules/World.h" + +namespace DFHack { + DBG_EXTERN(buildingplan, status); +} + +using std::string; +using std::vector; +using namespace DFHack; + +BuildingTypeKey DefaultItemFilters::getKey(PersistentDataItem &filter_config) { + return BuildingTypeKey( + (df::building_type)get_config_val(filter_config, FILTER_CONFIG_TYPE), + get_config_val(filter_config, FILTER_CONFIG_SUBTYPE), + get_config_val(filter_config, FILTER_CONFIG_CUSTOM)); +} + +DefaultItemFilters::DefaultItemFilters(color_ostream &out, BuildingTypeKey key, const std::vector &jitems) + : key(key) { + DEBUG(status,out).print("creating persistent data for filter key %d,%d,%d\n", + std::get<0>(key), std::get<1>(key), std::get<2>(key)); + filter_config = World::AddPersistentData(FILTER_CONFIG_KEY); + set_config_val(filter_config, FILTER_CONFIG_TYPE, std::get<0>(key)); + set_config_val(filter_config, FILTER_CONFIG_SUBTYPE, std::get<1>(key)); + set_config_val(filter_config, FILTER_CONFIG_CUSTOM, std::get<2>(key)); + item_filters.resize(jitems.size()); + filter_config.val() = serialize_item_filters(item_filters); +} + +DefaultItemFilters::DefaultItemFilters(color_ostream &out, PersistentDataItem &filter_config, const std::vector &jitems) + : key(getKey(filter_config)), filter_config(filter_config) { + auto &serialized = filter_config.val(); + DEBUG(status,out).print("deserializing item filters for key %d,%d,%d: %s\n", + std::get<0>(key), std::get<1>(key), std::get<2>(key), serialized.c_str()); + std::vector filters = deserialize_item_filters(out, serialized); + if (filters.size() != jitems.size()) { + WARN(status,out).print("ignoring invalid filters_str for key %d,%d,%d: '%s'\n", + std::get<0>(key), std::get<1>(key), std::get<2>(key), serialized.c_str()); + item_filters.resize(jitems.size()); + } else + item_filters = filters; +} + +void DefaultItemFilters::setItemFilter(DFHack::color_ostream &out, const ItemFilter &filter, int index) { + if (index < 0 || item_filters.size() <= (size_t)index) { + WARN(status,out).print("invalid index for filter key %d,%d,%d: %d\n", + std::get<0>(key), std::get<1>(key), std::get<2>(key), index); + return; + } + + item_filters[index] = filter; + filter_config.val() = serialize_item_filters(item_filters); + DEBUG(status,out).print("updated item filter and persisted for key %d,%d,%d: %s\n", + std::get<0>(key), std::get<1>(key), std::get<2>(key), filter_config.val().c_str()); +} diff --git a/plugins/buildingplan/defaultitemfilters.h b/plugins/buildingplan/defaultitemfilters.h new file mode 100644 index 000000000..4d1d5cbd2 --- /dev/null +++ b/plugins/buildingplan/defaultitemfilters.h @@ -0,0 +1,24 @@ +#pragma once + +#include "buildingplan.h" +#include "buildingtypekey.h" + +#include "modules/Persistence.h" + +class DefaultItemFilters { +public: + static BuildingTypeKey getKey(DFHack::PersistentDataItem &filter_config); + + const BuildingTypeKey key; + + DefaultItemFilters(DFHack::color_ostream &out, BuildingTypeKey key, const std::vector &jitems); + DefaultItemFilters(DFHack::color_ostream &out, DFHack::PersistentDataItem &filter_config, const std::vector &jitems); + + void setItemFilter(DFHack::color_ostream &out, const ItemFilter &filter, int index); + + const std::vector & getItemFilters() const { return item_filters; } + +private: + DFHack::PersistentDataItem filter_config; + std::vector item_filters; +}; diff --git a/plugins/buildingplan/itemfilter.cpp b/plugins/buildingplan/itemfilter.cpp new file mode 100644 index 000000000..a714b62d4 --- /dev/null +++ b/plugins/buildingplan/itemfilter.cpp @@ -0,0 +1,212 @@ +#include "itemfilter.h" + +#include "Debug.h" + +#include "df/item.h" + +namespace DFHack { + DBG_EXTERN(buildingplan, status); +} + +using std::string; +using std::vector; + +using namespace DFHack; + +ItemFilter::ItemFilter() { + clear(); +} + +void ItemFilter::clear() { + min_quality = df::item_quality::Ordinary; + max_quality = df::item_quality::Masterful; + decorated_only = false; + mat_mask.whole = 0; + materials.clear(); +} + +bool ItemFilter::isEmpty() const { + return min_quality == df::item_quality::Ordinary + && max_quality == df::item_quality::Masterful + && !decorated_only + && !mat_mask.whole + && materials.empty(); +} + +static bool deserializeMaterialMask(string ser, df::dfhack_material_category mat_mask) { + if (ser.empty()) + return true; + + if (!parseJobMaterialCategory(&mat_mask, ser)) { + DEBUG(status).print("invalid job material category serialization: '%s'", ser.c_str()); + return false; + } + return true; +} + +static bool deserializeMaterials(string ser, vector &materials) { + if (ser.empty()) + return true; + + vector mat_names; + split_string(&mat_names, ser, ","); + for (auto m = mat_names.begin(); m != mat_names.end(); m++) { + DFHack::MaterialInfo material; + if (!material.find(*m) || !material.isValid()) { + DEBUG(status).print("invalid material name serialization: '%s'", ser.c_str()); + return false; + } + materials.push_back(material); + } + return true; +} + +ItemFilter::ItemFilter(color_ostream &out, string serialized) { + clear(); + + vector tokens; + split_string(&tokens, serialized, "/"); + if (tokens.size() != 5) { + DEBUG(status,out).print("invalid ItemFilter serialization: '%s'", serialized.c_str()); + return; + } + + if (!deserializeMaterialMask(tokens[0], mat_mask) || !deserializeMaterials(tokens[1], materials)) + return; + + setMinQuality(atoi(tokens[2].c_str())); + setMaxQuality(atoi(tokens[3].c_str())); + decorated_only = static_cast(atoi(tokens[4].c_str())); +} + +// format: mat,mask,elements/materials,list/minq/maxq/decorated +string ItemFilter::serialize() const { + std::ostringstream ser; + ser << bitfield_to_string(mat_mask, ",") << "/"; + if (!materials.empty()) { + ser << materials[0].getToken(); + for (size_t i = 1; i < materials.size(); ++i) + ser << "," << materials[i].getToken(); + } + ser << "/" << static_cast(min_quality); + ser << "/" << static_cast(max_quality); + ser << "/" << static_cast(decorated_only); + return ser.str(); +} + +static void clampItemQuality(df::item_quality *quality) { + if (*quality > df::item_quality::Artifact) { + DEBUG(status).print("clamping quality to Artifact"); + *quality = df::item_quality::Artifact; + } + if (*quality < df::item_quality::Ordinary) { + DEBUG(status).print("clamping quality to Ordinary"); + *quality = df::item_quality::Ordinary; + } +} + +void ItemFilter::setMinQuality(int quality) { + min_quality = static_cast(quality); + clampItemQuality(&min_quality); + if (max_quality < min_quality) + max_quality = min_quality; +} + +void ItemFilter::setMaxQuality(int quality) { + max_quality = static_cast(quality); + clampItemQuality(&max_quality); + if (max_quality < min_quality) + min_quality = max_quality; +} + +void ItemFilter::setDecoratedOnly(bool decorated) { + decorated_only = decorated; +} + +void ItemFilter::setMaterialMask(uint32_t mask) { + mat_mask.whole = mask; +} + +void ItemFilter::setMaterials(const vector &materials) { + this->materials = materials; +} + +string ItemFilter::getMinQuality() const { + return ENUM_KEY_STR(item_quality, min_quality); +} + +string ItemFilter::getMaxQuality() const { + return ENUM_KEY_STR(item_quality, max_quality); +} + +bool ItemFilter::getDecoratedOnly() const { + return decorated_only; +} + +uint32_t ItemFilter::getMaterialMask() const { + return mat_mask.whole; +} + +static string material_to_string_fn(const MaterialInfo &m) { return m.toString(); } + +vector ItemFilter::getMaterials() const { + vector descriptions; + transform_(materials, descriptions, material_to_string_fn); + + if (descriptions.size() == 0) + bitfield_to_string(&descriptions, mat_mask); + + if (descriptions.size() == 0) + descriptions.push_back("any"); + + return descriptions; +} + +static bool matchesMask(DFHack::MaterialInfo &mat, df::dfhack_material_category mat_mask) { + return mat_mask.whole ? mat.matches(mat_mask) : true; +} + +bool ItemFilter::matches(df::dfhack_material_category mask) const { + return mask.whole & mat_mask.whole; +} + +bool ItemFilter::matches(DFHack::MaterialInfo &material) const { + for (auto it = materials.begin(); it != materials.end(); ++it) + if (material.matches(*it)) + return true; + return false; +} + +bool ItemFilter::matches(df::item *item) const { + if (item->getQuality() < min_quality || item->getQuality() > max_quality) + return false; + + if (decorated_only && !item->hasImprovements()) + return false; + + auto imattype = item->getActualMaterial(); + auto imatindex = item->getActualMaterialIndex(); + auto item_mat = DFHack::MaterialInfo(imattype, imatindex); + + return (materials.size() == 0) ? matchesMask(item_mat, mat_mask) : matches(item_mat); +} + +vector deserialize_item_filters(color_ostream &out, const string &serialized) { + std::vector filters; + + vector filter_strs; + split_string(&filter_strs, serialized, ";"); + for (auto &str : filter_strs) { + filters.emplace_back(out, str); + } + + return filters; +} + +string serialize_item_filters(const vector &filters) { + vector strs; + for (auto &filter : filters) { + strs.emplace_back(filter.serialize()); + } + return join_strings(";", strs); +} diff --git a/plugins/buildingplan/itemfilter.h b/plugins/buildingplan/itemfilter.h new file mode 100644 index 000000000..6eb7551b4 --- /dev/null +++ b/plugins/buildingplan/itemfilter.h @@ -0,0 +1,42 @@ +#pragma once + +#include "modules/Materials.h" + +#include "df/dfhack_material_category.h" +#include "df/item_quality.h" + +class ItemFilter { +public: + ItemFilter(); + ItemFilter(DFHack::color_ostream &out, std::string serialized); + + void clear(); + bool isEmpty() const; + std::string serialize() const; + + void setMinQuality(int quality); + void setMaxQuality(int quality); + void setDecoratedOnly(bool decorated); + void setMaterialMask(uint32_t mask); + void setMaterials(const std::vector &materials); + + std::string getMinQuality() const; + std::string getMaxQuality() const; + bool getDecoratedOnly() const; + uint32_t getMaterialMask() const; + std::vector getMaterials() const; + + bool matches(df::dfhack_material_category mask) const; + bool matches(DFHack::MaterialInfo &material) const; + bool matches(df::item *item) const; + +private: + df::item_quality min_quality; + df::item_quality max_quality; + bool decorated_only; + df::dfhack_material_category mat_mask; + std::vector materials; +}; + +std::vector deserialize_item_filters(DFHack::color_ostream &out, const std::string &serialized); +std::string serialize_item_filters(const std::vector &filters); diff --git a/plugins/buildingplan/plannedbuilding.cpp b/plugins/buildingplan/plannedbuilding.cpp new file mode 100644 index 000000000..27be36a5b --- /dev/null +++ b/plugins/buildingplan/plannedbuilding.cpp @@ -0,0 +1,110 @@ +#include "plannedbuilding.h" +#include "buildingplan.h" + +#include "Debug.h" +#include "MiscUtils.h" + +#include "modules/World.h" + +#include "df/job.h" + +namespace DFHack { + DBG_EXTERN(buildingplan, status); +} + +using std::string; +using std::vector; +using namespace DFHack; + +static vector> get_vector_ids(color_ostream &out, int bld_id) { + vector> ret; + + df::building *bld = df::building::find(bld_id); + + if (!bld || bld->jobs.size() != 1) + return ret; + + auto &job = bld->jobs[0]; + for (auto &jitem : job->job_items) { + ret.emplace_back(getVectorIds(out, jitem)); + } + return ret; +} + +static vector> deserialize_vector_ids(color_ostream &out, PersistentDataItem &bld_config) { + vector> ret; + + vector rawstrs; + split_string(&rawstrs, bld_config.val(), "|"); + const string &serialized = rawstrs[0]; + + DEBUG(status,out).print("deserializing vector ids for building %d: %s\n", + get_config_val(bld_config, BLD_CONFIG_ID), serialized.c_str()); + + vector joined; + split_string(&joined, serialized, ";"); + for (auto &str : joined) { + vector lst; + split_string(&lst, str, ","); + vector ids; + for (auto &s : lst) + ids.emplace_back(df::job_item_vector_id(string_to_int(s))); + ret.emplace_back(ids); + } + + if (!ret.size()) + ret = get_vector_ids(out, get_config_val(bld_config, BLD_CONFIG_ID)); + + return ret; +} + +static std::vector get_item_filters(color_ostream &out, PersistentDataItem &bld_config) { + std::vector ret; + + vector rawstrs; + split_string(&rawstrs, bld_config.val(), "|"); + if (rawstrs.size() < 2) + return ret; + return deserialize_item_filters(out, rawstrs[1]); +} + +static string serialize(const vector> &vector_ids, const vector &item_filters) { + vector joined; + for (auto &vec_list : vector_ids) { + joined.emplace_back(join_strings(",", vec_list)); + } + std::ostringstream out; + out << join_strings(";", joined) << "|" << serialize_item_filters(item_filters); + return out.str(); +} + +PlannedBuilding::PlannedBuilding(color_ostream &out, df::building *bld, HeatSafety heat, const vector &item_filters) + : id(bld->id), vector_ids(get_vector_ids(out, id)), heat_safety(heat), + item_filters(item_filters) { + DEBUG(status,out).print("creating persistent data for building %d\n", id); + bld_config = World::AddPersistentData(BLD_CONFIG_KEY); + set_config_val(bld_config, BLD_CONFIG_ID, id); + set_config_val(bld_config, BLD_CONFIG_HEAT, heat_safety); + bld_config.val() = serialize(vector_ids, item_filters); + DEBUG(status,out).print("serialized state for building %d: %s\n", id, bld_config.val().c_str()); +} + +PlannedBuilding::PlannedBuilding(color_ostream &out, PersistentDataItem &bld_config) + : id(get_config_val(bld_config, BLD_CONFIG_ID)), + vector_ids(deserialize_vector_ids(out, bld_config)), + heat_safety((HeatSafety)get_config_val(bld_config, BLD_CONFIG_HEAT)), + item_filters(get_item_filters(out, bld_config)), + bld_config(bld_config) { } + +// Ensure the building still exists and is in a valid state. It can disappear +// for lots of reasons, such as running the game with the buildingplan plugin +// disabled, manually removing the building, modifying it via the API, etc. +df::building * PlannedBuilding::getBuildingIfValidOrRemoveIfNot(color_ostream &out) { + auto bld = df::building::find(id); + bool valid = bld && bld->getBuildStage() == 0; + if (!valid) { + remove(out); + return NULL; + } + return bld; +} diff --git a/plugins/buildingplan/plannedbuilding.h b/plugins/buildingplan/plannedbuilding.h new file mode 100644 index 000000000..5bd09ba5a --- /dev/null +++ b/plugins/buildingplan/plannedbuilding.h @@ -0,0 +1,36 @@ +#pragma once + +#include "buildingplan.h" +#include "itemfilter.h" + +#include "Core.h" + +#include "modules/Persistence.h" + +#include "df/building.h" +#include "df/job_item_vector_id.h" + +class PlannedBuilding { +public: + const df::building::key_field_type id; + + // job_item idx -> list of vectors the task is linked to + const std::vector> vector_ids; + + const HeatSafety heat_safety; + + const std::vector item_filters; + + PlannedBuilding(DFHack::color_ostream &out, df::building *bld, HeatSafety heat, const std::vector &item_filters); + PlannedBuilding(DFHack::color_ostream &out, DFHack::PersistentDataItem &bld_config); + + void remove(DFHack::color_ostream &out); + + // Ensure the building still exists and is in a valid state. It can disappear + // for lots of reasons, such as running the game with the buildingplan plugin + // disabled, manually removing the building, modifying it via the API, etc. + df::building * getBuildingIfValidOrRemoveIfNot(DFHack::color_ostream &out); + +private: + DFHack::PersistentDataItem bld_config; +}; diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 9b953dd7c..0e1341a3b 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -4,8 +4,6 @@ local _ENV = mkmodule('plugins.buildingplan') Native functions: - * void setSetting(string name, boolean value) - * bool isPlanModeEnabled(df::building_type type, int16_t subtype, int32_t custom) * bool isPlannableBuilding(df::building_type type, int16_t subtype, int32_t custom) * bool isPlannedBuilding(df::building *bld) * void addPlannedBuilding(df::building *bld) @@ -15,8 +13,15 @@ local _ENV = mkmodule('plugins.buildingplan') --]] local argparse = require('argparse') +local gui = require('gui') +local guidm = require('gui.dwarfmode') +local overlay = require('plugins.overlay') +local utils = require('utils') +local widgets = require('gui.widgets') require('dfhack.buildings') +local uibs = df.global.buildreq + local function process_args(opts, args) if args[1] == 'help' then opts.help = true @@ -36,67 +41,447 @@ function parse_commandline(...) return false end + local command = table.remove(positionals, 1) + if not command or command == 'status' then + printStatus() + elseif command == 'set' then + setSetting(positionals[1], positionals[2] == 'true') + else + return false + end + return true end function get_num_filters(btype, subtype, custom) - local filters = dfhack.buildings.getFiltersByType( - {}, btype, subtype, custom) - if filters then return #filters end - return 0 + local filters = dfhack.buildings.getFiltersByType({}, btype, subtype, custom) + return filters and #filters or 0 end -local dialogs = require('gui.dialogs') -local guidm = require('gui.dwarfmode') +function get_job_item(btype, subtype, custom, index) + local filters = dfhack.buildings.getFiltersByType({}, btype, subtype, custom) + if not filters or not filters[index] then return nil end + local obj = df.job_item:new() + obj:assign(filters[index]) + return obj +end -local function to_title_case(str) - str = str:gsub('(%a)([%w_]*)', - function (first, rest) return first:upper()..rest:lower() end) - str = str:gsub('_', ' ') - return str +local function get_cur_filters() + return dfhack.buildings.getFiltersByType({}, uibs.building_type, + uibs.building_subtype, uibs.custom_type) +end + +local function is_choosing_area() + return uibs.selection_pos.x >= 0 +end + +local function get_cur_area_dims(placement_data) + if not placement_data and not is_choosing_area() then return 1, 1, 1 end + local selection_pos = placement_data and placement_data.p1 or uibs.selection_pos + local pos = placement_data and placement_data.p2 or uibs.pos + return math.abs(selection_pos.x - pos.x) + 1, + math.abs(selection_pos.y - pos.y) + 1, + math.abs(selection_pos.z - pos.z) + 1 end -local function get_filter(btype, subtype, custom, reverse_idx) - local filters = dfhack.buildings.getFiltersByType( - {}, btype, subtype, custom) - if not filters or reverse_idx < 0 or reverse_idx >= #filters then - error(string.format('invalid index: %d', reverse_idx)) +local function get_quantity(filter, hollow, placement_data) + local quantity = filter.quantity or 1 + local dimx, dimy, dimz = get_cur_area_dims(placement_data) + if quantity < 1 then + return (((dimx * dimy) // 4) + 1) * dimz + end + if hollow and dimx > 2 and dimy > 2 then + return quantity * (2*dimx + 2*dimy - 4) * dimz end - return filters[#filters-reverse_idx] + return quantity * dimx * dimy * dimz end --- returns a reasonable label for the item based on the qualities of the filter --- does not need the core suspended --- reverse_idx is 0-based and is expected to be counted from the *last* filter -function get_item_label(btype, subtype, custom, reverse_idx) - local filter = get_filter(btype, subtype, custom, reverse_idx) - if filter.has_tool_use then - return to_title_case(df.tool_uses[filter.has_tool_use]) +local BUTTON_START_PEN, BUTTON_END_PEN, SELECTED_ITEM_PEN = nil, nil, nil +local reset_counts_flag = false +local reset_inspector_flag = false +function signal_reset() + BUTTON_START_PEN = nil + BUTTON_END_PEN = nil + SELECTED_ITEM_PEN = nil + reset_counts_flag = true + reset_inspector_flag = true +end + +local to_pen = dfhack.pen.parse +local function get_button_start_pen() + if not BUTTON_START_PEN then + local texpos_base = dfhack.textures.getControlPanelTexposStart() + BUTTON_START_PEN = to_pen{ch='[', fg=COLOR_YELLOW, + tile=texpos_base > 0 and texpos_base + 13 or nil} end - if filter.item_type then - return to_title_case(df.item_type[filter.item_type]) + return BUTTON_START_PEN +end +local function get_button_end_pen() + if not BUTTON_END_PEN then + local texpos_base = dfhack.textures.getControlPanelTexposStart() + BUTTON_END_PEN = to_pen{ch=']', fg=COLOR_YELLOW, + tile=texpos_base > 0 and texpos_base + 15 or nil} end - if filter.flags2 and filter.flags2.building_material then - if filter.flags2.fire_safe then - return "Fire-safe building material"; + return BUTTON_END_PEN +end +local function get_selected_item_pen() + if not SELECTED_ITEM_PEN then + local texpos_base = dfhack.textures.getControlPanelTexposStart() + SELECTED_ITEM_PEN = to_pen{ch='x', fg=COLOR_GREEN, + tile=texpos_base > 0 and texpos_base + 9 or nil} + end + return SELECTED_ITEM_PEN +end + +BuildingplanScreen = defclass(BuildingplanScreen, gui.ZScreen) +BuildingplanScreen.ATTRS { + pass_movement_keys=true, + pass_mouse_clicks=false, + defocusable=false, +} + +-------------------------------- +-- ItemSelection +-- + +local BUILD_TEXT_PEN = to_pen{fg=COLOR_BLACK, bg=COLOR_GREEN, keep_lower=true} +local BUILD_TEXT_HPEN = to_pen{fg=COLOR_WHITE, bg=COLOR_GREEN, keep_lower=true} + +-- map of building type -> {set=set of recently used, list=list of recently used} +-- most recent entries are at the *end* of the list +local recently_used = {} + +local function sort_by_type(a, b) + local ad, bd = a.data, b.data + return ad.item_type < bd.item_type or + (ad.item_type == bd.item_type and ad.item_subtype < bd.item_subtype) or + (ad.item_type == bd.item_type and ad.item_subtype == bd.item_subtype and a.search_key < b.search_key) or + (ad.item_type == bd.item_type and ad.item_subtype == bd.item_subtype and a.search_key == b.search_key and ad.quality > bd.quality) +end + +local function sort_by_recency(a, b) + local tracker = recently_used[uibs.building_type] + if not tracker then return sort_by_type(a, b) end + local recent_a, recent_b = tracker.set[a.search_key], tracker.set[b.search_key] + -- if they're both in the set, return the one with the greater index, + -- indicating more recent + if recent_a and recent_b then return recent_a > recent_b end + if recent_a and not recent_b then return true end + if not recent_a and recent_b then return false end + return sort_by_type(a, b) +end + +local function sort_by_name(a, b) + return a.search_key < b.search_key or + (a.search_key == b.search_key and sort_by_type(a, b)) +end + +local function sort_by_quantity(a, b) + local ad, bd = a.data, b.data + return ad.quantity > bd.quantity or + (ad.quantity == bd.quantity and sort_by_type(a, b)) +end + +ItemSelection = defclass(ItemSelection, widgets.Window) +ItemSelection.ATTRS{ + frame_title='Choose items', + frame={w=56, h=20, l=4, t=8}, + resizable=true, + index=DEFAULT_NIL, + quantity=DEFAULT_NIL, + on_submit=DEFAULT_NIL, + on_cancel=DEFAULT_NIL, +} + +function ItemSelection:init() + local filter = get_cur_filters()[self.index] + self.num_selected = 0 + self.selected_set = {} + local plural = self.quantity == 1 and '' or 's' + + self:addviews{ + widgets.Label{ + frame={t=0, l=0, r=10}, + text={ + get_desc(filter), + plural, + NEWLINE, + ('Select up to %d item%s ('):format(self.quantity, plural), + {text=function() return self.num_selected end}, + ' selected)', + }, + }, + widgets.Label{ + frame={r=0, w=9, t=0, h=3}, + text_pen=BUILD_TEXT_PEN, + text_hpen=BUILD_TEXT_HPEN, + text={ + ' ', NEWLINE, + ' Build ', NEWLINE, + ' ', + }, + on_click=self:callback('submit'), + }, + widgets.FilteredList{ + view_id='flist', + frame={t=3, l=0, r=0, b=4}, + case_sensitive=false, + choices=self:get_choices(sort_by_recency), + icon_width=2, + on_submit=self:callback('toggle_group'), + }, + widgets.CycleHotkeyLabel{ + frame={l=0, b=2}, + key='CUSTOM_CTRL_X', + label='Sort by:', + options={ + {label='Recently used', value=sort_by_recency}, + {label='Name', value=sort_by_name}, + {label='Amount', value=sort_by_quantity}, + }, + on_change=self:callback('on_sort'), + }, + widgets.HotkeyLabel{ + frame={l=0, b=1}, + key='SELECT', + label='Use all/none', + auto_width=true, + on_activate=function() self:toggle_group(self.subviews.flist.list:getSelected()) end, + }, + widgets.HotkeyLabel{ + frame={l=22, b=1}, + key='CUSTOM_CTRL_D', + label='Build', + auto_width=true, + on_activate=self:callback('submit'), + }, + widgets.HotkeyLabel{ + frame={l=38, b=1}, + key='LEAVESCREEN', + label='Go back', + auto_width=true, + on_activate=self:callback('on_cancel'), + }, + widgets.HotkeyLabel{ + frame={l=0, b=0}, + key='KEYBOARD_CURSOR_RIGHT_FAST', + key_sep=' : ', + label='Use one', + auto_width=true, + on_activate=function() self:increment_group(self.subviews.flist.list:getSelected()) end, + }, + widgets.Label{ + frame={l=6, b=0, w=5}, + text_pen=COLOR_LIGHTGREEN, + text='Right', + }, + widgets.HotkeyLabel{ + frame={l=23, b=0}, + key='KEYBOARD_CURSOR_LEFT_FAST', + key_sep=' : ', + label='Use one fewer', + auto_width=true, + on_activate=function() self:decrement_group(self.subviews.flist.list:getSelected()) end, + }, + widgets.Label{ + frame={l=29, b=0, w=4}, + text_pen=COLOR_LIGHTGREEN, + text='Left', + }, + } +end + +-- resort and restore selection +function ItemSelection:on_sort(sort_fn) + local flist = self.subviews.flist + local saved_filter = flist:getFilter() + flist:setFilter('') + flist:setChoices(self:get_choices(sort_fn), flist:getSelected()) + flist:setFilter(saved_filter) +end + +local function make_search_key(str) + local out = '' + for c in str:gmatch("[%w%s]") do + out = out .. c + end + return out +end + +function ItemSelection:get_choices(sort_fn) + local item_ids = getAvailableItems(uibs.building_type, + uibs.building_subtype, uibs.custom_type, self.index - 1) + local buckets = {} + for _,item_id in ipairs(item_ids) do + local item = df.item.find(item_id) + if not item then goto continue end + local desc = dfhack.items.getDescription(item, 0, true) + if buckets[desc] then + local bucket = buckets[desc] + table.insert(bucket.data.item_ids, item_id) + bucket.data.quantity = bucket.data.quantity + 1 + else + local entry = { + search_key=make_search_key(desc), + icon=self:callback('get_entry_icon', item_id), + data={ + item_ids={item_id}, + item_type=item:getType(), + item_subtype=item:getSubtype(), + quantity=1, + quality=item:getQuality(), + selected=0, + }, + } + buckets[desc] = entry end - if filter.flags2.magma_safe then - return "Magma-safe building material"; + ::continue:: + end + local choices = {} + for desc,choice in pairs(buckets) do + local data = choice.data + choice.text = { + {width=10, text=function() return ('[%d/%d]'):format(data.selected, data.quantity) end}, + {gap=2, text=desc}, + } + table.insert(choices, choice) + end + table.sort(choices, sort_fn) + return choices +end + +function ItemSelection:increment_group(idx, choice) + local data = choice.data + if self.quantity <= self.num_selected then return false end + if data.selected >= data.quantity then return false end + data.selected = data.selected + 1 + self.num_selected = self.num_selected + 1 + local item_id = data.item_ids[data.selected] + self.selected_set[item_id] = true + return true +end + +function ItemSelection:decrement_group(idx, choice) + local data = choice.data + if data.selected <= 0 then return false end + local item_id = data.item_ids[data.selected] + self.selected_set[item_id] = nil + self.num_selected = self.num_selected - 1 + data.selected = data.selected - 1 + return true +end + +function ItemSelection:toggle_group(idx, choice) + local data = choice.data + if data.selected > 0 then + while self:decrement_group(idx, choice) do end + else + while self:increment_group(idx, choice) do end + end +end + +function ItemSelection:get_entry_icon(item_id) + return self.selected_set[item_id] and get_selected_item_pen() or nil +end + +local function track_recently_used(choices) + -- use same set for all subtypes + local tracker = ensure_key(recently_used, uibs.building_type) + for _,choice in ipairs(choices) do + local data = choice.data + if data.selected <= 0 then goto continue end + local key = choice.search_key + local recent_set = ensure_key(tracker, 'set') + local recent_list = ensure_key(tracker, 'list') + if recent_set[key] then + if recent_list[#recent_list] ~= key then + for i,v in ipairs(recent_list) do + if v == key then + table.remove(recent_list, i) + table.insert(recent_list, key) + break + end + end + tracker.set = utils.invert(recent_list) + end + else + -- only keep most recent 10 + if #recent_list >= 10 then + -- remove least recently used from list and set + recent_set[table.remove(recent_list, 1)] = nil + end + table.insert(recent_list, key) + recent_set[key] = #recent_list end - return "Generic building material"; + ::continue:: + end +end + +function ItemSelection:submit() + local selected_items = {} + for item_id in pairs(self.selected_set) do + table.insert(selected_items, item_id) end - if filter.vector_id then - return to_title_case(df.job_item_vector_id[filter.vector_id]) + if #selected_items > 0 then + track_recently_used(self.subviews.flist:getChoices()) end - return "Unknown"; + self.on_submit(selected_items) end +function ItemSelection:onInput(keys) + if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + self.on_cancel() + return true + elseif keys._MOUSE_L_DOWN then + local list = self.subviews.flist.list + local idx = list:getIdxUnderMouse() + if idx then + list:setSelected(idx) + local modstate = dfhack.internal.getModstate() + if modstate & 2 > 0 then -- ctrl + local choice = list:getChoices()[idx] + if modstate & 1 > 0 then -- shift + self:decrement_group(idx, choice) + else + self:increment_group(idx, choice) + end + return true + end + end + end + return ItemSelection.super.onInput(self, keys) +end + +ItemSelectionScreen = defclass(ItemSelectionScreen, BuildingplanScreen) +ItemSelectionScreen.ATTRS { + focus_path='dwarfmode/Building/Placement/dfhack/lua/buildingplan/itemselection', + force_pause=true, + pass_pause=false, + index=DEFAULT_NIL, + quantity=DEFAULT_NIL, + on_submit=DEFAULT_NIL, + on_cancel=DEFAULT_NIL, +} + +function ItemSelectionScreen:init() + self:addviews{ + ItemSelection{ + index=self.index, + quantity=self.quantity, + on_submit=self.on_submit, + on_cancel=self.on_cancel, + } + } +end + +-------------------------------- +-- FilterSelection +-- + -- returns whether the items matched by the specified filter can have a quality -- rating. This also conveniently indicates whether an item can be decorated. --- does not need the core suspended --- reverse_idx is 0-based and is expected to be counted from the *last* filter -function item_can_be_improved(btype, subtype, custom, reverse_idx) - local filter = get_filter(btype, subtype, custom, reverse_idx) +local function can_be_improved(idx) + local filter = get_cur_filters()[idx] if filter.flags2 and filter.flags2.building_material then return false; end @@ -106,41 +491,991 @@ function item_can_be_improved(btype, subtype, custom, reverse_idx) filter.item_type ~= df.item_type.BOULDER end --- needs the core suspended --- returns a vector of constructed buildings (usually of size 1, but potentially --- more for constructions) -function construct_buildings_from_ui_state() - local uibs = df.global.buildreq - local world = df.global.world - local direction = world.selected_direction - local _, width, height = dfhack.buildings.getCorrectSize( - world.building_width, world.building_height, uibs.building_type, - uibs.building_subtype, uibs.custom_type, direction) - -- the cursor is at the center of the building; we need the upper-left - -- corner of the building - local pos = guidm.getCursorPos() - pos.x = pos.x - math.floor(width/2) - pos.y = pos.y - math.floor(height/2) - local min_x, max_x = pos.x, pos.x - local min_y, max_y = pos.y, pos.y - if width == 1 and height == 1 and - (world.building_width > 1 or world.building_height > 1) then - min_x = math.ceil(pos.x - world.building_width/2) - max_x = min_x + world.building_width - 1 - min_y = math.ceil(pos.y - world.building_height/2) - max_y = min_y + world.building_height - 1 +local OPTIONS_COL_WIDTH = 28 +local TYPE_COL_WIDTH = 20 +local HEADER_HEIGHT = 5 +local FOOTER_HEIGHT = 4 + +FilterSelection = defclass(FilterSelection, widgets.Window) +FilterSelection.ATTRS{ + frame_title='Choose filters [MOCK -- NOT FUNCTIONAL]', + frame={w=80, h=53, l=30, t=8}, + resizable=true, + index=DEFAULT_NIL, +} + +local STANDIN_PEN = to_pen{fg=COLOR_GREEN, bg=COLOR_GREEN, ch=' '} + +function FilterSelection:init() + self:addviews{ + widgets.Panel{ + view_id='options_panel', + frame={l=0, t=0, b=FOOTER_HEIGHT, w=OPTIONS_COL_WIDTH}, + autoarrange_subviews=true, + subviews={ + widgets.Panel{ + view_id='quality_panel', + frame={l=0, r=0, h=24}, + frame_inset={t=1}, + frame_style=gui.INTERIOR_FRAME, + frame_title='Item quality', + subviews={ + widgets.HotkeyLabel{ + frame={l=0, t=0}, + key='CUSTOM_SHIFT_Q', + }, + widgets.HotkeyLabel{ + frame={l=1, t=0}, + key='CUSTOM_SHIFT_W', + label='Set max quality', + }, + widgets.Panel{ + view_id='quality_slider', + frame={l=0, t=2, w=3, h=15}, + frame_background=STANDIN_PEN, + }, + widgets.Label{ + frame={l=3, t=3}, + text='- Artifact (1)', + }, + widgets.Label{ + frame={l=3, t=5}, + text='- Masterful (3)', + }, + widgets.Label{ + frame={l=3, t=7}, + text='- Exceptional (34)', + }, + widgets.Label{ + frame={l=3, t=9}, + text='- Superior (50)', + }, + widgets.Label{ + frame={l=3, t=11}, + text='- FinelyCrafted (67)', + }, + widgets.Label{ + frame={l=3, t=13}, + text='- WellCrafted (79)', + }, + widgets.Label{ + frame={l=3, t=15}, + text='- Ordinary (206)', + }, + widgets.HotkeyLabel{ + frame={l=0, t=18}, + key='CUSTOM_SHIFT_Z', + }, + widgets.HotkeyLabel{ + frame={l=1, t=18}, + key='CUSTOM_SHIFT_X', + label='Set min quality', + }, + widgets.CycleHotkeyLabel{ + frame={l=0, t=20}, + key='CUSTOM_SHIFT_D', + label='Decorated only:', + options={'No', 'Yes'}, + }, + }, + }, + widgets.ResizingPanel{ + view_id='building_panel', + frame={l=0, r=0}, + frame_inset={t=1}, + frame_style=gui.INTERIOR_FRAME, + frame_title='Building options', + autoarrange_subviews=true, + autoarrange_gap=1, + subviews={ + widgets.WrappedLabel{ + frame={l=0}, + text_to_wrap='These options will affect all items for the current building type.', + }, + widgets.CycleHotkeyLabel{ + frame={l=0}, + key='CUSTOM_SHIFT_G', + label='Building safety:', + options={ + {label='Any', value=0}, + {label='Magma', value=2, pen=COLOR_RED}, + {label='Fire', value=1, pen=COLOR_LIGHTRED}, + }, + }, + }, + }, + widgets.Panel{ + view_id='global_panel', + frame={l=0, r=0, b=0}, + frame_inset={t=1}, + frame_style=gui.INTERIOR_FRAME, + frame_title='Global options', + autoarrange_subviews=true, + subviews={ + widgets.WrappedLabel{ + frame={l=0}, + text_to_wrap='These options will affect the selection of "Generic Materials" for future buildings.', + }, + widgets.Panel{ + frame={h=1}, + }, + widgets.ToggleHotkeyLabel{ + frame={l=0}, + key='CUSTOM_SHIFT_B', + label='Blocks', + label_width=8, + }, + widgets.ToggleHotkeyLabel{ + frame={l=0}, + key='CUSTOM_SHIFT_L', + label='Logs', + label_width=8, + }, + widgets.ToggleHotkeyLabel{ + frame={l=0}, + key='CUSTOM_SHIFT_O', + label='Boulders', + label_width=8, + }, + widgets.ToggleHotkeyLabel{ + frame={l=0}, + key='CUSTOM_SHIFT_P', + label='Bars', + label_width=8, + }, + }, + }, + }, + }, + widgets.Panel{ + view_id='materials_panel', + frame={l=OPTIONS_COL_WIDTH, t=0, b=FOOTER_HEIGHT, r=0}, + subviews={ + widgets.Panel{ + view_id='header', + frame={l=0, t=0, h=HEADER_HEIGHT, r=0}, + subviews={ + widgets.EditField{ + frame={l=1, t=0}, + label_text='Search: ', + on_char=function(ch) return ch:match('%l') end, + }, + widgets.CycleHotkeyLabel{ + frame={l=1, t=2, w=21}, + label='Sort by:', + key='CUSTOM_SHIFT_R', + options={'name', 'available'}, + }, + widgets.ToggleHotkeyLabel{ + frame={l=24, t=2, w=24}, + label='Hide unavailable:', + key='CUSTOM_SHIFT_H', + initial_option=false, + }, + widgets.Label{ + frame={l=1, b=0}, + text='Type', + text_pen=COLOR_LIGHTRED, + }, + widgets.Label{ + frame={l=TYPE_COL_WIDTH, b=0}, + text='Material', + text_pen=COLOR_LIGHTRED, + }, + }, + }, + widgets.Panel{ + view_id='materials_lists', + frame={l=0, t=HEADER_HEIGHT, r=0, b=0}, + frame_style=gui.INTERIOR_FRAME, + subviews={ + widgets.List{ + view_id='materials_categories', + frame={l=1, t=0, b=0, w=TYPE_COL_WIDTH-3}, + scroll_keys={}, + choices={ + {text='Stone', key='CUSTOM_SHIFT_S'}, + {text='Wood', key='CUSTOM_SHIFT_W'}, + {text='Metal', key='CUSTOM_SHIFT_M'}, + {text='Other', key='CUSTOM_SHIFT_O'}, + }, + }, + widgets.List{ + view_id='materials_mats', + frame={l=TYPE_COL_WIDTH, t=0, r=0, b=0}, + choices={ + {text='9 - granite'}, + {text='0 - graphite'}, + }, + }, + }, + }, + widgets.Panel{ + view_id='divider', + frame={l=TYPE_COL_WIDTH-1, t=HEADER_HEIGHT, b=0, w=1}, + on_render=self:callback('draw_divider'), + } + }, + }, + widgets.Panel{ + view_id='footer', + frame={l=0, r=0, b=0, h=FOOTER_HEIGHT}, + frame_inset={l=20, t=1}, + subviews={ + widgets.HotkeyLabel{ + frame={l=0, t=0}, + label='Toggle', + auto_width=true, + key='SELECT', + }, + widgets.HotkeyLabel{ + frame={l=0, t=2}, + label='Done', + auto_width=true, + key='LEAVESCREEN', + }, + widgets.HotkeyLabel{ + frame={l=30, t=0}, + label='Select all', + auto_width=true, + key='CUSTOM_SHIFT_A', + }, + widgets.HotkeyLabel{ + frame={l=30, t=1}, + label='Invert selection', + auto_width=true, + key='CUSTOM_SHIFT_I', + }, + widgets.HotkeyLabel{ + frame={l=30, t=2}, + label='Clear selection', + auto_width=true, + key='CUSTOM_SHIFT_C', + }, + }, + } + } +end + +local texpos = dfhack.textures.getThinBordersTexposStart() +local tp = function(offset) + if texpos == -1 then return nil end + return texpos + offset +end + +local TOP_PEN = to_pen{tile=tp(10), ch=194, fg=COLOR_GREY, bg=COLOR_BLACK} +local MID_PEN = to_pen{tile=tp(4), ch=192, fg=COLOR_GREY, bg=COLOR_BLACK} +local BOT_PEN = to_pen{tile=tp(11), ch=179, fg=COLOR_GREY, bg=COLOR_BLACK} + +function FilterSelection:draw_divider(dc) + local y2 = dc.height - 1 + for y=0,y2 do + dc:seek(0, y) + if y == 0 then + dc:char(nil, TOP_PEN) + elseif y == y2 then + dc:char(nil, BOT_PEN) + else + dc:char(nil, MID_PEN) + end + end +end + +FilterSelectionScreen = defclass(FilterSelectionScreen, BuildingplanScreen) +FilterSelectionScreen.ATTRS { + focus_path='dwarfmode/Building/Placement/dfhack/lua/buildingplan/filterselection', + index=DEFAULT_NIL, +} + +function FilterSelectionScreen:init() + self:addviews{ + FilterSelection{index=self.index} + } +end + +function FilterSelectionScreen:onShow() + df.global.game.main_interface.bottom_mode_selected = -1 +end + +function FilterSelectionScreen:onDismiss() + df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT +end + +-------------------------------- +-- ItemLine +-- + +local function cur_building_has_no_area() + if uibs.building_type == df.building_type.Construction then return false end + local filters = dfhack.buildings.getFiltersByType({}, + uibs.building_type, uibs.building_subtype, uibs.custom_type) + -- this works because all variable-size buildings have either no item + -- filters or a quantity of -1 for their first (and only) item + return filters and filters[1] and (not filters[1].quantity or filters[1].quantity > 0) +end + +local function is_plannable() + return get_cur_filters() and + not (uibs.building_type == df.building_type.Construction + and uibs.building_subtype == df.construction_type.TrackNSEW) +end + +local function is_construction() + return uibs.building_type == df.building_type.Construction +end + +local function is_stairs() + return is_construction() + and uibs.building_subtype == df.construction_type.UpDownStair +end + +local direction_panel_frame = {t=4, h=13, w=46, r=28} + +local direction_panel_types = utils.invert{ + df.building_type.Bridge, + df.building_type.ScrewPump, + df.building_type.WaterWheel, + df.building_type.AxleHorizontal, + df.building_type.Rollers, +} + +local function has_direction_panel() + return direction_panel_types[uibs.building_type] + or (uibs.building_type == df.building_type.Trap + and uibs.building_subtype == df.trap_type.TrackStop) +end + +local pressure_plate_panel_frame = {t=4, h=37, w=46, r=28} + +local function has_pressure_plate_panel() + return uibs.building_type == df.building_type.Trap + and uibs.building_subtype == df.trap_type.PressurePlate +end + +local function is_over_options_panel() + local frame = nil + if has_direction_panel() then + frame = direction_panel_frame + elseif has_pressure_plate_panel() then + frame = pressure_plate_panel_frame + else + return false end + local v = widgets.Widget{frame=frame} + local rect = gui.mkdims_wh(0, 0, dfhack.screen.getWindowSize()) + v:updateLayout(gui.ViewRect{rect=rect}) + return v:getMousePos() +end + +local function to_title_case(str) + str = str:gsub('(%a)([%w_]*)', + function (first, rest) return first:upper()..rest:lower() end) + str = str:gsub('_', ' ') + return str +end + +ItemLine = defclass(ItemLine, widgets.Panel) +ItemLine.ATTRS{ + idx=DEFAULT_NIL, + is_selected_fn=DEFAULT_NIL, + is_hollow_fn=DEFAULT_NIL, + on_select=DEFAULT_NIL, + on_filter=DEFAULT_NIL, + on_clear_filter=DEFAULT_NIL, +} + +function ItemLine:init() + self.frame.h = 1 + self.visible = function() return #get_cur_filters() >= self.idx end + self:addviews{ + widgets.Label{ + frame={t=0, l=0}, + text='*', + auto_width=true, + visible=self.is_selected_fn, + }, + widgets.Label{ + frame={t=0, l=25}, + text={ + {tile=get_button_start_pen}, + {gap=6, tile=get_button_end_pen}, + }, + auto_width=true, + on_click=function() self.on_filter(self.idx) end, + }, + widgets.Label{ + frame={t=0, l=33}, + text={ + {tile=get_button_start_pen}, + {gap=1, tile=get_button_end_pen}, + }, + auto_width=true, + on_click=function() self.on_clear_filter(self.idx) end, + }, + widgets.Label{ + frame={t=0, l=2}, + text={ + {width=21, text=self:callback('get_item_line_text')}, + {gap=3, text='filter', pen=COLOR_GREEN}, + {gap=2, text='x', pen=self:callback('get_x_pen')}, + {gap=3, text=function() return self.note end, + pen=function() return self.note_pen end}, + }, + }, + } +end + +function ItemLine:reset() + self.desc = nil + self.available = nil +end + +function ItemLine:onInput(keys) + if keys._MOUSE_L_DOWN and self:getMousePos() then + self.on_select(self.idx) + end + return ItemLine.super.onInput(self, keys) +end + +function ItemLine:get_x_pen() + return hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx - 1) and + COLOR_GREEN or COLOR_GREY +end + +function get_desc(filter) + local desc = 'Unknown' + if filter.has_tool_use and filter.has_tool_use > -1 then + desc = to_title_case(df.tool_uses[filter.has_tool_use]) + elseif filter.flags2 and filter.flags2.screw then + desc = 'Screw' + elseif filter.item_type and filter.item_type > -1 then + desc = to_title_case(df.item_type[filter.item_type]) + elseif filter.vector_id and filter.vector_id > -1 then + desc = to_title_case(df.job_item_vector_id[filter.vector_id]) + elseif filter.flags2 and filter.flags2.building_material then + desc = 'Building material'; + if filter.flags2.fire_safe then + desc = 'Fire-safe material'; + end + if filter.flags2.magma_safe then + desc = 'Magma-safe material'; + end + end + + if desc:endswith('s') then + desc = desc:sub(1,-2) + end + if desc == 'Trappart' then + desc = 'Mechanism' + elseif desc == 'Wood' then + desc = 'Log' + end + return desc +end + +function ItemLine:get_item_line_text() + local idx = self.idx + local filter = get_cur_filters()[idx] + local quantity = get_quantity(filter, self.is_hollow_fn()) + + self.desc = self.desc or get_desc(filter) + + self.available = self.available or countAvailableItems(uibs.building_type, + uibs.building_subtype, uibs.custom_type, idx - 1) + if self.available >= quantity then + self.note_pen = COLOR_GREEN + self.note = 'Available now' + else + self.note_pen = COLOR_YELLOW + self.note = 'Will link later' + end + + return ('%d %s%s'):format(quantity, self.desc, quantity == 1 and '' or 's') +end + +function ItemLine:reduce_quantity() + if not self.available then return end + local filter = get_cur_filters()[self.idx] + self.available = math.max(0, self.available - get_quantity(filter, self.is_hollow_fn())) +end + +local function get_placement_errors() + local out = '' + for _,str in ipairs(uibs.errors) do + if #out > 0 then out = out .. NEWLINE end + out = out .. str.value + end + return out +end + +-------------------------------- +-- PlannerOverlay +-- + +PlannerOverlay = defclass(PlannerOverlay, overlay.OverlayWidget) +PlannerOverlay.ATTRS{ + default_pos={x=5,y=9}, + default_enabled=true, + viewscreens='dwarfmode/Building/Placement', + frame={w=56, h=20}, +} + +function PlannerOverlay:init() + self.selected = 1 + + local main_panel = widgets.Panel{ + view_id='main', + frame={t=0, l=0, r=0, h=14}, + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, + } + + local function make_is_selected_fn(idx) + return function() return self.selected == idx end + end + + local function on_select_fn(idx) + self.selected = idx + end + + local function is_hollow_fn() + return self.subviews.hollow:getOptionValue() + end + + main_panel:addviews{ + widgets.Label{ + frame={}, + auto_width=true, + text='No items required.', + visible=function() return #get_cur_filters() == 0 end, + }, + ItemLine{view_id='item1', frame={t=0, l=0, r=0}, idx=1, + is_selected_fn=make_is_selected_fn(1), is_hollow_fn=is_hollow_fn, + on_select=on_select_fn, on_filter=self:callback('set_filter'), + on_clear_filter=self:callback('clear_filter')}, + ItemLine{view_id='item2', frame={t=2, l=0, r=0}, idx=2, + is_selected_fn=make_is_selected_fn(2), is_hollow_fn=is_hollow_fn, + on_select=on_select_fn, on_filter=self:callback('set_filter'), + on_clear_filter=self:callback('clear_filter')}, + ItemLine{view_id='item3', frame={t=4, l=0, r=0}, idx=3, + is_selected_fn=make_is_selected_fn(3), is_hollow_fn=is_hollow_fn, + on_select=on_select_fn, on_filter=self:callback('set_filter'), + on_clear_filter=self:callback('clear_filter')}, + ItemLine{view_id='item4', frame={t=6, l=0, r=0}, idx=4, + is_selected_fn=make_is_selected_fn(4), is_hollow_fn=is_hollow_fn, + on_select=on_select_fn, on_filter=self:callback('set_filter'), + on_clear_filter=self:callback('clear_filter')}, + widgets.CycleHotkeyLabel{ + view_id='hollow', + frame={t=3, l=4}, + key='CUSTOM_H', + label='Hollow area:', + visible=is_construction, + options={ + {label='No', value=false}, + {label='Yes', value=true}, + }, + }, + widgets.CycleHotkeyLabel{ + view_id='stairs_top_subtype', + frame={t=4, l=4}, + key='CUSTOM_R', + label='Top Stair Type: ', + visible=is_stairs, + options={ + {label='Auto', value='auto'}, + {label='UpDown', value=df.construction_type.UpDownStair}, + {label='Down', value=df.construction_type.DownStair}, + }, + }, + widgets.CycleHotkeyLabel { + view_id='stairs_bottom_subtype', + frame={t=5, l=4}, + key='CUSTOM_B', + label='Bottom Stair Type: ', + visible=is_stairs, + options={ + {label='Auto', value='auto'}, + {label='UpDown', value=df.construction_type.UpDownStair}, + {label='Up', value=df.construction_type.UpStair}, + }, + }, + widgets.Label{ + frame={b=3, l=17}, + text={ + 'Selected area: ', + {text=function() + return ('%dx%dx%d'):format(get_cur_area_dims(self.saved_placement)) + end + }, + }, + visible=function() + return not cur_building_has_no_area() and (self.saved_placement or is_choosing_area()) + end, + }, + widgets.Panel{ + visible=function() return #get_cur_filters() > 0 end, + subviews={ + widgets.HotkeyLabel{ + frame={b=1, l=0}, + key='STRING_A042', + auto_width=true, + enabled=function() return #get_cur_filters() > 1 end, + on_activate=function() self.selected = ((self.selected - 2) % #get_cur_filters()) + 1 end, + }, + widgets.HotkeyLabel{ + frame={b=1, l=1}, + key='STRING_A047', + label='Prev/next item', + auto_width=true, + enabled=function() return #get_cur_filters() > 1 end, + on_activate=function() self.selected = (self.selected % #get_cur_filters()) + 1 end, + }, + widgets.HotkeyLabel{ + frame={b=1, l=21}, + key='CUSTOM_F', + label='Set filter', + auto_width=true, + on_activate=function() self:set_filter(self.selected) end, + }, + widgets.HotkeyLabel{ + frame={b=1, l=37}, + key='CUSTOM_X', + label='Clear filter', + auto_width=true, + on_activate=function() self:clear_filter(self.selected) end, + enabled=function() + return hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1) + end + }, + widgets.CycleHotkeyLabel{ + view_id='choose', + frame={b=0, l=0, w=25}, + key='CUSTOM_I', + label='Choose from items:', + options={{label='Yes', value=true}, + {label='No', value=false}}, + initial_option=false, + enabled=function() + for idx = 1,4 do + if (self.subviews['item'..idx].available or 0) > 0 then + return true + end + end + end, + }, + widgets.CycleHotkeyLabel{ + view_id='safety', + frame={b=0, l=29, w=25}, + key='CUSTOM_G', + label='Building safety:', + options={ + {label='Any', value=0}, + {label='Magma', value=2, pen=COLOR_RED}, + {label='Fire', value=1, pen=COLOR_LIGHTRED}, + }, + initial_option=0, + on_change=function(heat) + setHeatSafetyFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, heat) + end, + }, + }, + }, + } + + local error_panel = widgets.ResizingPanel{ + view_id='errors', + frame={t=14, l=0, r=0}, + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, + } + + error_panel:addviews{ + widgets.WrappedLabel{ + frame={t=0, l=0, r=0}, + text_pen=COLOR_LIGHTRED, + text_to_wrap=get_placement_errors, + visible=function() return #uibs.errors > 0 end, + }, + widgets.Label{ + frame={t=0, l=0, r=0}, + text_pen=COLOR_GREEN, + text='OK to build', + visible=function() return #uibs.errors == 0 end, + }, + } + + self:addviews{ + main_panel, + error_panel, + } +end + +function PlannerOverlay:reset() + self.subviews.item1:reset() + self.subviews.item2:reset() + self.subviews.item3:reset() + self.subviews.item4:reset() + reset_counts_flag = false +end + +function PlannerOverlay:set_filter(idx) + FilterSelectionScreen{index=idx}:show() +end + +function PlannerOverlay:clear_filter(idx) + setMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx - 1, "") +end + +local function get_placement_data() + local pos = uibs.pos + local direction = uibs.direction + local width, height, depth = get_cur_area_dims() + local _, adjusted_width, adjusted_height = dfhack.buildings.getCorrectSize( + width, height, uibs.building_type, uibs.building_subtype, + uibs.custom_type, direction) + -- get the upper-left corner of the building/area at min z-level + local has_selection = is_choosing_area() + local start_pos = xyz2pos( + has_selection and math.min(uibs.selection_pos.x, pos.x) or pos.x - adjusted_width//2, + has_selection and math.min(uibs.selection_pos.y, pos.y) or pos.y - adjusted_height//2, + has_selection and math.min(uibs.selection_pos.z, pos.z) or pos.z + ) + if uibs.building_type == df.building_type.ScrewPump then + if direction == df.screw_pump_direction.FromSouth then + start_pos.y = start_pos.y + 1 + elseif direction == df.screw_pump_direction.FromEast then + start_pos.x = start_pos.x + 1 + end + end + local min_x, max_x = start_pos.x, start_pos.x + local min_y, max_y = start_pos.y, start_pos.y + local min_z, max_z = start_pos.z, start_pos.z + if adjusted_width == 1 and adjusted_height == 1 + and (width > 1 or height > 1 or depth > 1) then + max_x = min_x + width - 1 + max_y = min_y + height - 1 + max_z = math.max(uibs.selection_pos.z, pos.z) + end + return { + p1=xyz2pos(min_x, min_y, min_z), + p2=xyz2pos(max_x, max_y, max_z), + width=adjusted_width, + height=adjusted_height + } +end + +function PlannerOverlay:save_placement() + self.saved_placement = get_placement_data() + if (uibs.selection_pos:isValid()) then + self.saved_selection_pos_valid = true + self.saved_selection_pos = copyall(uibs.selection_pos) + self.saved_pos = copyall(uibs.pos) + uibs.selection_pos:clear() + else + self.saved_selection_pos = copyall(self.saved_placement.p1) + self.saved_pos = copyall(self.saved_placement.p2) + self.saved_pos.x = self.saved_pos.x + self.saved_placement.width - 1 + self.saved_pos.y = self.saved_pos.y + self.saved_placement.height - 1 + end +end + +function PlannerOverlay:restore_placement() + if self.saved_selection_pos_valid then + uibs.selection_pos = self.saved_selection_pos + self.saved_selection_pos_valid = nil + else + uibs.selection_pos:clear() + end + self.saved_selection_pos = nil + self.saved_pos = nil + local placement_data = self.saved_placement + self.saved_placement = nil + return placement_data +end + +function PlannerOverlay:onInput(keys) + if not is_plannable() then return false end + if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + if uibs.selection_pos:isValid() then + uibs.selection_pos:clear() + return true + end + self.selected = 1 + self.subviews.hollow:setOption(false) + self.subviews.choose:setOption(false) + self:reset() + reset_counts_flag = true + return false + end + if PlannerOverlay.super.onInput(self, keys) then + return true + end + if keys._MOUSE_L_DOWN then + if is_over_options_panel() then return false end + local detect_rect = copyall(self.frame_rect) + detect_rect.height = self.subviews.main.frame_rect.height + + self.subviews.errors.frame_rect.height + detect_rect.y2 = detect_rect.y1 + detect_rect.height - 1 + if self.subviews.main:getMousePos(gui.ViewRect{rect=detect_rect}) + or self.subviews.errors:getMousePos() then + return true + end + if #uibs.errors > 0 then return true end + if dfhack.gui.getMousePos() then + if is_choosing_area() or cur_building_has_no_area() then + local filters = get_cur_filters() + local num_filters = #filters + if num_filters == 0 then + return false -- we don't add value; let the game place it + end + local choose = self.subviews.choose + if choose.enabled() and choose:getOptionValue() then + self:save_placement() + local is_hollow = self.subviews.hollow:getOptionValue() + local chosen_items, active_screens = {}, {} + local pending = num_filters + df.global.game.main_interface.bottom_mode_selected = -1 + for idx = num_filters,1,-1 do + chosen_items[idx] = {} + if (self.subviews['item'..idx].available or 0) > 0 then + active_screens[idx] = ItemSelectionScreen{ + index=idx, + quantity=get_quantity(filters[idx], is_hollow, + self.saved_placement), + on_submit=function(items) + chosen_items[idx] = items + active_screens[idx]:dismiss() + active_screens[idx] = nil + pending = pending - 1 + if pending == 0 then + df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT + self:place_building(self:restore_placement(), chosen_items) + end + end, + on_cancel=function() + for i,scr in pairs(active_screens) do + scr:dismiss() + end + df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT + self:restore_placement() + end, + }:show() + else + pending = pending - 1 + end + end + else + self:place_building(get_placement_data()) + end + return true + elseif not is_choosing_area() then + return false + end + end + end + return keys._MOUSE_L +end + +function PlannerOverlay:render(dc) + if not is_plannable() then return end + self.subviews.errors:updateLayout() + PlannerOverlay.super.render(self, dc) +end + +local GOOD_PEN, BAD_PEN +function reload_cursors() + GOOD_PEN = to_pen{ch='o', fg=COLOR_GREEN, tile=dfhack.screen.findGraphicsTile('CURSORS', 1, 2)} + BAD_PEN = to_pen{ch='X', fg=COLOR_RED, tile=dfhack.screen.findGraphicsTile('CURSORS', 3, 0)} +end +reload_cursors() + +function PlannerOverlay:onRenderFrame(dc, rect) + PlannerOverlay.super.onRenderFrame(self, dc, rect) + + if reset_counts_flag then + self:reset() + self.subviews.safety:setOption(getHeatSafetyFilter( + uibs.building_type, uibs.building_subtype, uibs.custom_type)) + end + + local selection_pos = self.saved_selection_pos or uibs.selection_pos + if not selection_pos or selection_pos.x < 0 then return end + + local pos = self.saved_pos or uibs.pos + local bounds = { + x1 = math.min(selection_pos.x, pos.x), + x2 = math.max(selection_pos.x, pos.x), + y1 = math.min(selection_pos.y, pos.y), + y2 = math.max(selection_pos.y, pos.y), + } + + local hollow = self.subviews.hollow:getOptionValue() + local pen = (self.saved_selection_pos or #uibs.errors == 0) and GOOD_PEN or BAD_PEN + + local function get_overlay_pen(pos) + if not hollow then return pen end + if pos.x == bounds.x1 or pos.x == bounds.x2 or + pos.y == bounds.y1 or pos.y == bounds.y2 then + return pen + end + return gui.TRANSPARENT_PEN + end + + guidm.renderMapOverlay(get_overlay_pen, bounds) +end + +function PlannerOverlay:get_stairs_subtype(pos, corner1, corner2) + local subtype = uibs.building_subtype + if pos.z == corner1.z then + local opt = self.subviews.stairs_bottom_subtype:getOptionValue() + if opt == 'auto' then + local tt = dfhack.maps.getTileType(pos) + local shape = df.tiletype.attrs[tt].shape + if shape ~= df.tiletype_shape.STAIR_DOWN then + subtype = df.construction_type.UpStair + end + else + subtype = opt + end + elseif pos.z == corner2.z then + local opt = self.subviews.stairs_top_subtype:getOptionValue() + if opt == 'auto' then + local tt = dfhack.maps.getTileType(pos) + local shape = df.tiletype.attrs[tt].shape + if shape ~= df.tiletype_shape.STAIR_UP then + subtype = df.construction_type.DownStair + end + else + subtype = opt + end + end + return subtype +end + +function PlannerOverlay:place_building(placement_data, chosen_items) + local p1, p2 = placement_data.p1, placement_data.p2 local blds = {} - for y=min_y,max_y do for x=min_x,max_x do - local bld, err = dfhack.buildings.constructBuilding{ - type=uibs.building_type, subtype=uibs.building_subtype, - custom=uibs.custom_type, pos=xyz2pos(x, y, pos.z), - width=width, height=height, direction=direction} + local hollow = self.subviews.hollow:getOptionValue() + local subtype = uibs.building_subtype + for z=p1.z,p2.z do for y=p1.y,p2.y do for x=p1.x,p2.x do + if hollow and x ~= p1.x and x ~= p2.x and y ~= p1.y and y ~= p2.y then + goto continue + end + local pos = xyz2pos(x, y, z) + if is_stairs() then + subtype = self:get_stairs_subtype(pos, p1, p2) + end + local bld, err = dfhack.buildings.constructBuilding{pos=pos, + type=uibs.building_type, subtype=subtype, custom=uibs.custom_type, + width=placement_data.width, height=placement_data.height, + direction=uibs.direction} if err then for _,b in ipairs(blds) do dfhack.buildings.deconstruct(b) end - error(err) + dfhack.printerr(err .. (' (%d, %d, %d)'):format(pos.x, pos.y, pos.z)) + return end -- assign fields for the types that need them. we can't pass them all in -- to the call to constructBuilding since attempting to assign unrelated @@ -153,133 +1488,166 @@ function construct_buildings_from_ui_state() if k == 'speed' then bld.speed = uibs.speed end end table.insert(blds, bld) - end end - return blds + ::continue:: + end end end + self.subviews.item1:reduce_quantity() + self.subviews.item2:reduce_quantity() + self.subviews.item3:reduce_quantity() + self.subviews.item4:reduce_quantity() + for _,bld in ipairs(blds) do + -- attach chosen items and reduce job_item quantity + if chosen_items then + local job = bld.jobs[0] + local jitems = job.job_items + for idx=1,#get_cur_filters() do + local item_ids = chosen_items[idx] + while jitems[idx-1].quantity > 0 and #item_ids > 0 do + local item_id = item_ids[#item_ids] + local item = df.item.find(item_id) + if not item then + dfhack.printerr(('item no longer available: %d'):format(item_id)) + break + end + if not dfhack.job.attachJobItem(job, item, df.job_item_ref.T_role.Hauled, idx-1, -1) then + dfhack.printerr(('cannot attach item: %d'):format(item_id)) + break + end + jitems[idx-1].quantity = jitems[idx-1].quantity - 1 + item_ids[#item_ids] = nil + end + end + end + addPlannedBuilding(bld) + end + scheduleCycle() + uibs.selection_pos:clear() end --- --- GlobalSettings dialog +-------------------------------- +-- InspectorLine -- -local GlobalSettings = defclass(GlobalSettings, dialogs.MessageBox) -GlobalSettings.focus_path = 'buildingplan_globalsettings' +local function get_building_filters() + local bld = dfhack.gui.getSelectedBuilding() + return dfhack.buildings.getFiltersByType({}, + bld:getType(), bld:getSubtype(), bld:getCustomType()) +end -GlobalSettings.ATTRS{ - settings = {} +InspectorLine = defclass(InspectorLine, widgets.Panel) +InspectorLine.ATTRS{ + idx=DEFAULT_NIL, } -function GlobalSettings:onDismiss() - for k,v in pairs(self.settings) do - -- call back into C++ to save changes - setSetting(k, v) - end +function InspectorLine:init() + self.frame.h = 2 + self.visible = function() return #get_building_filters() >= self.idx end + self:addviews{ + widgets.Label{ + frame={t=0, l=0}, + text={{text=self:callback('get_desc_string')}}, + }, + widgets.Label{ + frame={t=1, l=2}, + text={{text=self:callback('get_status_line')}}, + }, + } end --- does not need the core suspended. -function show_global_settings_dialog(settings) - GlobalSettings{ - frame_title="Buildingplan Global Settings", - settings=settings, - }:show() +function InspectorLine:get_desc_string() + if self.desc then return self.desc end + self.desc = getDescString(dfhack.gui.getSelectedBuilding(), self.idx-1) + return self.desc end -function GlobalSettings:toggle_setting(name) - self.settings[name] = not self.settings[name] +function InspectorLine:get_status_line() + if self.status then return self.status end + local queue_pos = getQueuePosition(dfhack.gui.getSelectedBuilding(), self.idx-1) + if queue_pos <= 0 then + return 'Item attached' + end + self.status = ('Position in line: %d'):format(queue_pos) + return self.status end -function GlobalSettings:get_setting_string(name) - if self.settings[name] then return 'On' end - return 'Off' +function InspectorLine:reset() + self.desc = nil + self.status = nil end -function GlobalSettings:get_setting_pen(name) - if self.settings[name] then return COLOR_LIGHTGREEN end - return COLOR_LIGHTRED +-------------------------------- +-- InspectorOverlay +-- + +InspectorOverlay = defclass(InspectorOverlay, overlay.OverlayWidget) +InspectorOverlay.ATTRS{ + default_pos={x=-41,y=14}, + default_enabled=true, + viewscreens='dwarfmode/ViewSheets/BUILDING', + frame={w=30, h=15}, + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, +} + +function InspectorOverlay:init() + self:addviews{ + widgets.Label{ + frame={t=0, l=0}, + text='Waiting for items:', + }, + InspectorLine{view_id='item1', frame={t=2, l=0}, idx=1}, + InspectorLine{view_id='item2', frame={t=4, l=0}, idx=2}, + InspectorLine{view_id='item3', frame={t=6, l=0}, idx=3}, + InspectorLine{view_id='item4', frame={t=8, l=0}, idx=4}, + widgets.HotkeyLabel{ + frame={t=11, l=0}, + label='adjust filters', + key='CUSTOM_CTRL_F', + }, + widgets.HotkeyLabel{ + frame={t=12, l=0}, + label='make top priority', + key='CUSTOM_CTRL_T', + on_activate=self:callback('make_top_priority'), + }, + } end -function GlobalSettings:is_setting_enabled(name) - return self.settings[name] +function InspectorOverlay:reset() + self.subviews.item1:reset() + self.subviews.item2:reset() + self.subviews.item3:reset() + self.subviews.item4:reset() + reset_inspector_flag = false end -function GlobalSettings:make_setting_label_token(text, key, name, width) - return {text=text, key=key, key_sep=': ', key_pen=COLOR_LIGHTGREEN, - on_activate=self:callback('toggle_setting', name), width=width} +function InspectorOverlay:make_top_priority() + makeTopPriority(dfhack.gui.getSelectedBuilding()) + self:reset() end -function GlobalSettings:make_setting_value_token(name) - return {text=self:callback('get_setting_string', name), - enabled=self:callback('is_setting_enabled', name), - pen=self:callback('get_setting_pen', name), - dpen=COLOR_GRAY} +function InspectorOverlay:onInput(keys) + if not isPlannedBuilding(dfhack.gui.getSelectedBuilding()) then + return false + end + if keys._MOUSE_L_DOWN or keys._MOUSE_R_DOWN or keys.LEAVESCREEN then + self:reset() + end + return InspectorOverlay.super.onInput(self, keys) end --- mockup: ---[[ - Buildingplan Global Settings - - e: Enable all: Off - Enables buildingplan for all building types. Use this to avoid having to - manually enable buildingplan for each building type that you want to plan. - Note that DFHack quickfort will use buildingplan to manage buildings - regardless of whether buildingplan is "enabled" for the building type. - - Allowed types for generic, fire-safe, and magma-safe building material: - b: Blocks: On - s: Boulders: On - w: Wood: On - r: Bars: Off - Changes to these settings will be applied to newly-planned buildings. - - A: Apply building material filter settings to existing planned buildings - Use this if your planned buildings can't be completed because the settings - above were too restrictive when the buildings were originally planned. - - M: Edit list of materials to avoid - potash - pearlash - ash - coal - Buildingplan will avoid using these material types when a planned building's - material filter is set to 'any'. They can stil be matched when they are - explicitly allowed by a planned building's material filter. Changes to this - list take effect for existing buildings immediately. - - g: Allow bags: Off - This allows bags to be placed where a 'coffer' is planned. - - f: Legacy Quickfort Mode: Off - Compatibility mode for the legacy Python-based Quickfort application. This - setting is not needed for DFHack quickfort. ---]] -function GlobalSettings:init() - - self.subviews.label:setText{ - self:make_setting_label_token('Enable all', 'CUSTOM_E', 'all_enabled', 12), - self:make_setting_value_token('all_enabled'), '\n', - ' Enables buildingplan for all building types. Use this to avoid having\n', - ' to manually enable buildingplan for each building type that you want\n', - ' to plan. Note that DFHack quickfort will use buildingplan to manage\n', - ' buildings regardless of whether buildingplan is "enabled" for the\n', - ' building type.\n', - '\n', - 'Allowed types for generic, fire-safe, and magma-safe building material:\n', - self:make_setting_label_token('Blocks', 'CUSTOM_B', 'blocks', 10), - self:make_setting_value_token('blocks'), '\n', - self:make_setting_label_token('Boulders', 'CUSTOM_S', 'boulders', 10), - self:make_setting_value_token('boulders'), '\n', - self:make_setting_label_token('Wood', 'CUSTOM_W', 'logs', 10), - self:make_setting_value_token('logs'), '\n', - self:make_setting_label_token('Bars', 'CUSTOM_R', 'bars', 10), - self:make_setting_value_token('bars'), '\n', - ' Changes to these settings will be applied to newly-planned buildings.\n', - ' If no types are enabled above, then any building material is allowed.\n', - '\n', - self:make_setting_label_token('Legacy Quickfort Mode', 'CUSTOM_F', - 'quickfort_mode', 23), - self:make_setting_value_token('quickfort_mode'), '\n', - ' Compatibility mode for the legacy Python-based Quickfort application.\n', - ' This setting is not needed for DFHack quickfort.' - } +function InspectorOverlay:render(dc) + if not isPlannedBuilding(dfhack.gui.getSelectedBuilding()) then + return + end + if reset_inspector_flag then + self:reset() + end + InspectorOverlay.super.render(self, dc) end +OVERLAY_WIDGETS = { + planner=PlannerOverlay, + inspector=InspectorOverlay, +} + return _ENV