|
|
|
@ -12,12 +12,14 @@
|
|
|
|
|
#include "modules/Gui.h"
|
|
|
|
|
#include "modules/Job.h"
|
|
|
|
|
|
|
|
|
|
#include "LuaTools.h"
|
|
|
|
|
#include "uicommon.h"
|
|
|
|
|
|
|
|
|
|
#include "buildingplan-planner.h"
|
|
|
|
|
#include "buildingplan-lib.h"
|
|
|
|
|
|
|
|
|
|
static const std::string planned_building_persistence_key_v1 = "buildingplan/constraints";
|
|
|
|
|
static const std::string planned_building_persistence_key_v2 = "buildingplan/constraints2";
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* ItemFilter
|
|
|
|
@ -37,24 +39,24 @@ void ItemFilter::clear()
|
|
|
|
|
materials.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool ItemFilter::deserialize(PersistentDataItem &config)
|
|
|
|
|
bool ItemFilter::deserialize(std::string ser)
|
|
|
|
|
{
|
|
|
|
|
clear();
|
|
|
|
|
|
|
|
|
|
std::vector<std::string> tokens;
|
|
|
|
|
split_string(&tokens, config.val(), "/");
|
|
|
|
|
if (tokens.size() != 2)
|
|
|
|
|
split_string(&tokens, ser, "/");
|
|
|
|
|
if (tokens.size() != 5)
|
|
|
|
|
{
|
|
|
|
|
debug("invalid ItemFilter serialization: '%s'", config.val().c_str());
|
|
|
|
|
debug("invalid ItemFilter serialization: '%s'", ser.c_str());
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!deserializeMaterialMask(tokens[0]) || !deserializeMaterials(tokens[1]))
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
setMinQuality(config.ival(2) - 1);
|
|
|
|
|
setMaxQuality(config.ival(4) - 1);
|
|
|
|
|
decorated_only = config.ival(3) - 1;
|
|
|
|
|
setMinQuality(atoi(tokens[2].c_str()));
|
|
|
|
|
setMaxQuality(atoi(tokens[3].c_str()));
|
|
|
|
|
decorated_only = static_cast<bool>(atoi(tokens[4].c_str()));
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -91,7 +93,8 @@ bool ItemFilter::deserializeMaterials(std::string ser)
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ItemFilter::serialize(PersistentDataItem &config) const
|
|
|
|
|
// format: mat,mask,elements/materials,list/minq/maxq/decorated
|
|
|
|
|
std::string ItemFilter::serialize() const
|
|
|
|
|
{
|
|
|
|
|
std::ostringstream ser;
|
|
|
|
|
ser << bitfield_to_string(mat_mask, ",") << "/";
|
|
|
|
@ -101,10 +104,10 @@ void ItemFilter::serialize(PersistentDataItem &config) const
|
|
|
|
|
for (size_t i = 1; i < materials.size(); ++i)
|
|
|
|
|
ser << "," << materials[i].getToken();
|
|
|
|
|
}
|
|
|
|
|
config.val() = ser.str();
|
|
|
|
|
config.ival(2) = min_quality + 1;
|
|
|
|
|
config.ival(4) = max_quality + 1;
|
|
|
|
|
config.ival(3) = static_cast<int>(decorated_only) + 1;
|
|
|
|
|
ser << "/" << static_cast<int>(min_quality);
|
|
|
|
|
ser << "/" << static_cast<int>(max_quality);
|
|
|
|
|
ser << "/" << static_cast<int>(decorated_only);
|
|
|
|
|
return ser.str();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ItemFilter::clearMaterialMask()
|
|
|
|
@ -230,113 +233,87 @@ bool ItemFilter::matches(df::item *item) const
|
|
|
|
|
* PlannedBuilding
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
static std::vector<ItemFilter> deserializeFilters(PersistentDataItem &config)
|
|
|
|
|
// format: itemfilterser|itemfilterser|...
|
|
|
|
|
static std::string serializeFilters(const std::vector<ItemFilter> &filters)
|
|
|
|
|
{
|
|
|
|
|
std::ostringstream ser;
|
|
|
|
|
if (!filters.empty())
|
|
|
|
|
{
|
|
|
|
|
// simplified implementation while we can assume there is only one filter
|
|
|
|
|
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;
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
PlannedBuilding::PlannedBuilding(df::building *building, const std::vector<ItemFilter> &filters)
|
|
|
|
|
: building(building),
|
|
|
|
|
building_id(building->id),
|
|
|
|
|
filters(filters)
|
|
|
|
|
if (!lua_checkstack(L, 4) || !Lua::PushModulePublic(
|
|
|
|
|
out, L, "plugins.buildingplan", "get_num_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);
|
|
|
|
|
debug("failed to push the lua method on the stack");
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PlannedBuilding::PlannedBuilding(PersistentDataItem &config)
|
|
|
|
|
: config(config),
|
|
|
|
|
building(df::building::find(config.ival(1))),
|
|
|
|
|
building_id(config.ival(1)),
|
|
|
|
|
filters(deserializeFilters(config))
|
|
|
|
|
{ }
|
|
|
|
|
Lua::Push(L, std::get<0>(key));
|
|
|
|
|
Lua::Push(L, std::get<1>(key));
|
|
|
|
|
Lua::Push(L, std::get<2>(key));
|
|
|
|
|
|
|
|
|
|
bool PlannedBuilding::assignClosestItem(std::vector<df::item *> *items_vector)
|
|
|
|
|
if (!Lua::SafeCall(out, L, 3, 1))
|
|
|
|
|
{
|
|
|
|
|
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;
|
|
|
|
|
debug("lua call failed");
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (closest_distance > -1 && assignItem(*closest_item))
|
|
|
|
|
{
|
|
|
|
|
debug("Item assigned");
|
|
|
|
|
items_vector->erase(closest_item);
|
|
|
|
|
remove();
|
|
|
|
|
return true;
|
|
|
|
|
int num_filters = lua_tonumber(L, -1);
|
|
|
|
|
lua_pop(L, 1);
|
|
|
|
|
return num_filters;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void delete_item_fn(df::job_item *x) { delete x; }
|
|
|
|
|
|
|
|
|
|
bool PlannedBuilding::assignItem(df::item *item)
|
|
|
|
|
PlannedBuilding::PlannedBuilding(PersistentDataItem &config)
|
|
|
|
|
: config(config),
|
|
|
|
|
building(df::building::find(config.ival(0))),
|
|
|
|
|
building_id(config.ival(0)),
|
|
|
|
|
filters(deserializeFilters(config.val()))
|
|
|
|
|
{
|
|
|
|
|
auto ref = df::allocate<df::general_ref_building_holderst>();
|
|
|
|
|
if (!ref)
|
|
|
|
|
if (building)
|
|
|
|
|
{
|
|
|
|
|
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 (filters.size() !=
|
|
|
|
|
getNumFilters(toBuildingTypeKey(building)))
|
|
|
|
|
{
|
|
|
|
|
auto act = (df::building_actual *) building;
|
|
|
|
|
act->design = new df::building_design();
|
|
|
|
|
act->design->flags.bits.rough = rough;
|
|
|
|
|
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<ItemFilter> & PlannedBuilding::getFilters() const
|
|
|
|
|
{
|
|
|
|
|
// if we want to be able to dynamically change the filters, we'll need to
|
|
|
|
|
// re-bucket the tasks in Planner.
|
|
|
|
|
return filters;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -412,45 +391,112 @@ std::size_t BuildingTypeKeyHash::operator() (const BuildingTypeKey & key) const
|
|
|
|
|
* Planner
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
void Planner::initialize()
|
|
|
|
|
{
|
|
|
|
|
#define add_building_type(btype, itype) \
|
|
|
|
|
item_for_building_type[df::building_type::btype] = df::item_type::itype; \
|
|
|
|
|
available_item_vectors[df::item_type::itype] = std::vector<df::item *>(); \
|
|
|
|
|
is_relevant_item_type[df::item_type::itype] = true; \
|
|
|
|
|
|
|
|
|
|
FOR_ENUM_ITEMS(item_type, it)
|
|
|
|
|
is_relevant_item_type[it] = false;
|
|
|
|
|
|
|
|
|
|
add_building_type(Armorstand, ARMORSTAND);
|
|
|
|
|
add_building_type(Bed, BED);
|
|
|
|
|
add_building_type(Chair, CHAIR);
|
|
|
|
|
add_building_type(Coffin, COFFIN);
|
|
|
|
|
add_building_type(Door, DOOR);
|
|
|
|
|
add_building_type(Floodgate, FLOODGATE);
|
|
|
|
|
add_building_type(Hatch, HATCH_COVER);
|
|
|
|
|
add_building_type(GrateWall, GRATE);
|
|
|
|
|
add_building_type(GrateFloor, GRATE);
|
|
|
|
|
add_building_type(BarsVertical, BAR);
|
|
|
|
|
add_building_type(BarsFloor, BAR);
|
|
|
|
|
add_building_type(Cabinet, CABINET);
|
|
|
|
|
add_building_type(Box, BOX);
|
|
|
|
|
// skip kennels, farm plot
|
|
|
|
|
add_building_type(Weaponrack, WEAPONRACK);
|
|
|
|
|
add_building_type(Statue, STATUE);
|
|
|
|
|
add_building_type(Slab, SLAB);
|
|
|
|
|
add_building_type(Table, TABLE);
|
|
|
|
|
// skip roads ... furnaces
|
|
|
|
|
add_building_type(WindowGlass, WINDOW);
|
|
|
|
|
// skip gem window ... support
|
|
|
|
|
add_building_type(AnimalTrap, ANIMALTRAP);
|
|
|
|
|
add_building_type(Chain, CHAIN);
|
|
|
|
|
add_building_type(Cage, CAGE);
|
|
|
|
|
// skip archery target
|
|
|
|
|
add_building_type(TractionBench, TRACTION_BENCH);
|
|
|
|
|
// skip nest box, hive (tools)
|
|
|
|
|
|
|
|
|
|
#undef add_building_type
|
|
|
|
|
// convert v1 persistent data into v2 format
|
|
|
|
|
// we can remove this conversion code once v2 has been live for a while
|
|
|
|
|
void migrateV1ToV2()
|
|
|
|
|
{
|
|
|
|
|
std::vector<PersistentDataItem> configs;
|
|
|
|
|
DFHack::World::GetPersistentData(&configs, planned_building_persistence_key_v1);
|
|
|
|
|
if (configs.empty())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
debug("migrating %zu persisted configs to new format", configs.size());
|
|
|
|
|
for (auto config : configs)
|
|
|
|
|
{
|
|
|
|
|
df::building *bld = df::building::find(config.ival(1));
|
|
|
|
|
if (!bld)
|
|
|
|
|
{
|
|
|
|
|
debug("buliding no longer exists; removing config");
|
|
|
|
|
DFHack::World::DeletePersistentData(config);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (bld->getBuildStage() != 0 || bld->jobs.size() != 1
|
|
|
|
|
|| bld->jobs[0]->job_items.size() != 1)
|
|
|
|
|
{
|
|
|
|
|
debug("building in invalid state; removing config");
|
|
|
|
|
DFHack::World::DeletePersistentData(config);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// fix up the building so we can set the material properties later
|
|
|
|
|
bld->mat_type = -1;
|
|
|
|
|
bld->mat_index = -1;
|
|
|
|
|
|
|
|
|
|
// the v1 filters are not initialized correctly and will match any item.
|
|
|
|
|
// we need to fix them up a bit.
|
|
|
|
|
auto filter = bld->jobs[0]->job_items[0];
|
|
|
|
|
df::item_type type;
|
|
|
|
|
switch (bld->getType())
|
|
|
|
|
{
|
|
|
|
|
case df::building_type::Armorstand: type = df::item_type::ARMORSTAND; break;
|
|
|
|
|
case df::building_type::Bed: type = df::item_type::BED; break;
|
|
|
|
|
case df::building_type::Chair: type = df::item_type::CHAIR; break;
|
|
|
|
|
case df::building_type::Coffin: type = df::item_type::COFFIN; break;
|
|
|
|
|
case df::building_type::Door: type = df::item_type::DOOR; break;
|
|
|
|
|
case df::building_type::Floodgate: type = df::item_type::FLOODGATE; break;
|
|
|
|
|
case df::building_type::Hatch: type = df::item_type::HATCH_COVER; break;
|
|
|
|
|
case df::building_type::GrateWall: type = df::item_type::GRATE; break;
|
|
|
|
|
case df::building_type::GrateFloor: type = df::item_type::GRATE; break;
|
|
|
|
|
case df::building_type::BarsVertical: type = df::item_type::BAR; break;
|
|
|
|
|
case df::building_type::BarsFloor: type = df::item_type::BAR; break;
|
|
|
|
|
case df::building_type::Cabinet: type = df::item_type::CABINET; break;
|
|
|
|
|
case df::building_type::Box: type = df::item_type::BOX; break;
|
|
|
|
|
case df::building_type::Weaponrack: type = df::item_type::WEAPONRACK; break;
|
|
|
|
|
case df::building_type::Statue: type = df::item_type::STATUE; break;
|
|
|
|
|
case df::building_type::Slab: type = df::item_type::SLAB; break;
|
|
|
|
|
case df::building_type::Table: type = df::item_type::TABLE; break;
|
|
|
|
|
case df::building_type::WindowGlass: type = df::item_type::WINDOW; break;
|
|
|
|
|
case df::building_type::AnimalTrap: type = df::item_type::ANIMALTRAP; break;
|
|
|
|
|
case df::building_type::Chain: type = df::item_type::CHAIN; break;
|
|
|
|
|
case df::building_type::Cage: type = df::item_type::CAGE; break;
|
|
|
|
|
case df::building_type::TractionBench: type = df::item_type::TRACTION_BENCH; break;
|
|
|
|
|
default:
|
|
|
|
|
debug("building has unhandled type; removing config");
|
|
|
|
|
DFHack::World::DeletePersistentData(config);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
filter->item_type = type;
|
|
|
|
|
filter->item_subtype = -1;
|
|
|
|
|
filter->mat_type = -1;
|
|
|
|
|
filter->mat_index = -1;
|
|
|
|
|
filter->flags1.whole = 0;
|
|
|
|
|
filter->flags2.whole = 0;
|
|
|
|
|
filter->flags2.bits.allow_artifact = true;
|
|
|
|
|
filter->flags3.whole = 0;
|
|
|
|
|
filter->flags4 = 0;
|
|
|
|
|
filter->flags5 = 0;
|
|
|
|
|
filter->metal_ore = -1;
|
|
|
|
|
filter->min_dimension = -1;
|
|
|
|
|
filter->has_tool_use = df::tool_uses::NONE;
|
|
|
|
|
filter->quantity = 1;
|
|
|
|
|
|
|
|
|
|
std::vector<std::string> tokens;
|
|
|
|
|
split_string(&tokens, config.val(), "/");
|
|
|
|
|
if (tokens.size() != 2)
|
|
|
|
|
{
|
|
|
|
|
debug("invalid v1 format; removing config");
|
|
|
|
|
DFHack::World::DeletePersistentData(config);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ItemFilter item_filter;
|
|
|
|
|
item_filter.deserializeMaterialMask(tokens[0]);
|
|
|
|
|
item_filter.deserializeMaterials(tokens[1]);
|
|
|
|
|
item_filter.setMinQuality(config.ival(2) - 1);
|
|
|
|
|
item_filter.setMaxQuality(config.ival(4) - 1);
|
|
|
|
|
if (config.ival(3) - 1)
|
|
|
|
|
item_filter.toggleDecoratedOnly();
|
|
|
|
|
|
|
|
|
|
// create the v2 record
|
|
|
|
|
std::vector<ItemFilter> item_filters;
|
|
|
|
|
item_filters.push_back(item_filter);
|
|
|
|
|
PlannedBuilding pb(bld, item_filters);
|
|
|
|
|
|
|
|
|
|
// remove the v1 record
|
|
|
|
|
DFHack::World::DeletePersistentData(config);
|
|
|
|
|
debug("v1 record successfully migrated");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Planner::reset()
|
|
|
|
@ -458,9 +504,12 @@ void Planner::reset()
|
|
|
|
|
debug("resetting Planner state");
|
|
|
|
|
default_item_filters.clear();
|
|
|
|
|
planned_buildings.clear();
|
|
|
|
|
tasks.clear();
|
|
|
|
|
|
|
|
|
|
migrateV1ToV2();
|
|
|
|
|
|
|
|
|
|
std::vector<PersistentDataItem> items;
|
|
|
|
|
DFHack::World::GetPersistentData(&items, planned_building_persistence_key_v1);
|
|
|
|
|
DFHack::World::GetPersistentData(&items, planned_building_persistence_key_v2);
|
|
|
|
|
debug("found data for %zu planned buildings", items.size());
|
|
|
|
|
|
|
|
|
|
for (auto i = items.begin(); i != items.end(); i++)
|
|
|
|
@ -472,7 +521,8 @@ void Planner::reset()
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
planned_buildings.push_back(pb);
|
|
|
|
|
if (registerTasks(pb))
|
|
|
|
|
planned_buildings.insert(std::make_pair(pb.getBuilding()->id, pb));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -487,19 +537,19 @@ void Planner::addPlannedBuilding(df::building *bld)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// protect against multiple registrations
|
|
|
|
|
if (getPlannedBuilding(bld))
|
|
|
|
|
if (planned_buildings.count(bld->id) != 0)
|
|
|
|
|
{
|
|
|
|
|
debug("building already registered");
|
|
|
|
|
debug("failed to add building: already registered");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PlannedBuilding pb(bld, item_filters);
|
|
|
|
|
if (pb.isValid())
|
|
|
|
|
if (pb.isValid() && registerTasks(pb))
|
|
|
|
|
{
|
|
|
|
|
for (auto job : bld->jobs)
|
|
|
|
|
job->flags.bits.suspend = true;
|
|
|
|
|
|
|
|
|
|
planned_buildings.push_back(pb);
|
|
|
|
|
planned_buildings.insert(std::make_pair(bld->id, pb));
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
@ -507,19 +557,107 @@ void Planner::addPlannedBuilding(df::building *bld)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PlannedBuilding * Planner::getPlannedBuilding(df::building *bld)
|
|
|
|
|
static std::string getBucket(const df::job_item & ji,
|
|
|
|
|
const std::vector<ItemFilter> & item_filters)
|
|
|
|
|
{
|
|
|
|
|
std::ostringstream ser;
|
|
|
|
|
|
|
|
|
|
// pull out and serialize only known relevant fields. if we miss a few, then
|
|
|
|
|
// the filter bucket will be slighly less specific than it could be, but
|
|
|
|
|
// that's probably ok. we'll just end up bucketing slightly different items
|
|
|
|
|
// together. this is only a problem if the different filter at the front of
|
|
|
|
|
// the queue doesn't match any available items and blocks filters behind it
|
|
|
|
|
// that could be matched.
|
|
|
|
|
ser << ji.item_type << ':' << ji.item_subtype << ':' << ji.mat_type << ':'
|
|
|
|
|
<< ji.mat_index << ':' << ji.flags1.whole << ':' << ji.flags2.whole
|
|
|
|
|
<< ':' << ji.flags3.whole << ':' << ji.flags4 << ':' << ji.flags5 << ':'
|
|
|
|
|
<< ji.metal_ore << ':' << ji.has_tool_use;
|
|
|
|
|
|
|
|
|
|
for (auto & item_filter : item_filters)
|
|
|
|
|
{
|
|
|
|
|
ser << ':' << item_filter.serialize();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ser.str();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool Planner::registerTasks(PlannedBuilding & pb)
|
|
|
|
|
{
|
|
|
|
|
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)
|
|
|
|
|
{
|
|
|
|
|
for (auto & pb : planned_buildings)
|
|
|
|
|
debug("unexpected number of job items: want >0, got %d", num_job_items);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
for (int job_item_idx = 0; job_item_idx < num_job_items; ++job_item_idx)
|
|
|
|
|
{
|
|
|
|
|
auto vector_id = df::job_item_vector_id::IN_PLAY;
|
|
|
|
|
auto job_item = job_items[job_item_idx];
|
|
|
|
|
if (job_item->vector_id)
|
|
|
|
|
vector_id = job_item->vector_id;
|
|
|
|
|
auto bucket = getBucket(*job_item, pb.getFilters());
|
|
|
|
|
for (int item_num = 0; item_num < job_item->quantity; ++item_num)
|
|
|
|
|
{
|
|
|
|
|
if (pb.getBuilding() == bld)
|
|
|
|
|
return &pb;
|
|
|
|
|
int32_t id = bld->id;
|
|
|
|
|
tasks[vector_id][bucket].push(std::make_pair(id, job_item_idx));
|
|
|
|
|
debug("added task: %s/%s/%d,%d; "
|
|
|
|
|
"%zu vectors, %zu buckets, %zu tasks in bucket",
|
|
|
|
|
ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(),
|
|
|
|
|
bucket.c_str(), id, job_item_idx, tasks.size(),
|
|
|
|
|
tasks[vector_id].size(), tasks[vector_id][bucket].size());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PlannedBuilding * Planner::getPlannedBuilding(df::building *bld)
|
|
|
|
|
{
|
|
|
|
|
if (!bld || planned_buildings.count(bld->id) == 0)
|
|
|
|
|
return NULL;
|
|
|
|
|
return &planned_buildings.at(bld->id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool Planner::isPlannableBuilding(BuildingTypeKey key)
|
|
|
|
|
{
|
|
|
|
|
return item_for_building_type.count(std::get<0>(key)) > 0;
|
|
|
|
|
if (getNumFilters(key) == 0)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
// restrict supported types to be the same as the previous implementation
|
|
|
|
|
switch(std::get<0>(key))
|
|
|
|
|
{
|
|
|
|
|
case df::enums::building_type::Armorstand:
|
|
|
|
|
case df::enums::building_type::Bed:
|
|
|
|
|
case df::enums::building_type::Chair:
|
|
|
|
|
case df::enums::building_type::Coffin:
|
|
|
|
|
case df::enums::building_type::Door:
|
|
|
|
|
case df::enums::building_type::Floodgate:
|
|
|
|
|
case df::enums::building_type::Hatch:
|
|
|
|
|
case df::enums::building_type::GrateWall:
|
|
|
|
|
case df::enums::building_type::GrateFloor:
|
|
|
|
|
case df::enums::building_type::BarsVertical:
|
|
|
|
|
case df::enums::building_type::BarsFloor:
|
|
|
|
|
case df::enums::building_type::Cabinet:
|
|
|
|
|
case df::enums::building_type::Box:
|
|
|
|
|
case df::enums::building_type::Weaponrack:
|
|
|
|
|
case df::enums::building_type::Statue:
|
|
|
|
|
case df::enums::building_type::Slab:
|
|
|
|
|
case df::enums::building_type::Table:
|
|
|
|
|
case df::enums::building_type::WindowGlass:
|
|
|
|
|
case df::enums::building_type::AnimalTrap:
|
|
|
|
|
case df::enums::building_type::Chain:
|
|
|
|
|
case df::enums::building_type::Cage:
|
|
|
|
|
case df::enums::building_type::TractionBench:
|
|
|
|
|
return true;
|
|
|
|
|
default:
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Planner::ItemFiltersWrapper Planner::getItemFilters(BuildingTypeKey key)
|
|
|
|
@ -535,84 +673,257 @@ 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))
|
|
|
|
|
static bool itemPassesScreen(df::item * item)
|
|
|
|
|
{
|
|
|
|
|
debug("Unable to allocate an item");
|
|
|
|
|
++building_iter;
|
|
|
|
|
continue;
|
|
|
|
|
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_filter.matches(item);
|
|
|
|
|
}
|
|
|
|
|
debug("Removing building plan");
|
|
|
|
|
building_iter->remove();
|
|
|
|
|
building_iter = planned_buildings.erase(building_iter);
|
|
|
|
|
|
|
|
|
|
// 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<df::item*> &items = df::global::world->items.other[df::items_other_id::IN_PLAY];
|
|
|
|
|
// sort the items so they get added to the structure in the correct order
|
|
|
|
|
std::sort(job->items.begin(), job->items.end(), job_item_idx_lt);
|
|
|
|
|
|
|
|
|
|
for (size_t i = 0; i < items.size(); i++)
|
|
|
|
|
// derive the material properties of the building and job from the first
|
|
|
|
|
// applicable item, though if any boulders are involved, it makes the whole
|
|
|
|
|
// structure "rough".
|
|
|
|
|
bool rough = false;
|
|
|
|
|
for (auto attached_item : job->items)
|
|
|
|
|
{
|
|
|
|
|
df::item *item = attached_item->item;
|
|
|
|
|
rough = rough || item->getType() == item_type::BOULDER;
|
|
|
|
|
if (bld->mat_type == -1)
|
|
|
|
|
{
|
|
|
|
|
df::item *item = items[i];
|
|
|
|
|
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 (item->flags.whole & bad_flags.whole)
|
|
|
|
|
continue;
|
|
|
|
|
if (bld->needsDesign())
|
|
|
|
|
{
|
|
|
|
|
auto act = (df::building_actual *)bld;
|
|
|
|
|
if (!act->design)
|
|
|
|
|
act->design = new df::building_design();
|
|
|
|
|
act->design->flags.bits.rough = rough;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
df::item_type itype = item->getType();
|
|
|
|
|
if (!is_relevant_item_type[itype])
|
|
|
|
|
continue;
|
|
|
|
|
// we're good to go!
|
|
|
|
|
job->flags.bits.suspend = false;
|
|
|
|
|
Job::checkBuildingsNow();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (itype == df::item_type::BOX && item->isBag())
|
|
|
|
|
continue; //Skip bags
|
|
|
|
|
void Planner::popInvalidTasks(std::queue<std::pair<int32_t, int>> & task_queue)
|
|
|
|
|
{
|
|
|
|
|
while (!task_queue.empty())
|
|
|
|
|
{
|
|
|
|
|
auto & task = task_queue.front();
|
|
|
|
|
auto id = task.first;
|
|
|
|
|
if (planned_buildings.count(id) > 0)
|
|
|
|
|
{
|
|
|
|
|
PlannedBuilding & pb = planned_buildings.at(id);
|
|
|
|
|
if (pb.isValid() &&
|
|
|
|
|
pb.getBuilding()->jobs[0]->job_items[task.second]->quantity)
|
|
|
|
|
{
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
debug("discarding invalid task: bld=%d, job_item_idx=%d",
|
|
|
|
|
id, task.second);
|
|
|
|
|
task_queue.pop();
|
|
|
|
|
unregisterBuilding(id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (item->flags.bits.artifact)
|
|
|
|
|
void Planner::doCycle()
|
|
|
|
|
{
|
|
|
|
|
debug("running cycle for %zu registered buildings",
|
|
|
|
|
planned_buildings.size());
|
|
|
|
|
for (auto it = tasks.begin(); it != tasks.end();)
|
|
|
|
|
{
|
|
|
|
|
auto & buckets = it->second;
|
|
|
|
|
auto other_id = ENUM_ATTR(job_item_vector_id, other, it->first);
|
|
|
|
|
auto item_vector = df::global::world->items.other[other_id];
|
|
|
|
|
debug("matching %zu items in vector %s against %zu buckets",
|
|
|
|
|
item_vector.size(),
|
|
|
|
|
ENUM_KEY_STR(job_item_vector_id, it->first).c_str(),
|
|
|
|
|
buckets.size());
|
|
|
|
|
for (auto item_it = item_vector.rbegin();
|
|
|
|
|
item_it != item_vector.rend();
|
|
|
|
|
++item_it)
|
|
|
|
|
{
|
|
|
|
|
auto item = *item_it;
|
|
|
|
|
if (!itemPassesScreen(item))
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
if (item->flags.bits.in_job ||
|
|
|
|
|
item->isAssignedToStockpile() ||
|
|
|
|
|
item->flags.bits.owned ||
|
|
|
|
|
item->flags.bits.in_chest)
|
|
|
|
|
for (auto bucket_it = buckets.begin(); bucket_it != buckets.end();)
|
|
|
|
|
{
|
|
|
|
|
auto & task_queue = bucket_it->second;
|
|
|
|
|
popInvalidTasks(task_queue);
|
|
|
|
|
if (task_queue.empty())
|
|
|
|
|
{
|
|
|
|
|
debug("removing empty bucket: %s/%s; %zu buckets left",
|
|
|
|
|
ENUM_KEY_STR(job_item_vector_id, it->first).c_str(),
|
|
|
|
|
bucket_it->first.c_str(),
|
|
|
|
|
buckets.size() - 1);
|
|
|
|
|
bucket_it = buckets.erase(bucket_it);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
available_item_vectors[itype].push_back(item);
|
|
|
|
|
auto & task = task_queue.front();
|
|
|
|
|
auto id = task.first;
|
|
|
|
|
auto & pb = planned_buildings.at(id);
|
|
|
|
|
auto building = pb.getBuilding();
|
|
|
|
|
auto job = building->jobs[0];
|
|
|
|
|
auto filter_idx = task.second;
|
|
|
|
|
if (matchesFilters(item, job->job_items[filter_idx],
|
|
|
|
|
pb.getFilters()[filter_idx])
|
|
|
|
|
&& DFHack::Job::attachJobItem(job, item,
|
|
|
|
|
df::job_item_ref::Hauled, filter_idx))
|
|
|
|
|
{
|
|
|
|
|
MaterialInfo material;
|
|
|
|
|
material.decode(item);
|
|
|
|
|
ItemTypeInfo item_type;
|
|
|
|
|
item_type.decode(item);
|
|
|
|
|
debug("attached %s %s to filter %d for %s(%d): %s/%s",
|
|
|
|
|
material.toString().c_str(),
|
|
|
|
|
item_type.toString().c_str(),
|
|
|
|
|
filter_idx,
|
|
|
|
|
ENUM_KEY_STR(building_type, building->getType()).c_str(),
|
|
|
|
|
id,
|
|
|
|
|
ENUM_KEY_STR(job_item_vector_id, it->first).c_str(),
|
|
|
|
|
bucket_it->first.c_str());
|
|
|
|
|
// keep quantity aligned with the actual number of remaining
|
|
|
|
|
// items so if buildingplan is turned off, the building will
|
|
|
|
|
// be completed with the correct number of items.
|
|
|
|
|
--job->job_items[filter_idx]->quantity;
|
|
|
|
|
task_queue.pop();
|
|
|
|
|
if (isJobReady(job))
|
|
|
|
|
{
|
|
|
|
|
finalizeBuilding(building);
|
|
|
|
|
unregisterBuilding(id);
|
|
|
|
|
}
|
|
|
|
|
if (task_queue.empty())
|
|
|
|
|
{
|
|
|
|
|
debug(
|
|
|
|
|
"removing empty item bucket: %s/%s; %zu remaining",
|
|
|
|
|
ENUM_KEY_STR(job_item_vector_id, it->first).c_str(),
|
|
|
|
|
bucket_it->first.c_str(),
|
|
|
|
|
buckets.size() - 1);
|
|
|
|
|
buckets.erase(bucket_it);
|
|
|
|
|
}
|
|
|
|
|
// we found a home for this item; no need to look further
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
++bucket_it;
|
|
|
|
|
}
|
|
|
|
|
if (buckets.empty())
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (buckets.empty())
|
|
|
|
|
{
|
|
|
|
|
debug("removing empty vector: %s; %zu vectors left",
|
|
|
|
|
ENUM_KEY_STR(job_item_vector_id, it->first).c_str(),
|
|
|
|
|
tasks.size() - 1);
|
|
|
|
|
it = tasks.erase(it);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
++it;
|
|
|
|
|
}
|
|
|
|
|
debug("cycle done; %zu registered buildings left",
|
|
|
|
|
planned_buildings.size());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Planner planner;
|
|
|
|
|