diff --git a/dfhack-config/quickfort/quickfort.txt b/dfhack-config/quickfort/quickfort.txt index c1d70201f..3a57bfbaf 100644 --- a/dfhack-config/quickfort/quickfort.txt +++ b/dfhack-config/quickfort/quickfort.txt @@ -5,19 +5,16 @@ # # If you have edited this file but want to revert to "factory defaults", delete # this file and a fresh one will be copied from -# dfhack-config/default/quickfort/qickfort.txt the next time you start DFHack. +# dfhack-config/default/quickfort/quickfort.txt the next time you start DFHack. # Directory tree to search for blueprints. Can be set to an absolute or relative # path. If set to a relative path, resolves to a directory under the DF folder. +# Note that if you change this directory, you will not automatically pick up +# blueprints written by the DFHack "blueprint" plugin (which always writes to +# the "blueprints" dir). blueprints_dir=blueprints -# Force all blueprint buildings that could be built with any building material -# to only use blocks. The prevents logs, boulders, and bars (e.g. potash and -# coal) from being wasted on constructions. If set to false, buildings will be -# built with any available building material. -buildings_use_blocks=true - -# Set to "true" or "false". If true, will designate dig blueprints in marker +# Set to "true" or "false". If true, will designate all dig blueprints in marker # mode. If false, only cells with dig codes explicitly prefixed with an "m" will # be designated in marker mode. force_marker_mode=false diff --git a/docs/changelog.txt b/docs/changelog.txt index 27581c1f7..b41710d04 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -38,10 +38,12 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - `dwarfmonitor`: fixed a crash when opening the ``prefs`` screen if units have vague preferences ## Misc Improvements -- `buildingplan`: all buildings, furniture, and constructions are now supported (except for the few building types not supported by dfhack itself) -- `buildingplan`: now respects building job_item filters when matching items +- `buildingplan`: all buildings, furniture, and constructions are now supported (except for instruments) +- `buildingplan`: now respects building job_item filters when matching items, so you can set your own programmatic filters for buildings before submitting them to buildingplan - `buildingplan`: default filter setting for max quality changed from ``artifact`` to ``masterwork`` - `buildingplan`: min quality adjustment hotkeys changed from 'qw' to 'QW' to avoid conflict with existing hotkeys for setting roller speed. max quality adjustment hotkeys changed from 'QW' to 'AS' to make room for the min quality hotkey changes. +- `buildingplan`: new global settings page accessible via ``G`` hotkey when on any building build screen; ``Quickfort Mode`` toggle for legacy Python Quickfort has been moved to this page +- `buildingplan`: new global settings for whether generic building materials should match blocks, boulders, logs, and/or bars. defaults are everything but bars. ## API - `buildingplan`: added Lua interface API diff --git a/plugins/buildingplan-planner.cpp b/plugins/buildingplan-planner.cpp index 5c7e447e4..a9c00532e 100644 --- a/plugins/buildingplan-planner.cpp +++ b/plugins/buildingplan-planner.cpp @@ -501,6 +501,33 @@ void migrateV1ToV2() } } +static void init_global_settings(std::map & settings) +{ + settings.clear(); + settings["blocks"] = true; + settings["boulders"] = true; + settings["logs"] = true; + settings["bars"] = false; +} + +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; + return true; +} + void Planner::reset() { debug("resetting Planner state"); @@ -508,6 +535,8 @@ void Planner::reset() planned_buildings.clear(); tasks.clear(); + init_global_settings(global_settings); + migrateV1ToV2(); std::vector items; @@ -584,6 +613,41 @@ static std::string getBucket(const df::job_item & ji, 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(); @@ -599,22 +663,27 @@ bool Planner::registerTasks(PlannedBuilding & pb) 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 vector_id = df::job_item_vector_id::IN_PLAY; auto job_item = job_items[job_item_idx]; - if (job_item->vector_id) - vector_id = job_item->vector_id; auto bucket = getBucket(*job_item, pb.getFilters()); - for (int item_num = 0; item_num < job_item->quantity; ++item_num) + 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) { - int32_t id = bld->id; - 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()); + 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; @@ -801,99 +870,144 @@ void Planner::popInvalidTasks(std::queue> & task_queue) } } -void Planner::doCycle() +void Planner::doVector(df::job_item_vector_id vector_id, + std::map>> & buckets) { - debug("running cycle for %zu registered buildings", - planned_buildings.size()); - for (auto it = tasks.begin(); it != tasks.end();) + 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 & buckets = it->second; - auto other_id = ENUM_ATTR(job_item_vector_id, other, it->first); - 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, it->first).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 item = *item_it; - if (!itemPassesScreen(item)) + 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; - for (auto bucket_it = buckets.begin(); bucket_it != buckets.end();) + } + 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)) { - auto & task_queue = bucket_it->second; - popInvalidTasks(task_queue); - if (task_queue.empty()) + 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)) { - debug("removing empty bucket: %s/%s; %zu bucket(s) left", - ENUM_KEY_STR(job_item_vector_id, it->first).c_str(), - bucket_it->first.c_str(), - buckets.size() - 1); - bucket_it = buckets.erase(bucket_it); - continue; + finalizeBuilding(building); + unregisterBuilding(id); } - 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)) + if (task_queue.empty()) { - 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, it->first).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, it->first).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; + 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); } - ++bucket_it; - } - if (buckets.empty()) + // 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, it->first).c_str(), + 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()); } diff --git a/plugins/buildingplan-planner.h b/plugins/buildingplan-planner.h index 22d0487c3..18cfaf0b1 100644 --- a/plugins/buildingplan-planner.h +++ b/plugins/buildingplan-planner.h @@ -104,6 +104,9 @@ public: std::vector &item_filters; }; + const std::map & getGlobalSettings() const; + bool setGlobalSetting(std::string name, bool value); + void reset(); void addPlannedBuilding(df::building *bld); @@ -117,6 +120,7 @@ public: void doCycle(); private: + std::map global_settings; std::unordered_map, BuildingTypeKeyHash> default_item_filters; @@ -128,6 +132,8 @@ private: 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.cpp b/plugins/buildingplan.cpp index adf3081f5..ad0bdf358 100644 --- a/plugins/buildingplan.cpp +++ b/plugins/buildingplan.cpp @@ -19,7 +19,7 @@ DFHACK_PLUGIN("buildingplan"); #define PLUGIN_VERSION 2.0 REQUIRE_GLOBAL(ui); REQUIRE_GLOBAL(ui_build_selector); -REQUIRE_GLOBAL(world); +REQUIRE_GLOBAL(world); // used in buildingplan library #define MAX_MASK 10 #define MAX_MATERIAL 21 @@ -288,9 +288,9 @@ static bool is_planmode_enabled(BuildingTypeKey key) 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); + auto L = Lua::Core::State; + color_ostream_proxy out(Core::getInstance().getConsole()); + Lua::StackUnwinder top(L); if (!lua_checkstack(L, 5) || !Lua::PushModulePublic( @@ -315,11 +315,11 @@ static std::string get_item_label(const BuildingTypeKey &key, int item_idx) static bool construct_planned_building() { - auto L = Lua::Core::State; - color_ostream_proxy out(Core::getInstance().getConsole()); + auto L = Lua::Core::State; + color_ostream_proxy out(Core::getInstance().getConsole()); - CoreSuspendClaimer suspend; - Lua::StackUnwinder top(L); + CoreSuspendClaimer suspend; + Lua::StackUnwinder top(L); if (!(lua_checkstack(L, 1) && Lua::PushModulePublic(out, L, "plugins.buildingplan", @@ -343,6 +343,36 @@ static bool construct_planned_building() return true; } +static void show_global_settings_dialog() +{ + auto L = Lua::Core::State; + color_ostream_proxy out(Core::getInstance().getConsole()); + Lua::StackUnwinder top(L); + + if (!lua_checkstack(L, 2) || + !Lua::PushModulePublic( + out, L, "plugins.buildingplan", "show_global_settings_dialog")) + { + debug("Failed to push the module"); + return; + } + + lua_newtable(L); + int ctable = lua_gettop(L); + Lua::SetField(L, quickfort_mode, ctable, "quickfort_mode"); + + for (auto & setting : planner.getGlobalSettings()) + { + Lua::SetField(L, setting.second, ctable, setting.first.c_str()); + } + + if (!Lua::SafeCall(out, L, 1, 0)) + { + debug("Failed call to show_global_settings_dialog"); + return; + } +} + struct buildingplan_query_hook : public df::viewscreen_dwarfmodest { typedef df::viewscreen_dwarfmodest interpose_base; @@ -526,7 +556,7 @@ struct buildingplan_place_hook : public df::viewscreen_dwarfmodest } if (input->count(interface_key::CUSTOM_P) || - input->count(interface_key::CUSTOM_F) || + input->count(interface_key::CUSTOM_G) || input->count(interface_key::CUSTOM_D) || input->count(interface_key::CUSTOM_Q) || input->count(interface_key::CUSTOM_W) || @@ -544,9 +574,9 @@ struct buildingplan_place_hook : public df::viewscreen_dwarfmodest Gui::refreshSidebar(); return true; } - if (input->count(interface_key::CUSTOM_SHIFT_F)) + if (input->count(interface_key::CUSTOM_SHIFT_G)) { - quickfort_mode = !quickfort_mode; + show_global_settings_dialog(); return true; } @@ -664,7 +694,7 @@ struct buildingplan_place_hook : public df::viewscreen_dwarfmodest } OutputToggleString(x, y, "Planning Mode", "P", planmode_enabled[key], true, left_margin); - OutputToggleString(x, y, "Quickfort Mode", "F", quickfort_mode, true, left_margin); + OutputHotkeyString(x, y, "Global Settings", "G", true, left_margin); if (!is_planmode_enabled(key)) return; @@ -909,10 +939,21 @@ static void scheduleCycle() { cycle_requested = true; } +static void setSetting(std::string name, bool value) { + if (name == "quickfort_mode") + { + debug("setting quickfort_mode %d -> %d", quickfort_mode, value); + quickfort_mode = value; + return; + } + planner.setGlobalSetting(name, value); +} + DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(isPlannableBuilding), DFHACK_LUA_FUNCTION(addPlannedBuilding), DFHACK_LUA_FUNCTION(doCycle), DFHACK_LUA_FUNCTION(scheduleCycle), + DFHACK_LUA_FUNCTION(setSetting), DFHACK_LUA_END }; diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 2de7db7d2..aa5a71eba 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -4,6 +4,7 @@ local _ENV = mkmodule('plugins.buildingplan') Native functions: + * void setSetting(string name, boolean value) * bool isPlannableBuilding(df::building_type type, int16_t subtype, int32_t custom) * void addPlannedBuilding(df::building *bld) * void doCycle() @@ -11,6 +12,7 @@ local _ENV = mkmodule('plugins.buildingplan') --]] +local dialogs = require('gui.dialogs') local guidm = require('gui.dwarfmode') require('dfhack.buildings') @@ -92,4 +94,114 @@ function construct_building_from_ui_state() return bld end +-- +-- GlobalSettings dialog +-- + +local GlobalSettings = defclass(GlobalSettings, dialogs.MessageBox) +GlobalSettings.focus_path = 'buildingplan_globalsettings' + +GlobalSettings.ATTRS{ + settings = {} +} + +function GlobalSettings:onDismiss() + for k,v in pairs(self.settings) do + -- call back into C++ to save changes + setSetting(k, v) + end +end + +-- does not need the core suspended. +function show_global_settings_dialog(settings) + GlobalSettings{ + frame_title="Buildingplan Global Settings", + settings=settings, + }:show() +end + +function GlobalSettings:toggle_setting(name) + self.settings[name] = not self.settings[name] +end + +function GlobalSettings:get_setting_string(name) + if self.settings[name] then return 'On' end + return 'Off' +end + +function GlobalSettings:is_setting_enabled(name) + return self.settings[name] +end + +function GlobalSettings:make_setting_label_token(text, key, name, width) + return {text=text, key=key, key_sep=': ', key_pen=COLOR_GREEN, + on_activate=self:callback('toggle_setting', name), width=width} +end + +function GlobalSettings:make_setting_value_token(name) + return {text=self:callback('get_setting_string', name), + enabled=self:callback('is_setting_enabled', name), + pen=COLOR_YELLOW, dpen=COLOR_GRAY} +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{ + '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.' + } +end + return _ENV