dfhack/plugins/buildingplan-planner.cpp

1075 lines
34 KiB
C++

#include <functional>
#include <climits> // for CHAR_BIT
#include "df/building_design.h"
#include "df/building_doorst.h"
#include "df/building_type.h"
#include "df/general_ref_building_holderst.h"
#include "df/job_item.h"
#include "df/ui_build_selector.h"
#include "modules/Buildings.h"
#include "modules/Gui.h"
#include "modules/Job.h"
#include "LuaTools.h"
#include "uicommon.h"
#include "buildingplan.h"
static const std::string planned_building_persistence_key_v1 = "buildingplan/constraints";
static const std::string planned_building_persistence_key_v2 = "buildingplan/constraints2";
static const std::string global_settings_persistence_key = "buildingplan/global";
/*
* ItemFilter
*/
ItemFilter::ItemFilter()
{
clear();
}
void ItemFilter::clear()
{
min_quality = df::item_quality::Ordinary;
max_quality = df::item_quality::Masterful;
decorated_only = false;
clearMaterialMask();
materials.clear();
}
bool ItemFilter::deserialize(std::string ser)
{
clear();
std::vector<std::string> tokens;
split_string(&tokens, ser, "/");
if (tokens.size() != 5)
{
debug("invalid ItemFilter serialization: '%s'", ser.c_str());
return false;
}
if (!deserializeMaterialMask(tokens[0]) || !deserializeMaterials(tokens[1]))
return false;
setMinQuality(atoi(tokens[2].c_str()));
setMaxQuality(atoi(tokens[3].c_str()));
decorated_only = static_cast<bool>(atoi(tokens[4].c_str()));
return true;
}
bool ItemFilter::deserializeMaterialMask(std::string ser)
{
if (ser.empty())
return true;
if (!parseJobMaterialCategory(&mat_mask, ser))
{
debug("invalid job material category serialization: '%s'", ser.c_str());
return false;
}
return true;
}
bool ItemFilter::deserializeMaterials(std::string ser)
{
if (ser.empty())
return true;
std::vector<std::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("invalid material name serialization: '%s'", ser.c_str());
return false;
}
materials.push_back(material);
}
return true;
}
// format: mat,mask,elements/materials,list/minq/maxq/decorated
std::string ItemFilter::serialize() const
{
std::ostringstream ser;
ser << bitfield_to_string(mat_mask, ",") << "/";
if (!materials.empty())
{
ser << materials[0].getToken();
for (size_t i = 1; i < materials.size(); ++i)
ser << "," << materials[i].getToken();
}
ser << "/" << static_cast<int>(min_quality);
ser << "/" << static_cast<int>(max_quality);
ser << "/" << static_cast<int>(decorated_only);
return ser.str();
}
void ItemFilter::clearMaterialMask()
{
mat_mask.whole = 0;
}
void ItemFilter::addMaterialMask(uint32_t mask)
{
mat_mask.whole |= mask;
}
void ItemFilter::setMaterials(std::vector<DFHack::MaterialInfo> materials)
{
this->materials = materials;
}
static void clampItemQuality(df::item_quality *quality)
{
if (*quality > item_quality::Artifact)
{
debug("clamping quality to Artifact");
*quality = item_quality::Artifact;
}
if (*quality < item_quality::Ordinary)
{
debug("clamping quality to Ordinary");
*quality = item_quality::Ordinary;
}
}
void ItemFilter::setMinQuality(int quality)
{
min_quality = static_cast<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::incMinQuality() { setMinQuality(min_quality + 1); }
void ItemFilter::decMinQuality() { setMinQuality(min_quality - 1); }
void ItemFilter::incMaxQuality() { setMaxQuality(max_quality + 1); }
void ItemFilter::decMaxQuality() { setMaxQuality(max_quality - 1); }
void ItemFilter::toggleDecoratedOnly() { decorated_only = !decorated_only; }
static std::string material_to_string_fn(const MaterialInfo &m) { return m.toString(); }
uint32_t ItemFilter::getMaterialMask() const { return mat_mask.whole; }
std::vector<std::string> ItemFilter::getMaterials() const
{
std::vector<std::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;
}
std::string ItemFilter::getMinQuality() const
{
return ENUM_KEY_STR(item_quality, min_quality);
}
std::string ItemFilter::getMaxQuality() const
{
return ENUM_KEY_STR(item_quality, max_quality);
}
bool ItemFilter::getDecoratedOnly() const
{
return decorated_only;
}
bool ItemFilter::matchesMask(DFHack::MaterialInfo &mat) const
{
return mat_mask.whole ? mat.matches(mat_mask) : true;
}
bool ItemFilter::matches(df::dfhack_material_category mask) const
{
return mask.whole & mat_mask.whole;
}
bool ItemFilter::matches(DFHack::MaterialInfo &material) const
{
for (auto it = materials.begin(); it != materials.end(); ++it)
if (material.matches(*it))
return true;
return false;
}
bool ItemFilter::matches(df::item *item) const
{
if (item->getQuality() < min_quality || item->getQuality() > max_quality)
return false;
if (decorated_only && !item->hasImprovements())
return false;
auto imattype = item->getActualMaterial();
auto imatindex = item->getActualMaterialIndex();
auto item_mat = DFHack::MaterialInfo(imattype, imatindex);
return (materials.size() == 0) ? matchesMask(item_mat) : matches(item_mat);
}
/*
* PlannedBuilding
*/
// format: itemfilterser|itemfilterser|...
static std::string serializeFilters(const std::vector<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)
{
std::vector<std::string> isers;
split_string(&isers, ser, "|");
std::vector<ItemFilter> ret;
for (auto & iser : isers)
{
ItemFilter filter;
if (filter.deserialize(iser))
ret.push_back(filter);
}
return ret;
}
static size_t getNumFilters(BuildingTypeKey key)
{
auto L = Lua::Core::State;
color_ostream_proxy out(Core::getInstance().getConsole());
Lua::StackUnwinder top(L);
if (!lua_checkstack(L, 4) || !Lua::PushModulePublic(
out, L, "plugins.buildingplan", "get_num_filters"))
{
debug("failed to push the lua method on the stack");
return 0;
}
Lua::Push(L, std::get<0>(key));
Lua::Push(L, std::get<1>(key));
Lua::Push(L, std::get<2>(key));
if (!Lua::SafeCall(out, L, 3, 1))
{
debug("lua call failed");
return 0;
}
int num_filters = lua_tonumber(L, -1);
lua_pop(L, 1);
return num_filters;
}
PlannedBuilding::PlannedBuilding(df::building *building, const std::vector<ItemFilter> &filters)
: building(building),
building_id(building->id),
filters(filters)
{
config = DFHack::World::AddPersistentData(planned_building_persistence_key_v2);
config.ival(0) = building_id;
config.val() = serializeFilters(filters);
}
PlannedBuilding::PlannedBuilding(PersistentDataItem &config)
: config(config),
building(df::building::find(config.ival(0))),
building_id(config.ival(0)),
filters(deserializeFilters(config.val()))
{
if (building)
{
if (filters.size() !=
getNumFilters(toBuildingTypeKey(building)))
{
debug("invalid ItemFilter vector serialization: '%s'",
config.val().c_str());
building = NULL;
}
}
}
// Ensure the building still exists and is in a valid state. It can disappear
// for lots of reasons, such as running the game with the buildingplan plugin
// disabled, manually removing the building, modifying it via the API, etc.
bool PlannedBuilding::isValid() const
{
return building && df::building::find(building_id)
&& building->getBuildStage() == 0;
}
void PlannedBuilding::remove()
{
DFHack::World::DeletePersistentData(config);
building = NULL;
}
df::building * PlannedBuilding::getBuilding()
{
return building;
}
const std::vector<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;
}
/*
* BuildingTypeKey
*/
BuildingTypeKey toBuildingTypeKey(
df::building_type btype, int16_t subtype, int32_t custom)
{
return std::make_tuple(btype, subtype, custom);
}
BuildingTypeKey toBuildingTypeKey(df::building *bld)
{
return std::make_tuple(
bld->getType(), bld->getSubtype(), bld->getCustomType());
}
BuildingTypeKey toBuildingTypeKey(df::ui_build_selector *uibs)
{
return std::make_tuple(
uibs->building_type, uibs->building_subtype, uibs->custom_type);
}
// rotates a size_t value left by count bits
// assumes count is not 0 or >= size_t_bits
// replace this with std::rotl when we move to C++20
static std::size_t rotl_size_t(size_t val, uint32_t count)
{
static const int size_t_bits = CHAR_BIT * sizeof(std::size_t);
return val << count | val >> (size_t_bits - count);
}
std::size_t BuildingTypeKeyHash::operator() (const BuildingTypeKey & key) const
{
// cast first param to appease gcc-4.8, which is missing the enum
// specializations for std::hash
std::size_t h1 = std::hash<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);
}
/*
* Planner
*/
// convert v1 persistent data into v2 format
// we can remove this conversion code once v2 has been live for a while
void migrateV1ToV2()
{
std::vector<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 %s(%d) record successfully migrated",
ENUM_KEY_STR(building_type, bld->getType()).c_str(),
bld->id);
}
}
// assumes no setting has '=' or '|' characters
static std::string serialize_settings(std::map<std::string, bool> & settings)
{
std::ostringstream ser;
for (auto & entry : settings)
{
ser << entry.first << "=" << (entry.second ? "1" : "0") << "|";
}
return ser.str();
}
static void deserialize_settings(std::map<std::string, bool> & settings,
std::string ser)
{
std::vector<std::string> tokens;
split_string(&tokens, ser, "|");
for (auto token : tokens)
{
if (token.empty())
continue;
std::vector<std::string> parts;
split_string(&parts, token, "=");
if (parts.size() != 2)
{
debug("invalid serialized setting format: '%s'", token.c_str());
continue;
}
std::string key = parts[0];
if (settings.count(key) == 0)
{
debug("unknown serialized setting: '%s", key.c_str());
continue;
}
settings[key] = static_cast<bool>(atoi(parts[1].c_str()));
debug("deserialized setting: %s = %d", key.c_str(), settings[key]);
}
}
static DFHack::PersistentDataItem init_global_settings(
std::map<std::string, bool> & settings)
{
settings.clear();
settings["blocks"] = true;
settings["boulders"] = true;
settings["logs"] = true;
settings["bars"] = false;
// load persistent global settings if they exist; otherwise create them
std::vector<PersistentDataItem> items;
DFHack::World::GetPersistentData(&items, global_settings_persistence_key);
if (items.size() == 1)
{
DFHack::PersistentDataItem & config = items[0];
deserialize_settings(settings, config.val());
return config;
}
debug("initializing persistent global settings");
DFHack::PersistentDataItem config =
DFHack::World::AddPersistentData(global_settings_persistence_key);
config.val() = serialize_settings(settings);
return config;
}
const std::map<std::string, bool> & Planner::getGlobalSettings() const
{
return global_settings;
}
bool Planner::setGlobalSetting(std::string name, bool value)
{
if (global_settings.count(name) == 0)
{
debug("attempted to set invalid setting: '%s'", name.c_str());
return false;
}
debug("global setting '%s' %d -> %d",
name.c_str(), global_settings[name], value);
global_settings[name] = value;
if (config.isValid())
config.val() = serialize_settings(global_settings);
return true;
}
void Planner::reset()
{
debug("resetting Planner state");
default_item_filters.clear();
planned_buildings.clear();
tasks.clear();
config = init_global_settings(global_settings);
migrateV1ToV2();
std::vector<PersistentDataItem> items;
DFHack::World::GetPersistentData(&items, planned_building_persistence_key_v2);
debug("found data for %zu planned building(s)", items.size());
for (auto i = items.begin(); i != items.end(); i++)
{
PlannedBuilding pb(*i);
if (!pb.isValid())
{
debug("discarding invalid planned building");
pb.remove();
continue;
}
if (registerTasks(pb))
planned_buildings.insert(std::make_pair(pb.getBuilding()->id, pb));
}
}
void Planner::addPlannedBuilding(df::building *bld)
{
auto item_filters = getItemFilters(toBuildingTypeKey(bld)).get();
// not a supported type
if (item_filters.empty())
{
debug("failed to add building: unsupported type");
return;
}
// protect against multiple registrations
if (planned_buildings.count(bld->id) != 0)
{
debug("failed to add building: already registered");
return;
}
PlannedBuilding pb(bld, item_filters);
if (pb.isValid() && registerTasks(pb))
{
for (auto job : bld->jobs)
job->flags.bits.suspend = true;
planned_buildings.insert(std::make_pair(bld->id, pb));
}
else
{
pb.remove();
}
}
static std::string getBucket(const df::job_item & ji,
const std::vector<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();
}
// get a list of item vectors that we should search for matches
static std::vector<df::job_item_vector_id> getVectorIds(df::job_item *job_item,
const std::map<std::string, bool> & global_settings)
{
std::vector<df::job_item_vector_id> ret;
// if the filter already has the vector_id set to something specific, use it
if (job_item->vector_id > df::job_item_vector_id::IN_PLAY)
{
debug("using vector_id from job_item: %s",
ENUM_KEY_STR(job_item_vector_id, job_item->vector_id).c_str());
ret.push_back(job_item->vector_id);
return ret;
}
// if the filer is for building material, refer to our global settings for
// which vectors to search
if (job_item->flags2.bits.building_material)
{
if (global_settings.at("blocks"))
ret.push_back(df::job_item_vector_id::BLOCKS);
if (global_settings.at("boulders"))
ret.push_back(df::job_item_vector_id::BOULDER);
if (global_settings.at("logs"))
ret.push_back(df::job_item_vector_id::WOOD);
if (global_settings.at("bars"))
ret.push_back(df::job_item_vector_id::BAR);
}
// fall back to IN_PLAY if no other vector was appropriate
if (ret.empty())
ret.push_back(df::job_item_vector_id::IN_PLAY);
return ret;
}
bool Planner::registerTasks(PlannedBuilding & pb)
{
df::building * bld = pb.getBuilding();
if (bld->jobs.size() != 1)
{
debug("unexpected number of jobs: want 1, got %zu", bld->jobs.size());
return false;
}
auto job_items = bld->jobs[0]->job_items;
int num_job_items = job_items.size();
if (num_job_items < 1)
{
debug("unexpected number of job items: want >0, got %d", num_job_items);
return false;
}
int32_t id = bld->id;
for (int job_item_idx = 0; job_item_idx < num_job_items; ++job_item_idx)
{
auto job_item = job_items[job_item_idx];
auto bucket = getBucket(*job_item, pb.getFilters());
auto vector_ids = getVectorIds(job_item, global_settings);
// if there are multiple vector_ids, schedule duplicate tasks. after
// the correct number of items are matched, the extras will get popped
// as invalid
for (auto vector_id : vector_ids)
{
for (int item_num = 0; item_num < job_item->quantity; ++item_num)
{
tasks[vector_id][bucket].push(std::make_pair(id, job_item_idx));
debug("added task: %s/%s/%d,%d; "
"%zu vector(s), %zu filter bucket(s), %zu task(s) in bucket",
ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(),
bucket.c_str(), id, job_item_idx, tasks.size(),
tasks[vector_id].size(), tasks[vector_id][bucket].size());
}
}
}
return true;
}
PlannedBuilding * Planner::getPlannedBuilding(df::building *bld)
{
if (!bld || planned_buildings.count(bld->id) == 0)
return NULL;
return &planned_buildings.at(bld->id);
}
bool Planner::isPlannableBuilding(BuildingTypeKey key)
{
return getNumFilters(key) >= 1;
}
Planner::ItemFiltersWrapper Planner::getItemFilters(BuildingTypeKey key)
{
static std::vector<ItemFilter> empty_vector;
static const ItemFiltersWrapper empty_ret(empty_vector);
size_t nfilters = getNumFilters(key);
if (nfilters < 1)
return empty_ret;
while (default_item_filters[key].size() < nfilters)
default_item_filters[key].push_back(ItemFilter());
return ItemFiltersWrapper(default_item_filters[key]);
}
// precompute a bitmask with bad item flags
struct BadFlags
{
uint32_t whole;
BadFlags()
{
df::item_flags flags;
#define F(x) flags.bits.x = true;
F(dump); F(forbid); F(garbage_collect);
F(hostile); F(on_fire); F(rotten); F(trader);
F(in_building); F(construction); F(in_job);
F(owned); F(in_chest); F(removed); F(encased);
#undef F
whole = flags.whole;
}
};
static bool itemPassesScreen(df::item * item)
{
static BadFlags bad_flags;
return !(item->flags.whole & bad_flags.whole)
&& !item->isAssignedToStockpile()
// TODO: make this configurable
&& !(item->getType() == df::item_type::BOX && item->isBag());
}
static bool matchesFilters(df::item * item,
df::job_item * job_item,
const ItemFilter & item_filter)
{
// check the properties that are not checked by Job::isSuitableItem()
if (job_item->item_type > -1 && job_item->item_type != item->getType())
return false;
if (job_item->item_subtype > -1 &&
job_item->item_subtype != item->getSubtype())
return false;
if (job_item->flags2.bits.building_material && !item->isBuildMat())
return false;
if (job_item->metal_ore > -1 && !item->isMetalOre(job_item->metal_ore))
return false;
if (job_item->has_tool_use > df::tool_uses::NONE
&& !item->hasToolUse(job_item->has_tool_use))
return false;
return DFHack::Job::isSuitableItem(
job_item, item->getType(), item->getSubtype())
&& DFHack::Job::isSuitableMaterial(
job_item, item->getMaterial(), item->getMaterialIndex(),
item->getType())
&& item_filter.matches(item);
}
// note that this just removes the PlannedBuilding. the tasks will get dropped
// as we discover them in the tasks queues and they fail their isValid() check.
// this "lazy" task cleaning algorithm works because there is no way to
// re-register a building once it has been removed -- if it fails isValid()
// then it has either been built or desroyed. therefore there is no chance of
// duplicate tasks getting added to the tasks queues.
void Planner::unregisterBuilding(int32_t id)
{
if (planned_buildings.count(id) > 0)
{
planned_buildings.at(id).remove();
planned_buildings.erase(id);
}
}
static bool isJobReady(df::job * job)
{
int needed_items = 0;
for (auto job_item : job->job_items) { needed_items += job_item->quantity; }
if (needed_items)
{
debug("building needs %d more item(s)", needed_items);
return false;
}
return true;
}
static bool job_item_idx_lt(df::job_item_ref *a, df::job_item_ref *b)
{
// we want the items in the opposite order of the filters
return a->job_item_idx > b->job_item_idx;
}
// this function does not remove the job_items since their quantity fields are
// now all at 0, so there is no risk of having extra items attached. we don't
// remove them to keep the "finalize with buildingplan active" path as similar
// as possible to the "finalize with buildingplan disabled" path.
static void finalizeBuilding(df::building * bld)
{
debug("finalizing building %d", bld->id);
auto job = bld->jobs[0];
// sort the items so they get added to the structure in the correct order
std::sort(job->items.begin(), job->items.end(), job_item_idx_lt);
// derive the material properties of the building and job from the first
// applicable item, though if any boulders are involved, it makes the whole
// structure "rough".
bool rough = false;
for (auto attached_item : job->items)
{
df::item *item = attached_item->item;
rough = rough || item->getType() == item_type::BOULDER;
if (bld->mat_type == -1)
{
bld->mat_type = item->getMaterial();
job->mat_type = bld->mat_type;
}
if (bld->mat_index == -1)
{
bld->mat_index = item->getMaterialIndex();
job->mat_index = bld->mat_index;
}
}
if (bld->needsDesign())
{
auto act = (df::building_actual *)bld;
if (!act->design)
act->design = new df::building_design();
act->design->flags.bits.rough = rough;
}
// we're good to go!
job->flags.bits.suspend = false;
Job::checkBuildingsNow();
}
void Planner::popInvalidTasks(std::queue<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);
}
}
void Planner::doVector(df::job_item_vector_id vector_id,
std::map<std::string, std::queue<std::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("matching %zu item(s) in vector %s against %zu filter bucket(s)",
item_vector.size(),
ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(),
buckets.size());
for (auto item_it = item_vector.rbegin();
item_it != item_vector.rend();
++item_it)
{
auto item = *item_it;
if (!itemPassesScreen(item))
continue;
for (auto bucket_it = buckets.begin(); bucket_it != buckets.end();)
{
auto & task_queue = bucket_it->second;
popInvalidTasks(task_queue);
if (task_queue.empty())
{
debug("removing empty bucket: %s/%s; %zu bucket(s) left",
ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(),
bucket_it->first.c_str(),
buckets.size() - 1);
bucket_it = buckets.erase(bucket_it);
continue;
}
auto & task = task_queue.front();
auto id = task.first;
auto & pb = planned_buildings.at(id);
auto building = pb.getBuilding();
auto job = building->jobs[0];
auto filter_idx = task.second;
if (matchesFilters(item, job->job_items[filter_idx],
pb.getFilters()[filter_idx])
&& DFHack::Job::attachJobItem(job, item,
df::job_item_ref::Hauled, filter_idx))
{
MaterialInfo material;
material.decode(item);
ItemTypeInfo item_type;
item_type.decode(item);
debug("attached %s %s to filter %d for %s(%d): %s/%s",
material.toString().c_str(),
item_type.toString().c_str(),
filter_idx,
ENUM_KEY_STR(building_type, building->getType()).c_str(),
id,
ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(),
bucket_it->first.c_str());
// keep quantity aligned with the actual number of remaining
// items so if buildingplan is turned off, the building will
// be completed with the correct number of items.
--job->job_items[filter_idx]->quantity;
task_queue.pop();
if (isJobReady(job))
{
finalizeBuilding(building);
unregisterBuilding(id);
}
if (task_queue.empty())
{
debug(
"removing empty item bucket: %s/%s; %zu left",
ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(),
bucket_it->first.c_str(),
buckets.size() - 1);
buckets.erase(bucket_it);
}
// we found a home for this item; no need to look further
break;
}
++bucket_it;
}
if (buckets.empty())
break;
}
}
struct VectorsToScanLast
{
std::vector<df::job_item_vector_id> vectors;
VectorsToScanLast()
{
// order is important here. we want to match boulders before wood and
// everything before bars. blocks are not listed here since we'll have
// already scanned them when we did the first pass through the buckets.
vectors.push_back(df::job_item_vector_id::BOULDER);
vectors.push_back(df::job_item_vector_id::WOOD);
vectors.push_back(df::job_item_vector_id::BAR);
}
};
void Planner::doCycle()
{
debug("running cycle for %zu registered building(s)",
planned_buildings.size());
static const VectorsToScanLast vectors_to_scan_last;
for (auto it = tasks.begin(); it != tasks.end();)
{
auto vector_id = it->first;
// we could make this a set, but it's only three elements
if (std::find(vectors_to_scan_last.vectors.begin(),
vectors_to_scan_last.vectors.end(),
vector_id) != vectors_to_scan_last.vectors.end())
{
++it;
continue;
}
auto & buckets = it->second;
doVector(vector_id, buckets);
if (buckets.empty())
{
debug("removing empty vector: %s; %zu vector(s) left",
ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(),
tasks.size() - 1);
it = tasks.erase(it);
}
else
++it;
}
for (auto vector_id : vectors_to_scan_last.vectors)
{
if (tasks.count(vector_id) == 0)
continue;
auto & buckets = tasks[vector_id];
doVector(vector_id, buckets);
if (buckets.empty())
{
debug("removing empty vector: %s; %zu vector(s) left",
ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(),
tasks.size() - 1);
tasks.erase(vector_id);
}
}
debug("cycle done; %zu registered building(s) left",
planned_buildings.size());
}
Planner planner;