#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-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"; 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;