generalize buildingplan for all building types

but restrict to only the currently supported set so we can still assume only one filter is required for each building.

changes:
- update buildingplan plugin version to 2.0
- new serialization format for planned buildings
- old persistent data is automatically migrated to new format on load
- algorithm now respects job_item filters; items must match job_item filter and buildingplan ItemFilter
- more invalid items are now filtered out, like items encased in ice. are there any others we should be checking (see BadFlags struct)
- items are sorted before job is unsuspended so final item ordering is correct regardless of what order the items were matched and attached
- item counts in filters are kept up to date so if buildingplan is disabled before all filters are matched and the building is completed by DF itself, the item counts will come out correct (though the item ordering and building "roughness" may be incorrect)
- fixes two memory leaks in building finalization code
- allows artifacts to be matched (ItemFilter defaults now top out at Masterful -- Artifact is selectable but must be manually specified)
- add gui to switch between items for buildings that require multiple item types
develop
Myk Taylor 2020-10-16 14:08:52 -07:00
parent 1368fb4003
commit 100b374af7
5 changed files with 719 additions and 237 deletions

@ -12,12 +12,14 @@
#include "modules/Gui.h"
#include "modules/Job.h"
#include "LuaTools.h"
#include "uicommon.h"
#include "buildingplan-planner.h"
#include "buildingplan-lib.h"
static const std::string planned_building_persistence_key_v1 = "buildingplan/constraints";
static const std::string planned_building_persistence_key_v2 = "buildingplan/constraints2";
/*
* ItemFilter
@ -37,24 +39,24 @@ void ItemFilter::clear()
materials.clear();
}
bool ItemFilter::deserialize(PersistentDataItem &config)
bool ItemFilter::deserialize(std::string ser)
{
clear();
std::vector<std::string> tokens;
split_string(&tokens, config.val(), "/");
if (tokens.size() != 2)
split_string(&tokens, ser, "/");
if (tokens.size() != 5)
{
debug("invalid ItemFilter serialization: '%s'", config.val().c_str());
debug("invalid ItemFilter serialization: '%s'", ser.c_str());
return false;
}
if (!deserializeMaterialMask(tokens[0]) || !deserializeMaterials(tokens[1]))
return false;
setMinQuality(config.ival(2) - 1);
setMaxQuality(config.ival(4) - 1);
decorated_only = config.ival(3) - 1;
setMinQuality(atoi(tokens[2].c_str()));
setMaxQuality(atoi(tokens[3].c_str()));
decorated_only = static_cast<bool>(atoi(tokens[4].c_str()));
return true;
}
@ -91,7 +93,8 @@ bool ItemFilter::deserializeMaterials(std::string ser)
return true;
}
void ItemFilter::serialize(PersistentDataItem &config) const
// format: mat,mask,elements/materials,list/minq/maxq/decorated
std::string ItemFilter::serialize() const
{
std::ostringstream ser;
ser << bitfield_to_string(mat_mask, ",") << "/";
@ -101,10 +104,10 @@ void ItemFilter::serialize(PersistentDataItem &config) const
for (size_t i = 1; i < materials.size(); ++i)
ser << "," << materials[i].getToken();
}
config.val() = ser.str();
config.ival(2) = min_quality + 1;
config.ival(4) = max_quality + 1;
config.ival(3) = static_cast<int>(decorated_only) + 1;
ser << "/" << static_cast<int>(min_quality);
ser << "/" << static_cast<int>(max_quality);
ser << "/" << static_cast<int>(decorated_only);
return ser.str();
}
void ItemFilter::clearMaterialMask()
@ -230,20 +233,59 @@ bool ItemFilter::matches(df::item *item) const
* PlannedBuilding
*/
static std::vector<ItemFilter> deserializeFilters(PersistentDataItem &config)
// format: itemfilterser|itemfilterser|...
static std::string serializeFilters(const std::vector<ItemFilter> &filters)
{
std::ostringstream ser;
if (!filters.empty())
{
ser << filters[0].serialize();
for (size_t i = 1; i < filters.size(); ++i)
ser << "|" << filters[i].serialize();
}
return ser.str();
}
static std::vector<ItemFilter> deserializeFilters(std::string ser)
{
// simplified implementation while we can assume there is only one filter
std::vector<std::string> isers;
split_string(&isers, ser, "|");
std::vector<ItemFilter> ret;
ItemFilter itemFilter;
itemFilter.deserialize(config);
ret.push_back(itemFilter);
for (auto & iser : isers)
{
ItemFilter filter;
if (filter.deserialize(iser))
ret.push_back(filter);
}
return ret;
}
static size_t getNumFilters(BuildingTypeKey key)
{
// TODO: get num filters in Lua when we handle all building types
return 1;
auto L = Lua::Core::State;
color_ostream_proxy out(Core::getInstance().getConsole());
Lua::StackUnwinder top(L);
if (!lua_checkstack(L, 4) || !Lua::PushModulePublic(
out, L, "plugins.buildingplan", "get_num_filters"))
{
debug("failed to push the lua method on the stack");
return 0;
}
Lua::Push(L, std::get<0>(key));
Lua::Push(L, std::get<1>(key));
Lua::Push(L, std::get<2>(key));
if (!Lua::SafeCall(out, L, 3, 1))
{
debug("lua call failed");
return 0;
}
int num_filters = lua_tonumber(L, -1);
lua_pop(L, 1);
return num_filters;
}
PlannedBuilding::PlannedBuilding(df::building *building, const std::vector<ItemFilter> &filters)
@ -251,92 +293,27 @@ PlannedBuilding::PlannedBuilding(df::building *building, const std::vector<ItemF
building_id(building->id),
filters(filters)
{
config = DFHack::World::AddPersistentData(planned_building_persistence_key_v1);
config.ival(1) = building_id;
// assume all filter vectors are length 1 for now
filters[0].serialize(config);
config = DFHack::World::AddPersistentData(planned_building_persistence_key_v2);
config.ival(0) = building_id;
config.val() = serializeFilters(filters);
}
PlannedBuilding::PlannedBuilding(PersistentDataItem &config)
: config(config),
building(df::building::find(config.ival(1))),
building_id(config.ival(1)),
filters(deserializeFilters(config))
{ }
bool PlannedBuilding::assignClosestItem(std::vector<df::item *> *items_vector)
building(df::building::find(config.ival(0))),
building_id(config.ival(0)),
filters(deserializeFilters(config.val()))
{
decltype(items_vector->begin()) closest_item;
int32_t closest_distance = -1;
for (auto item_iter = items_vector->begin(); item_iter != items_vector->end(); item_iter++)
if (building)
{
auto item = *item_iter;
if (!filters[0].matches(item))
continue;
auto pos = item->pos;
auto distance = abs(pos.x - building->centerx) +
abs(pos.y - building->centery) +
abs(pos.z - building->z) * 50;
if (closest_distance > -1 && distance >= closest_distance)
continue;
closest_distance = distance;
closest_item = item_iter;
}
if (closest_distance > -1 && assignItem(*closest_item))
{
debug("Item assigned");
items_vector->erase(closest_item);
remove();
return true;
}
return false;
}
void delete_item_fn(df::job_item *x) { delete x; }
bool PlannedBuilding::assignItem(df::item *item)
{
auto ref = df::allocate<df::general_ref_building_holderst>();
if (!ref)
if (filters.size() !=
getNumFilters(toBuildingTypeKey(building)))
{
Core::printerr("Could not allocate general_ref_building_holderst\n");
return false;
debug("invalid ItemFilter vector serialization: '%s'",
config.val().c_str());
building = NULL;
}
ref->building_id = building->id;
if (building->jobs.size() != 1)
return false;
auto job = building->jobs[0];
for_each_(job->job_items, delete_item_fn);
job->job_items.clear();
job->flags.bits.suspend = false;
bool rough = false;
Job::attachJobItem(job, item, df::job_item_ref::Hauled);
if (item->getType() == item_type::BOULDER)
rough = true;
building->mat_type = item->getMaterial();
building->mat_index = item->getMaterialIndex();
job->mat_type = building->mat_type;
job->mat_index = building->mat_index;
if (building->needsDesign())
{
auto act = (df::building_actual *) building;
act->design = new df::building_design();
act->design->flags.bits.rough = rough;
}
return true;
}
// Ensure the building still exists and is in a valid state. It can disappear
@ -361,6 +338,8 @@ df::building * PlannedBuilding::getBuilding()
const std::vector<ItemFilter> & PlannedBuilding::getFilters() const
{
// if we want to be able to dynamically change the filters, we'll need to
// re-bucket the tasks in Planner.
return filters;
}
@ -412,45 +391,112 @@ std::size_t BuildingTypeKeyHash::operator() (const BuildingTypeKey & key) const
* Planner
*/
void Planner::initialize()
{
#define add_building_type(btype, itype) \
item_for_building_type[df::building_type::btype] = df::item_type::itype; \
available_item_vectors[df::item_type::itype] = std::vector<df::item *>(); \
is_relevant_item_type[df::item_type::itype] = true; \
FOR_ENUM_ITEMS(item_type, it)
is_relevant_item_type[it] = false;
add_building_type(Armorstand, ARMORSTAND);
add_building_type(Bed, BED);
add_building_type(Chair, CHAIR);
add_building_type(Coffin, COFFIN);
add_building_type(Door, DOOR);
add_building_type(Floodgate, FLOODGATE);
add_building_type(Hatch, HATCH_COVER);
add_building_type(GrateWall, GRATE);
add_building_type(GrateFloor, GRATE);
add_building_type(BarsVertical, BAR);
add_building_type(BarsFloor, BAR);
add_building_type(Cabinet, CABINET);
add_building_type(Box, BOX);
// skip kennels, farm plot
add_building_type(Weaponrack, WEAPONRACK);
add_building_type(Statue, STATUE);
add_building_type(Slab, SLAB);
add_building_type(Table, TABLE);
// skip roads ... furnaces
add_building_type(WindowGlass, WINDOW);
// skip gem window ... support
add_building_type(AnimalTrap, ANIMALTRAP);
add_building_type(Chain, CHAIN);
add_building_type(Cage, CAGE);
// skip archery target
add_building_type(TractionBench, TRACTION_BENCH);
// skip nest box, hive (tools)
#undef add_building_type
// convert v1 persistent data into v2 format
// we can remove this conversion code once v2 has been live for a while
void migrateV1ToV2()
{
std::vector<PersistentDataItem> configs;
DFHack::World::GetPersistentData(&configs, planned_building_persistence_key_v1);
if (configs.empty())
return;
debug("migrating %zu persisted configs to new format", configs.size());
for (auto config : configs)
{
df::building *bld = df::building::find(config.ival(1));
if (!bld)
{
debug("buliding no longer exists; removing config");
DFHack::World::DeletePersistentData(config);
continue;
}
if (bld->getBuildStage() != 0 || bld->jobs.size() != 1
|| bld->jobs[0]->job_items.size() != 1)
{
debug("building in invalid state; removing config");
DFHack::World::DeletePersistentData(config);
continue;
}
// fix up the building so we can set the material properties later
bld->mat_type = -1;
bld->mat_index = -1;
// the v1 filters are not initialized correctly and will match any item.
// we need to fix them up a bit.
auto filter = bld->jobs[0]->job_items[0];
df::item_type type;
switch (bld->getType())
{
case df::building_type::Armorstand: type = df::item_type::ARMORSTAND; break;
case df::building_type::Bed: type = df::item_type::BED; break;
case df::building_type::Chair: type = df::item_type::CHAIR; break;
case df::building_type::Coffin: type = df::item_type::COFFIN; break;
case df::building_type::Door: type = df::item_type::DOOR; break;
case df::building_type::Floodgate: type = df::item_type::FLOODGATE; break;
case df::building_type::Hatch: type = df::item_type::HATCH_COVER; break;
case df::building_type::GrateWall: type = df::item_type::GRATE; break;
case df::building_type::GrateFloor: type = df::item_type::GRATE; break;
case df::building_type::BarsVertical: type = df::item_type::BAR; break;
case df::building_type::BarsFloor: type = df::item_type::BAR; break;
case df::building_type::Cabinet: type = df::item_type::CABINET; break;
case df::building_type::Box: type = df::item_type::BOX; break;
case df::building_type::Weaponrack: type = df::item_type::WEAPONRACK; break;
case df::building_type::Statue: type = df::item_type::STATUE; break;
case df::building_type::Slab: type = df::item_type::SLAB; break;
case df::building_type::Table: type = df::item_type::TABLE; break;
case df::building_type::WindowGlass: type = df::item_type::WINDOW; break;
case df::building_type::AnimalTrap: type = df::item_type::ANIMALTRAP; break;
case df::building_type::Chain: type = df::item_type::CHAIN; break;
case df::building_type::Cage: type = df::item_type::CAGE; break;
case df::building_type::TractionBench: type = df::item_type::TRACTION_BENCH; break;
default:
debug("building has unhandled type; removing config");
DFHack::World::DeletePersistentData(config);
continue;
}
filter->item_type = type;
filter->item_subtype = -1;
filter->mat_type = -1;
filter->mat_index = -1;
filter->flags1.whole = 0;
filter->flags2.whole = 0;
filter->flags2.bits.allow_artifact = true;
filter->flags3.whole = 0;
filter->flags4 = 0;
filter->flags5 = 0;
filter->metal_ore = -1;
filter->min_dimension = -1;
filter->has_tool_use = df::tool_uses::NONE;
filter->quantity = 1;
std::vector<std::string> tokens;
split_string(&tokens, config.val(), "/");
if (tokens.size() != 2)
{
debug("invalid v1 format; removing config");
DFHack::World::DeletePersistentData(config);
continue;
}
ItemFilter item_filter;
item_filter.deserializeMaterialMask(tokens[0]);
item_filter.deserializeMaterials(tokens[1]);
item_filter.setMinQuality(config.ival(2) - 1);
item_filter.setMaxQuality(config.ival(4) - 1);
if (config.ival(3) - 1)
item_filter.toggleDecoratedOnly();
// create the v2 record
std::vector<ItemFilter> item_filters;
item_filters.push_back(item_filter);
PlannedBuilding pb(bld, item_filters);
// remove the v1 record
DFHack::World::DeletePersistentData(config);
debug("v1 record successfully migrated");
}
}
void Planner::reset()
@ -458,9 +504,12 @@ void Planner::reset()
debug("resetting Planner state");
default_item_filters.clear();
planned_buildings.clear();
tasks.clear();
migrateV1ToV2();
std::vector<PersistentDataItem> items;
DFHack::World::GetPersistentData(&items, planned_building_persistence_key_v1);
DFHack::World::GetPersistentData(&items, planned_building_persistence_key_v2);
debug("found data for %zu planned buildings", items.size());
for (auto i = items.begin(); i != items.end(); i++)
@ -472,7 +521,8 @@ void Planner::reset()
continue;
}
planned_buildings.push_back(pb);
if (registerTasks(pb))
planned_buildings.insert(std::make_pair(pb.getBuilding()->id, pb));
}
}
@ -487,19 +537,19 @@ void Planner::addPlannedBuilding(df::building *bld)
}
// protect against multiple registrations
if (getPlannedBuilding(bld))
if (planned_buildings.count(bld->id) != 0)
{
debug("building already registered");
debug("failed to add building: already registered");
return;
}
PlannedBuilding pb(bld, item_filters);
if (pb.isValid())
if (pb.isValid() && registerTasks(pb))
{
for (auto job : bld->jobs)
job->flags.bits.suspend = true;
planned_buildings.push_back(pb);
planned_buildings.insert(std::make_pair(bld->id, pb));
}
else
{
@ -507,19 +557,107 @@ void Planner::addPlannedBuilding(df::building *bld)
}
}
PlannedBuilding * Planner::getPlannedBuilding(df::building *bld)
static std::string getBucket(const df::job_item & ji,
const std::vector<ItemFilter> & item_filters)
{
std::ostringstream ser;
// pull out and serialize only known relevant fields. if we miss a few, then
// the filter bucket will be slighly less specific than it could be, but
// that's probably ok. we'll just end up bucketing slightly different items
// together. this is only a problem if the different filter at the front of
// the queue doesn't match any available items and blocks filters behind it
// that could be matched.
ser << ji.item_type << ':' << ji.item_subtype << ':' << ji.mat_type << ':'
<< ji.mat_index << ':' << ji.flags1.whole << ':' << ji.flags2.whole
<< ':' << ji.flags3.whole << ':' << ji.flags4 << ':' << ji.flags5 << ':'
<< ji.metal_ore << ':' << ji.has_tool_use;
for (auto & item_filter : item_filters)
{
ser << ':' << item_filter.serialize();
}
return ser.str();
}
bool Planner::registerTasks(PlannedBuilding & pb)
{
for (auto & pb : planned_buildings)
df::building * bld = pb.getBuilding();
if (bld->jobs.size() != 1)
{
debug("unexpected number of jobs: want 1, got %zu", bld->jobs.size());
return false;
}
auto job_items = bld->jobs[0]->job_items;
int num_job_items = job_items.size();
if (num_job_items < 1)
{
debug("unexpected number of job items: want >0, got %d", num_job_items);
return false;
}
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)
{
if (pb.getBuilding() == bld)
return &pb;
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 vectors, %zu buckets, %zu tasks in bucket",
ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(),
bucket.c_str(), id, job_item_idx, tasks.size(),
tasks[vector_id].size(), tasks[vector_id][bucket].size());
}
}
return true;
}
PlannedBuilding * Planner::getPlannedBuilding(df::building *bld)
{
if (!bld || planned_buildings.count(bld->id) == 0)
return NULL;
return &planned_buildings.at(bld->id);
}
bool Planner::isPlannableBuilding(BuildingTypeKey key)
{
return item_for_building_type.count(std::get<0>(key)) > 0;
if (getNumFilters(key) == 0)
return false;
// restrict supported types to be the same as the previous implementation
switch(std::get<0>(key))
{
case df::enums::building_type::Armorstand:
case df::enums::building_type::Bed:
case df::enums::building_type::Chair:
case df::enums::building_type::Coffin:
case df::enums::building_type::Door:
case df::enums::building_type::Floodgate:
case df::enums::building_type::Hatch:
case df::enums::building_type::GrateWall:
case df::enums::building_type::GrateFloor:
case df::enums::building_type::BarsVertical:
case df::enums::building_type::BarsFloor:
case df::enums::building_type::Cabinet:
case df::enums::building_type::Box:
case df::enums::building_type::Weaponrack:
case df::enums::building_type::Statue:
case df::enums::building_type::Slab:
case df::enums::building_type::Table:
case df::enums::building_type::WindowGlass:
case df::enums::building_type::AnimalTrap:
case df::enums::building_type::Chain:
case df::enums::building_type::Cage:
case df::enums::building_type::TractionBench:
return true;
default:
return false;
}
}
Planner::ItemFiltersWrapper Planner::getItemFilters(BuildingTypeKey key)
@ -535,84 +673,250 @@ Planner::ItemFiltersWrapper Planner::getItemFilters(BuildingTypeKey key)
return ItemFiltersWrapper(default_item_filters[key]);
}
void Planner::doCycle()
// precompute a bitmask with bad item flags
struct BadFlags
{
debug("Running Cycle");
if (planned_buildings.size() == 0)
return;
debug("Planned count: %zu", planned_buildings.size());
uint32_t whole;
gather_available_items();
for (auto building_iter = planned_buildings.begin(); building_iter != planned_buildings.end();)
{
if (building_iter->isValid())
BadFlags()
{
auto type = building_iter->getBuilding()->getType();
debug("Trying to allocate %s", enum_item_key_str(type));
df::item_flags flags;
#define F(x) flags.bits.x = true;
F(dump); F(forbid); F(garbage_collect);
F(hostile); F(on_fire); F(rotten); F(trader);
F(in_building); F(construction); F(in_job);
F(owned); F(in_chest); F(removed); F(encased);
#undef F
whole = flags.whole;
}
};
static bool itemPassesScreen(df::item * item)
{
static BadFlags bad_flags;
return !(item->flags.whole & bad_flags.whole)
&& !item->isAssignedToStockpile()
// TODO: make this configurable
&& !(item->getType() == df::item_type::BOX && item->isBag());
}
static bool matchesFilters(df::item * item,
df::job_item * job_item,
const ItemFilter & item_filter)
{
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;
auto required_item_type = item_for_building_type[type];
auto items_vector = &available_item_vectors[required_item_type];
if (items_vector->size() == 0 || !building_iter->assignClosestItem(items_vector))
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_filter.matches(item);
}
// note that this just removes the PlannedBuilding. the tasks will get dropped
// as we discover them in the tasks queues and they fail their isValid() check.
// this "lazy" task cleaning algorithm works because there is no way to
// re-register a building once it has been removed -- if it fails isValid()
// then it has either been built or desroyed. therefore there is no chance of
// duplicate tasks getting added to the tasks queues.
void Planner::unregisterBuilding(int32_t id)
{
if (planned_buildings.count(id) > 0)
{
debug("Unable to allocate an item");
++building_iter;
continue;
}
}
debug("Removing building plan");
building_iter->remove();
building_iter = planned_buildings.erase(building_iter);
planned_buildings.at(id).remove();
planned_buildings.erase(id);
}
}
void Planner::gather_available_items()
static bool isJobReady(df::job * job)
{
debug("Gather available items");
for (auto iter = available_item_vectors.begin(); iter != available_item_vectors.end(); iter++)
int needed_items = 0;
for (auto job_item : job->job_items) { needed_items += job_item->quantity; }
if (needed_items)
{
iter->second.clear();
debug("building needs %d more item(s)", needed_items);
return false;
}
return true;
}
// Precompute a bitmask with the bad flags
df::item_flags bad_flags;
bad_flags.whole = 0;
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;
}
#define F(x) bad_flags.bits.x = true;
F(dump); F(forbid); F(garbage_collect);
F(hostile); F(on_fire); F(rotten); F(trader);
F(in_building); F(construction); F(artifact);
#undef F
// this function does not remove the job_items since their quantity fields are
// now all at 0, so there is no risk of having extra items attached. we don't
// remove them to keep the "finalize with buildingplan active" path as similar
// as possible to the "finalize with buildingplan disabled" path.
static void finalizeBuilding(df::building * bld)
{
debug("finalizing building %d", bld->id);
auto job = bld->jobs[0];
std::vector<df::item*> &items = df::global::world->items.other[df::items_other_id::IN_PLAY];
// 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);
for (size_t i = 0; i < items.size(); i++)
// derive the material properties of the building and job from the first
// applicable item, though if any boulders are involved, it makes the whole
// structure "rough".
bool rough = false;
for (auto attached_item : job->items)
{
df::item *item = attached_item->item;
rough = rough || item->getType() == item_type::BOULDER;
if (bld->mat_type == -1)
{
bld->mat_type = item->getMaterial();
job->mat_type = bld->mat_type;
}
if (bld->mat_index == -1)
{
df::item *item = items[i];
bld->mat_index = item->getMaterialIndex();
job->mat_index = bld->mat_index;
}
}
if (item->flags.whole & bad_flags.whole)
continue;
if (bld->needsDesign())
{
auto act = (df::building_actual *)bld;
if (!act->design)
act->design = new df::building_design();
act->design->flags.bits.rough = rough;
}
df::item_type itype = item->getType();
if (!is_relevant_item_type[itype])
continue;
// we're good to go!
job->flags.bits.suspend = false;
Job::checkBuildingsNow();
}
if (itype == df::item_type::BOX && item->isBag())
continue; //Skip bags
void Planner::popInvalidTasks(std::queue<std::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)
{
PlannedBuilding & pb = planned_buildings.at(id);
if (pb.isValid() &&
pb.getBuilding()->jobs[0]->job_items[task.second]->quantity)
{
break;
}
}
debug("discarding invalid task: bld=%d, job_item_idx=%d",
id, task.second);
task_queue.pop();
unregisterBuilding(id);
}
}
if (item->flags.bits.artifact)
void Planner::doCycle()
{
debug("running cycle for %zu registered buildings",
planned_buildings.size());
for (auto it = tasks.begin(); it != tasks.end();)
{
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 items in vector %s against %zu buckets",
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;
if (item->flags.bits.in_job ||
item->isAssignedToStockpile() ||
item->flags.bits.owned ||
item->flags.bits.in_chest)
for (auto bucket_it = buckets.begin(); bucket_it != buckets.end();)
{
auto & task_queue = bucket_it->second;
popInvalidTasks(task_queue);
if (task_queue.empty())
{
debug("removing empty bucket: %s/%s; %zu buckets 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;
}
available_item_vectors[itype].push_back(item);
auto & task = task_queue.front();
auto id = task.first;
auto & pb = planned_buildings.at(id);
auto building = pb.getBuilding();
auto job = building->jobs[0];
auto filter_idx = task.second;
if (matchesFilters(item, job->job_items[filter_idx],
pb.getFilters()[filter_idx])
&& DFHack::Job::attachJobItem(job, item,
df::job_item_ref::Hauled, filter_idx))
{
MaterialInfo material;
material.decode(item);
ItemTypeInfo item_type;
item_type.decode(item);
debug("attached %s %s to filter %d for %s(%d): %s/%s",
material.toString().c_str(),
item_type.toString().c_str(),
filter_idx,
ENUM_KEY_STR(building_type, building->getType()).c_str(),
id,
ENUM_KEY_STR(job_item_vector_id, 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 remaining",
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;
}
++bucket_it;
}
if (buckets.empty())
break;
}
if (buckets.empty())
{
debug("removing empty vector: %s; %zu vectors left",
ENUM_KEY_STR(job_item_vector_id, it->first).c_str(),
tasks.size() - 1);
it = tasks.erase(it);
}
else
++it;
}
debug("cycle done; %zu registered buildings left",
planned_buildings.size());
}
Planner planner;

@ -1,5 +1,6 @@
#pragma once
#include <queue>
#include <unordered_map>
#include "df/building.h"
@ -16,8 +17,8 @@ public:
ItemFilter();
void clear();
bool deserialize(DFHack::PersistentDataItem &config);
void serialize(DFHack::PersistentDataItem &config) const;
bool deserialize(std::string ser);
std::string serialize() const;
void addMaterialMask(uint32_t mask);
void clearMaterialMask();
@ -40,6 +41,9 @@ public:
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;
@ -59,9 +63,6 @@ public:
PlannedBuilding(df::building *building, const std::vector<ItemFilter> &filters);
PlannedBuilding(DFHack::PersistentDataItem &config);
bool assignClosestItem(std::vector<df::item *> *items_vector);
bool assignItem(df::item *item);
bool isValid() const;
void remove();
@ -71,8 +72,8 @@ public:
private:
DFHack::PersistentDataItem config;
df::building *building;
df::building::key_field_type building_id;
std::vector<ItemFilter> filters;
const df::building::key_field_type building_id;
const std::vector<ItemFilter> filters;
};
// building type, subtype, custom
@ -103,7 +104,6 @@ public:
std::vector<ItemFilter> &item_filters;
};
void initialize();
void reset();
void addPlannedBuilding(df::building *bld);
@ -117,15 +117,17 @@ public:
void doCycle();
private:
std::map<df::building_type, df::item_type> item_for_building_type;
std::unordered_map<BuildingTypeKey,
std::vector<ItemFilter>,
BuildingTypeKeyHash> default_item_filters;
std::map<df::item_type, std::vector<df::item *>> available_item_vectors;
std::map<df::item_type, bool> is_relevant_item_type; //Needed for fast check when looping over all items
std::vector<PlannedBuilding> planned_buildings;
void gather_available_items();
// 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);
};
extern Planner planner;

@ -1,5 +1,3 @@
#include <unordered_map>
#include "df/entity_position.h"
#include "df/interface_key.h"
#include "df/ui_build_selector.h"
@ -17,7 +15,7 @@
#include "buildingplan-lib.h"
DFHACK_PLUGIN("buildingplan");
#define PLUGIN_VERSION 0.15
#define PLUGIN_VERSION 2.0
REQUIRE_GLOBAL(ui);
REQUIRE_GLOBAL(ui_build_selector);
REQUIRE_GLOBAL(world);
@ -287,6 +285,33 @@ static bool is_planmode_enabled(BuildingTypeKey key)
return planmode_enabled[key] || quickfort_mode;
}
static std::string get_item_label(const BuildingTypeKey &key, int item_idx)
{
auto L = Lua::Core::State;
color_ostream_proxy out(Core::getInstance().getConsole());
Lua::StackUnwinder top(L);
if (!lua_checkstack(L, 5) ||
!Lua::PushModulePublic(
out, L, "plugins.buildingplan", "get_item_label"))
return "Failed push";
Lua::Push(L, std::get<0>(key));
Lua::Push(L, std::get<1>(key));
Lua::Push(L, std::get<2>(key));
Lua::Push(L, item_idx);
if (!Lua::SafeCall(out, L, 4, 1))
return "Failed call";
const char *s = lua_tostring(L, -1);
if (!s)
return "No string";
lua_pop(L, 1);
return s;
}
static bool construct_planned_building()
{
auto L = Lua::Core::State;
@ -318,6 +343,16 @@ struct buildingplan_query_hook : public df::viewscreen_dwarfmodest
{
typedef df::viewscreen_dwarfmodest interpose_base;
// no non-static fields allowed (according to VTableInterpose.h)
static df::building *bld;
static PlannedBuilding *pb;
static int filter_count;
static int filter_idx;
// logic is reversed since we're starting at the last filter
bool hasNextFilter() const { return filter_idx > 0; }
bool hasPrevFilter() const { return filter_idx + 1 < filter_count; }
bool isInPlannedBuildingQueryMode()
{
return (ui->main.mode == df::ui_sidebar_mode::QueryBuilding ||
@ -325,21 +360,45 @@ struct buildingplan_query_hook : public df::viewscreen_dwarfmodest
planner.getPlannedBuilding(world->selected_building);
}
// reinit static fields when selected building changes
void initStatics()
{
df::building *cur_bld = world->selected_building;
if (bld != cur_bld)
{
bld = cur_bld;
pb = planner.getPlannedBuilding(bld);
filter_count = pb->getFilters().size();
filter_idx = filter_count - 1;
}
}
bool handleInput(set<df::interface_key> *input)
{
if (!isInPlannedBuildingQueryMode())
return false;
initStatics();
if (input->count(interface_key::SUSPENDBUILDING))
return true; // Don't unsuspend planned buildings
if (input->count(interface_key::DESTROYBUILDING))
{
// remove persistent data and allow the parent to handle the key
// so the building is removed
planner.getPlannedBuilding(world->selected_building)->remove();
// remove persistent data
pb->remove();
// still allow the building to be removed
return false;
}
// ctrl+Right
if (input->count(interface_key::A_MOVE_E_DOWN) && hasNextFilter())
--filter_idx;
// ctrl+Left
else if (input->count(interface_key::A_MOVE_W_DOWN) && hasPrevFilter())
++filter_idx;
else
return false;
return true;
}
DEFINE_VMETHOD_INTERPOSE(void, feed, (set<df::interface_key> *input))
@ -355,6 +414,8 @@ struct buildingplan_query_hook : public df::viewscreen_dwarfmodest
if (!isInPlannedBuildingQueryMode())
return;
initStatics();
// Hide suspend toggle option
auto dims = Gui::getDwarfmodeViewDims();
int left_margin = dims.menu_x1 + 1;
@ -363,10 +424,13 @@ struct buildingplan_query_hook : public df::viewscreen_dwarfmodest
Screen::Pen pen(' ', COLOR_BLACK);
Screen::fillRect(pen, x, y, dims.menu_x2, y);
// all current buildings only have one filter
auto & filter = planner.getPlannedBuilding(world->selected_building)->getFilters()[0];
auto & filter = pb->getFilters()[filter_idx];
y = 24;
OutputString(COLOR_WHITE, x, y, "Planned Building Filter", true, left_margin);
std::string item_label =
stl_sprintf("Item %d of %d", filter_count - filter_idx, filter_count);
OutputString(COLOR_WHITE, x, y, "Planned Building Filter", true, left_margin + 1);
OutputString(COLOR_WHITE, x, y, item_label.c_str(), true, left_margin + 1);
OutputString(COLOR_WHITE, x, y, get_item_label(toBuildingTypeKey(bld), filter_idx).c_str(), true, left_margin);
++y;
OutputString(COLOR_BROWN, x, y, "Min Quality: ", false, left_margin);
OutputString(COLOR_BLUE, x, y, filter.getMinQuality(), true, left_margin);
@ -380,13 +444,35 @@ struct buildingplan_query_hook : public df::viewscreen_dwarfmodest
auto filters = filter.getMaterials();
for (auto it = filters.begin(); it != filters.end(); ++it)
OutputString(COLOR_BLUE, x, y, "*" + *it, true, left_margin);
++y;
if (hasPrevFilter())
OutputHotkeyString(x, y, "Prev Item", "Ctrl+Left", true, left_margin);
if (hasNextFilter())
OutputHotkeyString(x, y, "Next Item", "Ctrl+Right", true, left_margin);
}
};
df::building * buildingplan_query_hook::bld;
PlannedBuilding * buildingplan_query_hook::pb;
int buildingplan_query_hook::filter_count;
int buildingplan_query_hook::filter_idx;
struct buildingplan_place_hook : public df::viewscreen_dwarfmodest
{
typedef df::viewscreen_dwarfmodest interpose_base;
// no non-static fields allowed (according to VTableInterpose.h)
static BuildingTypeKey key;
static std::vector<ItemFilter>::reverse_iterator filter_rbegin;
static std::vector<ItemFilter>::reverse_iterator filter_rend;
static std::vector<ItemFilter>::reverse_iterator filter;
static int filter_count;
static int filter_idx;
bool hasNextFilter() const { return filter + 1 != filter_rend; }
bool hasPrevFilter() const { return filter != filter_rbegin; }
bool isInPlannedBuildingPlacementMode()
{
return ui->main.mode == ui_sidebar_mode::Build &&
@ -395,6 +481,22 @@ struct buildingplan_place_hook : public df::viewscreen_dwarfmodest
planner.isPlannableBuilding(toBuildingTypeKey(ui_build_selector));
}
// reinit static fields when selected building type changes
void initStatics()
{
BuildingTypeKey cur_key = toBuildingTypeKey(ui_build_selector);
if (key != cur_key)
{
key = cur_key;
auto wrapper = planner.getItemFilters(key);
filter_rbegin = wrapper.rbegin();
filter_rend = wrapper.rend();
filter = filter_rbegin;
filter_count = wrapper.get().size();
filter_idx = filter_count - 1;
}
}
bool handleInput(set<df::interface_key> *input)
{
if (!isInPlannedBuildingPlacementMode())
@ -403,6 +505,8 @@ struct buildingplan_place_hook : public df::viewscreen_dwarfmodest
return false;
}
initStatics();
if (in_dummy_screen)
{
if (input->count(interface_key::SELECT) || input->count(interface_key::SEC_SELECT)
@ -425,7 +529,6 @@ struct buildingplan_place_hook : public df::viewscreen_dwarfmodest
show_help = true;
}
BuildingTypeKey key = toBuildingTypeKey(ui_build_selector);
if (input->count(interface_key::CUSTOM_SHIFT_P))
{
planmode_enabled[key] = !planmode_enabled[key];
@ -453,8 +556,6 @@ struct buildingplan_place_hook : public df::viewscreen_dwarfmodest
return true;
}
// all current buildings only have one filter
auto filter = planner.getItemFilters(key).rbegin();
if (input->count(interface_key::CUSTOM_SHIFT_M))
Screen::show(dts::make_unique<ViewscreenChooseMaterial>(*filter), plugin_self);
else if (input->count(interface_key::CUSTOM_Q))
@ -467,6 +568,18 @@ struct buildingplan_place_hook : public df::viewscreen_dwarfmodest
filter->incMaxQuality();
else if (input->count(interface_key::CUSTOM_SHIFT_D))
filter->toggleDecoratedOnly();
// ctrl+Right
else if (input->count(interface_key::A_MOVE_E_DOWN) && hasNextFilter())
{
++filter;
--filter_idx;
}
// ctrl+Left
else if (input->count(interface_key::A_MOVE_W_DOWN) && hasPrevFilter())
{
--filter;
++filter_idx;
}
else
return false;
return true;
@ -481,7 +594,6 @@ struct buildingplan_place_hook : public df::viewscreen_dwarfmodest
DEFINE_VMETHOD_INTERPOSE(void, render, ())
{
bool plannable = isInPlannedBuildingPlacementMode();
BuildingTypeKey key = toBuildingTypeKey(ui_build_selector);
if (plannable && is_planmode_enabled(key))
{
if (ui_build_selector->stage < 1)
@ -506,6 +618,8 @@ struct buildingplan_place_hook : public df::viewscreen_dwarfmodest
if (!plannable)
return;
initStatics();
auto dims = Gui::getDwarfmodeViewDims();
int left_margin = dims.menu_x1 + 1;
int x = left_margin;
@ -539,8 +653,13 @@ struct buildingplan_place_hook : public df::viewscreen_dwarfmodest
if (!is_planmode_enabled(key))
return;
auto filter = planner.getItemFilters(key).rbegin();
y += 2;
std::string title =
stl_sprintf("Filter for Item %d of %d:",
filter_count - filter_idx, filter_count);
OutputString(COLOR_WHITE, x, y, title.c_str(), true, left_margin + 1);
OutputString(COLOR_WHITE, x, y, get_item_label(key, filter_idx).c_str(), true, left_margin);
OutputHotkeyString(x, y, "Min Quality: ", "qw");
OutputString(COLOR_BROWN, x, y, filter->getMinQuality(), true, left_margin);
@ -554,9 +673,22 @@ struct buildingplan_place_hook : public df::viewscreen_dwarfmodest
for (auto it = filter_descriptions.begin();
it != filter_descriptions.end(); ++it)
OutputString(COLOR_BROWN, x, y, " *" + *it, true, left_margin);
y += 2;
if (hasPrevFilter())
OutputHotkeyString(x, y, "Prev Item", "Ctrl+Left", true, left_margin);
if (hasNextFilter())
OutputHotkeyString(x, y, "Next Item", "Ctrl+Right", true, left_margin);
}
};
BuildingTypeKey buildingplan_place_hook::key;
std::vector<ItemFilter>::reverse_iterator buildingplan_place_hook::filter_rbegin;
std::vector<ItemFilter>::reverse_iterator buildingplan_place_hook::filter_rend;
std::vector<ItemFilter>::reverse_iterator buildingplan_place_hook::filter;
int buildingplan_place_hook::filter_count;
int buildingplan_place_hook::filter_idx;
struct buildingplan_room_hook : public df::viewscreen_dwarfmodest
{
typedef df::viewscreen_dwarfmodest interpose_base;
@ -701,7 +833,6 @@ DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <Plug
PluginCommand(
"buildingplan", "Plan building construction before you have materials",
buildingplan_cmd, false, "Run 'buildingplan debug [on|off]' to toggle debugging, or 'buildingplan version' to query the plugin version."));
planner.initialize();
return CR_OK;
}

@ -130,8 +130,6 @@ DFhackCExport command_result plugin_init ( color_ostream &out, vector <PluginCom
buildings.push_back(BuildingInfo("N",df::building_type::NestBox,"Nest Box",1,1));
buildings.push_back(BuildingInfo("~h",df::building_type::Hive,"Hive",1,1));
planner.initialize();
return CR_OK;
}

@ -14,6 +14,53 @@ local _ENV = mkmodule('plugins.buildingplan')
local guidm = require('gui.dwarfmode')
require('dfhack.buildings')
-- does not need the core suspended
function get_num_filters(btype, subtype, custom)
local filters = dfhack.buildings.getFiltersByType(
{}, btype, subtype, custom)
if filters then return #filters end
return 0
end
local function to_title_case(str)
str = str:gsub('(%a)([%w_]*)',
function (first, rest) return first:upper()..rest:lower() end)
str = str:gsub('_', ' ')
return str
end
-- returns a reasonable label for the item based on the qualities of the filter
-- does not need the core suspended
-- reverse_idx is 0-based and is expected to be counted from the *last* filter
function get_item_label(btype, subtype, custom, reverse_idx)
local filters = dfhack.buildings.getFiltersByType(
{}, btype, subtype, custom)
if not filters then return 'No item' end
if reverse_idx < 0 or reverse_idx >= #filters then
return 'Invalid index'
end
local filter = filters[#filters-reverse_idx]
if filter.has_tool_use then
return to_title_case(df.tool_uses[filter.has_tool_use])
end
if filter.item_type then
return to_title_case(df.item_type[filter.item_type])
end
if filter.flags2 and filter.flags2.building_material then
if filter.flags2.fire_safe then
return "Fire-safe building material";
end
if filter.flags2.magma_safe then
return "Magma-safe building material";
end
return "Generic building material";
end
if filter.vector_id then
return to_title_case(df.job_item_vector_id[filter.vector_id])
end
return "Unknown";
end
-- needs the core suspended
function construct_building_from_ui_state()
local uibs = df.global.ui_build_selector