Merge remote-tracking branch 'myk002/buildingplan_building_material_filters' into develop

Conflicts:
	docs/changelog.txt
develop
lethosor 2020-10-28 23:57:55 -04:00
commit d508ad3c4b
No known key found for this signature in database
GPG Key ID: 76A269552F4F58C1
6 changed files with 378 additions and 106 deletions

@ -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

@ -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

@ -501,6 +501,33 @@ void migrateV1ToV2()
}
}
static void init_global_settings(std::map<std::string, bool> & settings)
{
settings.clear();
settings["blocks"] = true;
settings["boulders"] = true;
settings["logs"] = true;
settings["bars"] = false;
}
const std::map<std::string, bool> & 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<PersistentDataItem> 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<df::job_item_vector_id> getVectorIds(df::job_item *job_item,
const std::map<std::string, bool> & global_settings)
{
std::vector<df::job_item_vector_id> 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<std::pair<int32_t, int>> & task_queue)
}
}
void Planner::doCycle()
void Planner::doVector(df::job_item_vector_id vector_id,
std::map<std::string, std::queue<std::pair<int32_t, int>>> & 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<df::job_item_vector_id> 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());
}

@ -104,6 +104,9 @@ public:
std::vector<ItemFilter> &item_filters;
};
const std::map<std::string, bool> & 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<std::string, bool> global_settings;
std::unordered_map<BuildingTypeKey,
std::vector<ItemFilter>,
BuildingTypeKeyHash> default_item_filters;
@ -128,6 +132,8 @@ private:
bool registerTasks(PlannedBuilding &plannedBuilding);
void unregisterBuilding(int32_t id);
void popInvalidTasks(std::queue<std::pair<int32_t, int>> &task_queue);
void doVector(df::job_item_vector_id vector_id,
std::map<std::string, std::queue<std::pair<int32_t, int>>> & buckets);
};
extern Planner planner;

@ -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
};

@ -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