From 100b374af73659b2f00a9c26697f3b17fd1a3a72 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 16 Oct 2020 14:08:52 -0700 Subject: [PATCH 1/2] 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 --- plugins/buildingplan-planner.cpp | 714 ++++++++++++++++++++++--------- plugins/buildingplan-planner.h | 30 +- plugins/buildingplan.cpp | 163 ++++++- plugins/fortplan.cpp | 2 - plugins/lua/buildingplan.lua | 47 ++ 5 files changed, 719 insertions(+), 237 deletions(-) diff --git a/plugins/buildingplan-planner.cpp b/plugins/buildingplan-planner.cpp index 925d0f4c3..dcdcfb777 100644 --- a/plugins/buildingplan-planner.cpp +++ b/plugins/buildingplan-planner.cpp @@ -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 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(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(decorated_only) + 1; + ser << "/" << static_cast(min_quality); + ser << "/" << static_cast(max_quality); + ser << "/" << static_cast(decorated_only); + return ser.str(); } void ItemFilter::clearMaterialMask() @@ -230,20 +233,59 @@ bool ItemFilter::matches(df::item *item) const * PlannedBuilding */ -static std::vector deserializeFilters(PersistentDataItem &config) +// format: itemfilterser|itemfilterser|... +static std::string serializeFilters(const std::vector &filters) { - // simplified implementation while we can assume there is only one filter + 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 deserializeFilters(std::string ser) +{ + std::vector isers; + split_string(&isers, ser, "|"); std::vector 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 &filters) @@ -251,92 +293,27 @@ PlannedBuilding::PlannedBuilding(df::building *building, const std::vectorid), 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 *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++) - { - 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(); - if (!ref) - { - Core::printerr("Could not allocate general_ref_building_holderst\n"); - return false; - } - - 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()) + if (building) { - auto act = (df::building_actual *) building; - act->design = new df::building_design(); - act->design->flags.bits.rough = rough; + if (filters.size() != + getNumFilters(toBuildingTypeKey(building))) + { + debug("invalid ItemFilter vector serialization: '%s'", + config.val().c_str()); + building = NULL; + } } - - 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 & 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(); \ - 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 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 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 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 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 & 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) { - if (pb.getBuilding() == bld) - return &pb; + debug("unexpected number of job items: want >0, got %d", num_job_items); + return false; } - return NULL; + 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) + { + 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();) + BadFlags() { - if (building_iter->isValid()) - { - 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; + } +}; - 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)) - { - debug("Unable to allocate an item"); - ++building_iter; - continue; - } - } - debug("Removing building plan"); - building_iter->remove(); - building_iter = planned_buildings.erase(building_iter); +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; + + 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) + { + 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 &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 = items[i]; - - if (item->flags.whole & bad_flags.whole) - continue; - - df::item_type itype = item->getType(); - if (!is_relevant_item_type[itype]) - continue; + 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 (itype == df::item_type::BOX && item->isBag()) - continue; //Skip bags + if (bld->needsDesign()) + { + auto act = (df::building_actual *)bld; + if (!act->design) + act->design = new df::building_design(); + act->design->flags.bits.rough = rough; + } - if (item->flags.bits.artifact) - continue; + // we're good to go! + job->flags.bits.suspend = false; + Job::checkBuildingsNow(); +} - if (item->flags.bits.in_job || - item->isAssignedToStockpile() || - item->flags.bits.owned || - item->flags.bits.in_chest) +void Planner::popInvalidTasks(std::queue> & task_queue) +{ + while (!task_queue.empty()) + { + auto & task = task_queue.front(); + auto id = task.first; + if (planned_buildings.count(id) > 0) { - continue; + 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); + } +} - available_item_vectors[itype].push_back(item); +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; + 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; + } + 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; diff --git a/plugins/buildingplan-planner.h b/plugins/buildingplan-planner.h index cda217d81..22d0487c3 100644 --- a/plugins/buildingplan-planner.h +++ b/plugins/buildingplan-planner.h @@ -1,5 +1,6 @@ #pragma once +#include #include #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 materials; df::item_quality min_quality; @@ -59,9 +63,6 @@ public: PlannedBuilding(df::building *building, const std::vector &filters); PlannedBuilding(DFHack::PersistentDataItem &config); - bool assignClosestItem(std::vector *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 filters; + const df::building::key_field_type building_id; + const std::vector filters; }; // building type, subtype, custom @@ -103,7 +104,6 @@ public: std::vector &item_filters; }; - void initialize(); void reset(); void addPlannedBuilding(df::building *bld); @@ -117,15 +117,17 @@ public: void doCycle(); private: - std::map item_for_building_type; std::unordered_map, BuildingTypeKeyHash> default_item_filters; - std::map> available_item_vectors; - std::map is_relevant_item_type; //Needed for fast check when looping over all items - std::vector planned_buildings; - - void gather_available_items(); + // building id -> PlannedBuilding + std::unordered_map planned_buildings; + // vector id -> filter bucket -> queue of (building id, job_item index) + std::map>>> tasks; + + bool registerTasks(PlannedBuilding &plannedBuilding); + void unregisterBuilding(int32_t id); + void popInvalidTasks(std::queue> &task_queue); }; extern Planner planner; diff --git a/plugins/buildingplan.cpp b/plugins/buildingplan.cpp index 703e567b7..58c911dcf 100644 --- a/plugins/buildingplan.cpp +++ b/plugins/buildingplan.cpp @@ -1,5 +1,3 @@ -#include - #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 *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; } - 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 *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::reverse_iterator filter_rbegin; + static std::vector::reverse_iterator filter_rend; + static std::vector::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 *input) { if (!isInPlannedBuildingPlacementMode()) @@ -402,6 +504,8 @@ struct buildingplan_place_hook : public df::viewscreen_dwarfmodest show_help = false; return false; } + + initStatics(); if (in_dummy_screen) { @@ -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(*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::reverse_iterator buildingplan_place_hook::filter_rbegin; +std::vector::reverse_iterator buildingplan_place_hook::filter_rend; +std::vector::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 = #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 From 1a69a9b4833ad2ec6259d582376bf7fadeed5ec5 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 26 Oct 2020 16:54:50 -0700 Subject: [PATCH 2/2] add more important checks for matching items stolen (with love) from advfort.lua --- plugins/buildingplan-planner.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugins/buildingplan-planner.cpp b/plugins/buildingplan-planner.cpp index dcdcfb777..9d73ab4c4 100644 --- a/plugins/buildingplan-planner.cpp +++ b/plugins/buildingplan-planner.cpp @@ -704,6 +704,7 @@ 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; @@ -711,6 +712,12 @@ static bool matchesFilters(df::item * item, 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;