Merge pull request #2882 from myk002/myk_buildingplan

buildingplan, first draft
develop
Myk 2023-02-26 11:20:36 -08:00 committed by GitHub
commit 58f0c3ea02
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 3120 additions and 3446 deletions

@ -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 <buildingplan-filters>`
However, quickfort *does* use `buildingplan's filters <buildingplan>`
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

@ -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 <setting> true|false
buildingplan [status]
buildingplan set <setting> (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

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

@ -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 <queue>
#include <string>
#include <vector>
#include <unordered_map>
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<int32_t, PlannedBuilding> planned_buildings;
// vector id -> filter bucket -> queue of (building id, job_item index)
map<df::job_item_vector_id, map<string, queue<pair<int32_t, int>>>> 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<string> &parameters);
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 <PluginCommand> &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<PersistentDataItem> 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<Lua::LuaLambda&&>(args_lambda),
std::forward<Lua::LuaLambda&&>(res_lambda));
}
static command_result do_command(color_ostream &out, vector<string> &parameters) {
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 &param : 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<pair<int32_t, int>> & 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<string, queue<pair<int32_t, int>>> & 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<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);
}
};
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<df::job_item_vector_id> getVectorIds(color_ostream &out, df::job_item *job_item)
{
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(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
};

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

File diff suppressed because it is too large Load Diff

@ -1,140 +0,0 @@
#pragma once
#include <queue>
#include <unordered_map>
#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<DFHack::MaterialInfo> materials);
void incMinQuality();
void decMinQuality();
void incMaxQuality();
void decMaxQuality();
void toggleDecoratedOnly();
uint32_t getMaterialMask() const;
std::vector<std::string> 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<DFHack::MaterialInfo> 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<ItemFilter> &filters);
PlannedBuilding(DFHack::PersistentDataItem &config);
bool isValid() const;
void remove();
df::building * getBuilding();
const std::vector<ItemFilter> & getFilters() const;
private:
DFHack::PersistentDataItem config;
df::building *building;
const df::building::key_field_type building_id;
const std::vector<ItemFilter> filters;
};
// building type, subtype, custom
typedef std::tuple<df::building_type, int16_t, int32_t> 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<ItemFilter> & item_filters)
: item_filters(item_filters) { }
std::vector<ItemFilter>::reverse_iterator rbegin() const { return item_filters.rbegin(); }
std::vector<ItemFilter>::reverse_iterator rend() const { return item_filters.rend(); }
const std::vector<ItemFilter> & get() const { return item_filters; }
private:
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);
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<std::string, bool> global_settings;
std::unordered_map<BuildingTypeKey,
std::vector<ItemFilter>,
BuildingTypeKeyHash> default_item_filters;
// building id -> PlannedBuilding
std::unordered_map<int32_t, PlannedBuilding> planned_buildings;
// vector id -> filter bucket -> queue of (building id, job_item index)
std::map<df::job_item_vector_id, std::map<std::string, std::queue<std::pair<int32_t, int>>>> tasks;
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;

@ -1,226 +0,0 @@
#include "buildingplan.h"
#include <df/entity_position.h>
#include <df/job_type.h>
#include <df/world.h>
#include <modules/World.h>
#include <modules/Units.h>
#include <modules/Buildings.h>
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<Units::NoblePosition> getUniqueNoblePositions(df::unit *unit)
{
std::vector<Units::NoblePosition> 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<Units::NoblePosition> ReservedRoom::getOwnersNobleCode()
{
if (!building->owner)
return std::vector<Units::NoblePosition> ();
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<PersistentDataItem> 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;

@ -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<DFHack::Units::NoblePosition> 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<ReservedRoom> reservedRooms;
void addRoom(ReservedRoom &rr);
};
bool canReserveRoom(df::building *building);
std::vector<DFHack::Units::NoblePosition> getUniqueNoblePositions(df::unit *unit);
extern RoomMonitor roomMonitor;

File diff suppressed because it is too large Load Diff

@ -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 <deque>
typedef std::deque<std::pair<int32_t, int>> Bucket;
typedef std::map<df::job_item_vector_id, std::map<std::string, Bucket>> 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<df::job_item_vector_id> 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<df::job_item *> &jitems);
void finalizeBuilding(DFHack::color_ostream &out, df::building *bld);

@ -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 <unordered_map>
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<df::job_item *> &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<int32_t, PlannedBuilding> &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<string, Bucket> &buckets,
unordered_map<int32_t, PlannedBuilding> &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<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 buildingplan_cycle(color_ostream &out, Tasks &tasks,
unordered_map<int32_t, PlannedBuilding> &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());
}

@ -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<string> 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<int32_t>()(static_cast<int32_t>(std::get<0>(key)));
std::size_t h2 = std::hash<int16_t>()(std::get<1>(key));
std::size_t h3 = std::hash<int32_t>()(std::get<2>(key));
return h1 ^ rotl_size_t(h2, 8) ^ rotl_size_t(h3, 16);
}

@ -0,0 +1,22 @@
#pragma once
#include "df/building_type.h"
#include <tuple>
#include <string>
namespace DFHack {
class color_ostream;
}
// building type, subtype, custom
struct BuildingTypeKey : public std::tuple<df::building_type, int16_t, int32_t> {
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;
};

@ -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<const df::job_item *> &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<const df::job_item *> &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<ItemFilter> 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());
}

@ -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<const df::job_item *> &jitems);
DefaultItemFilters(DFHack::color_ostream &out, DFHack::PersistentDataItem &filter_config, const std::vector<const df::job_item *> &jitems);
void setItemFilter(DFHack::color_ostream &out, const ItemFilter &filter, int index);
const std::vector<ItemFilter> & getItemFilters() const { return item_filters; }
private:
DFHack::PersistentDataItem filter_config;
std::vector<ItemFilter> item_filters;
};

@ -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<DFHack::MaterialInfo> &materials) {
if (ser.empty())
return true;
vector<string> 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<string> 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<bool>(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<int>(min_quality);
ser << "/" << static_cast<int>(max_quality);
ser << "/" << static_cast<int>(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<df::item_quality>(quality);
clampItemQuality(&min_quality);
if (max_quality < min_quality)
max_quality = min_quality;
}
void ItemFilter::setMaxQuality(int quality) {
max_quality = static_cast<df::item_quality>(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<DFHack::MaterialInfo> &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<string> ItemFilter::getMaterials() const {
vector<string> 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<ItemFilter> deserialize_item_filters(color_ostream &out, const string &serialized) {
std::vector<ItemFilter> filters;
vector<string> 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<ItemFilter> &filters) {
vector<string> strs;
for (auto &filter : filters) {
strs.emplace_back(filter.serialize());
}
return join_strings(";", strs);
}

@ -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<DFHack::MaterialInfo> &materials);
std::string getMinQuality() const;
std::string getMaxQuality() const;
bool getDecoratedOnly() const;
uint32_t getMaterialMask() const;
std::vector<std::string> 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<DFHack::MaterialInfo> materials;
};
std::vector<ItemFilter> deserialize_item_filters(DFHack::color_ostream &out, const std::string &serialized);
std::string serialize_item_filters(const std::vector<ItemFilter> &filters);

@ -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<vector<df::job_item_vector_id>> get_vector_ids(color_ostream &out, int bld_id) {
vector<vector<df::job_item_vector_id>> 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<vector<df::job_item_vector_id>> deserialize_vector_ids(color_ostream &out, PersistentDataItem &bld_config) {
vector<vector<df::job_item_vector_id>> ret;
vector<string> 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<string> joined;
split_string(&joined, serialized, ";");
for (auto &str : joined) {
vector<string> lst;
split_string(&lst, str, ",");
vector<df::job_item_vector_id> 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<ItemFilter> get_item_filters(color_ostream &out, PersistentDataItem &bld_config) {
std::vector<ItemFilter> ret;
vector<string> 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<df::job_item_vector_id>> &vector_ids, const vector<ItemFilter> &item_filters) {
vector<string> 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<ItemFilter> &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;
}

@ -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<std::vector<df::job_item_vector_id>> vector_ids;
const HeatSafety heat_safety;
const std::vector<ItemFilter> item_filters;
PlannedBuilding(DFHack::color_ostream &out, df::building *bld, HeatSafety heat, const std::vector<ItemFilter> &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;
};

File diff suppressed because it is too large Load Diff