split buildingplan into a project

develop
Myk Taylor 2023-02-13 18:45:26 -08:00
parent 4b7bc937a4
commit 0faa160eaa
No known key found for this signature in database
9 changed files with 395 additions and 325 deletions

@ -89,7 +89,7 @@ dfhack_plugin(autonestbox autonestbox.cpp LINK_LIBRARIES lua)
dfhack_plugin(blueprint blueprint.cpp LINK_LIBRARIES lua) dfhack_plugin(blueprint blueprint.cpp LINK_LIBRARIES lua)
#dfhack_plugin(burrows burrows.cpp LINK_LIBRARIES lua) #dfhack_plugin(burrows burrows.cpp LINK_LIBRARIES lua)
#dfhack_plugin(building-hacks building-hacks.cpp LINK_LIBRARIES lua) #dfhack_plugin(building-hacks building-hacks.cpp LINK_LIBRARIES lua)
dfhack_plugin(buildingplan buildingplan.cpp LINK_LIBRARIES lua) add_subdirectory(buildingplan)
#dfhack_plugin(changeitem changeitem.cpp) #dfhack_plugin(changeitem changeitem.cpp)
dfhack_plugin(changelayer changelayer.cpp) dfhack_plugin(changelayer changelayer.cpp)
dfhack_plugin(changevein changevein.cpp) dfhack_plugin(changevein changevein.cpp)

@ -2,10 +2,12 @@ project(buildingplan)
set(COMMON_HDRS set(COMMON_HDRS
buildingplan.h buildingplan.h
buildingplan-planner.h itemfilter.h
buildingplan-rooms.h plannedbuilding.h
) )
set_source_files_properties(${COMMON_HDRS} PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties(${COMMON_HDRS} PROPERTIES HEADER_FILE_ONLY TRUE)
dfhack_plugin(buildingplan buildingplan.cpp buildingplan-planner.cpp dfhack_plugin(buildingplan
buildingplan-rooms.cpp ${COMMON_HDRS} LINK_LIBRARIES lua) buildingplan.cpp buildingplan_cycle.cpp itemfilter.cpp plannedbuilding.cpp
${COMMON_HDRS}
LINK_LIBRARIES lua)

@ -1,28 +1,17 @@
#include "Core.h" #include "plannedbuilding.h"
#include "buildingplan.h"
#include "Debug.h" #include "Debug.h"
#include "LuaTools.h" #include "LuaTools.h"
#include "PluginManager.h" #include "PluginManager.h"
#include "modules/Items.h"
#include "modules/Job.h"
#include "modules/Materials.h"
#include "modules/Persistence.h"
#include "modules/World.h" #include "modules/World.h"
#include "df/building.h"
#include "df/building_design.h"
#include "df/item.h" #include "df/item.h"
#include "df/job_item.h" #include "df/job_item.h"
#include "df/world.h" #include "df/world.h"
#include <deque>
#include <string>
#include <vector>
#include <unordered_map>
using std::map; using std::map;
using std::pair;
using std::deque;
using std::string; using std::string;
using std::unordered_map; using std::unordered_map;
using std::vector; using std::vector;
@ -40,72 +29,29 @@ namespace DFHack {
} }
static const string CONFIG_KEY = string(plugin_name) + "/config"; static const string CONFIG_KEY = string(plugin_name) + "/config";
static const string BLD_CONFIG_KEY = string(plugin_name) + "/building"; const string BLD_CONFIG_KEY = string(plugin_name) + "/building";
enum ConfigValues {
CONFIG_BLOCKS = 1,
CONFIG_BOULDERS = 2,
CONFIG_LOGS = 3,
CONFIG_BARS = 4,
};
enum BuildingConfigValues {
BLD_CONFIG_ID = 0,
};
static int get_config_val(PersistentDataItem &c, int index) { int get_config_val(PersistentDataItem &c, int index) {
if (!c.isValid()) if (!c.isValid())
return -1; return -1;
return c.ival(index); return c.ival(index);
} }
static bool get_config_bool(PersistentDataItem &c, int index) { bool get_config_bool(PersistentDataItem &c, int index) {
return get_config_val(c, index) == 1; return get_config_val(c, index) == 1;
} }
static void set_config_val(PersistentDataItem &c, int index, int value) { void set_config_val(PersistentDataItem &c, int index, int value) {
if (c.isValid()) if (c.isValid())
c.ival(index) = value; c.ival(index) = value;
} }
static void set_config_bool(PersistentDataItem &c, int index, bool value) { void set_config_bool(PersistentDataItem &c, int index, bool value) {
set_config_val(c, index, value ? 1 : 0); set_config_val(c, index, value ? 1 : 0);
} }
class PlannedBuilding {
public:
const df::building::key_field_type id;
PlannedBuilding(color_ostream &out, df::building *building) : id(building->id) {
DEBUG(status,out).print("creating persistent data for building %d\n", id);
bld_config = DFHack::World::AddPersistentData(BLD_CONFIG_KEY);
set_config_val(bld_config, BLD_CONFIG_ID, id);
}
PlannedBuilding(DFHack::PersistentDataItem &bld_config)
: id(get_config_val(bld_config, BLD_CONFIG_ID)), bld_config(bld_config) { }
void remove(color_ostream &out);
// 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.
df::building * getBuildingIfValidOrRemoveIfNot(color_ostream &out) {
auto bld = df::building::find(id);
bool valid = bld && bld->getBuildStage() == 0;
if (!valid) {
remove(out);
return NULL;
}
return bld;
}
private:
DFHack::PersistentDataItem bld_config;
};
static PersistentDataItem config; static PersistentDataItem config;
// building id -> PlannedBuilding // building id -> PlannedBuilding
unordered_map<int32_t, PlannedBuilding> planned_buildings; unordered_map<int32_t, PlannedBuilding> planned_buildings;
// vector id -> filter bucket -> queue of (building id, job_item index) // vector id -> filter bucket -> queue of (building id, job_item index)
map<df::job_item_vector_id, map<string, deque<pair<int32_t, int>>>> tasks; Tasks tasks;
// note that this just removes the PlannedBuilding. the tasks will get dropped // note that this just removes the PlannedBuilding. the tasks will get dropped
// as we discover them in the tasks queues and they fail to be found in planned_buildings. // as we discover them in the tasks queues and they fail to be found in planned_buildings.
@ -115,7 +61,7 @@ map<df::job_item_vector_id, map<string, deque<pair<int32_t, int>>>> tasks;
// no chance of duplicate tasks getting added to the tasks queues. // no chance of duplicate tasks getting added to the tasks queues.
void PlannedBuilding::remove(color_ostream &out) { void PlannedBuilding::remove(color_ostream &out) {
DEBUG(status,out).print("removing persistent data for building %d\n", id); DEBUG(status,out).print("removing persistent data for building %d\n", id);
DFHack::World::DeletePersistentData(config); World::DeletePersistentData(config);
if (planned_buildings.count(id) > 0) if (planned_buildings.count(id) > 0)
planned_buildings.erase(id); planned_buildings.erase(id);
} }
@ -124,7 +70,9 @@ static const int32_t CYCLE_TICKS = 600; // twice per game day
static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle
static command_result do_command(color_ostream &out, vector<string> &parameters); static command_result do_command(color_ostream &out, vector<string> &parameters);
static void do_cycle(color_ostream &out); void buildingplan_cycle(color_ostream &out, Tasks &tasks,
unordered_map<int32_t, PlannedBuilding> &planned_buildings);
static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb); static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb);
DFhackCExport command_result plugin_init(color_ostream &out, std::vector <PluginCommand> &commands) { DFhackCExport command_result plugin_init(color_ostream &out, std::vector <PluginCommand> &commands) {
@ -186,7 +134,7 @@ DFhackCExport command_result plugin_load_data (color_ostream &out) {
} }
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) {
if (event == DFHack::SC_WORLD_UNLOADED) { if (event == SC_WORLD_UNLOADED) {
DEBUG(status,out).print("world unloaded; clearing state for %s\n", plugin_name); DEBUG(status,out).print("world unloaded; clearing state for %s\n", plugin_name);
planned_buildings.clear(); planned_buildings.clear();
tasks.clear(); tasks.clear();
@ -196,6 +144,14 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan
static bool cycle_requested = false; static bool cycle_requested = false;
static void do_cycle(color_ostream &out) {
// mark that we have recently run
cycle_timestamp = world->frame_counter;
cycle_requested = false;
buildingplan_cycle(out, tasks, planned_buildings);
}
DFhackCExport command_result plugin_onupdate(color_ostream &out) { DFhackCExport command_result plugin_onupdate(color_ostream &out) {
if (!Core::getInstance().isWorldLoaded()) if (!Core::getInstance().isWorldLoaded())
return CR_OK; return CR_OK;
@ -249,259 +205,6 @@ static command_result do_command(color_ostream &out, vector<string> &parameters)
return show_help ? CR_WRONG_USAGE : CR_OK; return show_help ? CR_WRONG_USAGE : CR_OK;
} }
/////////////////////////////////////////////////////
// cycle logic
//
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);
F(spider_web);
#undef F
whole = flags.whole;
}
};
static bool itemPassesScreen(df::item * item) {
static const BadFlags bad_flags;
return !(item->flags.whole & bad_flags.whole)
&& !item->isAssignedToStockpile();
}
static bool matchesFilters(df::item * item, df::job_item * job_item) {
// 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());
}
static bool isJobReady(color_ostream &out, df::job * job) {
int needed_items = 0;
for (auto job_item : job->job_items) { needed_items += job_item->quantity; }
if (needed_items) {
DEBUG(cycle,out).print("building needs %d more item(s)\n", 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(color_ostream &out, df::building * bld) {
DEBUG(cycle,out).print("finalizing building %d\n", 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. 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() == df::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();
}
static df::building * popInvalidTasks(color_ostream &out, deque<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) {
auto bld = planned_buildings.at(id).getBuildingIfValidOrRemoveIfNot(out);
if (bld && bld->jobs[0]->job_items[task.second]->quantity)
return bld;
}
DEBUG(cycle,out).print("discarding invalid task: bld=%d, job_item_idx=%d\n", id, task.second);
task_queue.pop_front();
}
return NULL;
}
static void doVector(color_ostream &out, df::job_item_vector_id vector_id,
map<string, deque<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(cycle,out).print("matching %zu item(s) in vector %s against %zu filter bucket(s)\n",
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;
auto bld = popInvalidTasks(out, task_queue);
if (!bld) {
DEBUG(cycle,out).print("removing empty bucket: %s/%s; %zu bucket(s) left\n",
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 job = bld->jobs[0];
auto filter_idx = task.second;
if (matchesFilters(item, job->job_items[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(cycle,out).print("attached %s %s to filter %d for %s(%d): %s/%s\n",
material.toString().c_str(),
item_type.toString().c_str(),
filter_idx,
ENUM_KEY_STR(building_type, bld->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_front();
if (isJobReady(out, job)) {
finalizeBuilding(out, bld);
planned_buildings.at(id).remove(out);
}
if (task_queue.empty()) {
DEBUG(cycle,out).print(
"removing empty item bucket: %s/%s; %zu left\n",
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);
}
};
static void do_cycle(color_ostream &out) {
static const VectorsToScanLast vectors_to_scan_last;
// mark that we have recently run
cycle_timestamp = world->frame_counter;
cycle_requested = false;
DEBUG(cycle,out).print("running %s cycle for %zu registered buildings\n",
plugin_name, planned_buildings.size());
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(out, vector_id, buckets);
if (buckets.empty()) {
DEBUG(cycle,out).print("removing empty vector: %s; %zu vector(s) left\n",
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(out, vector_id, buckets);
if (buckets.empty()) {
DEBUG(cycle,out).print("removing empty vector: %s; %zu vector(s) left\n",
ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(),
tasks.size() - 1);
tasks.erase(vector_id);
}
}
DEBUG(cycle,out).print("cycle done; %zu registered building(s) left\n",
planned_buildings.size());
}
///////////////////////////////////////////////////// /////////////////////////////////////////////////////
// Lua API // Lua API
// core will already be suspended when coming in through here // core will already be suspended when coming in through here
@ -617,7 +320,7 @@ static void printStatus(color_ostream &out) {
int32_t total = 0; int32_t total = 0;
for (auto &buckets : tasks) { for (auto &buckets : tasks) {
for (auto &bucket_queue : buckets.second) { for (auto &bucket_queue : buckets.second) {
deque<pair<int32_t, int>> &tqueue = bucket_queue.second; Bucket &tqueue = bucket_queue.second;
for (auto it = tqueue.begin(); it != tqueue.end();) { for (auto it = tqueue.begin(); it != tqueue.end();) {
auto & task = *it; auto & task = *it;
auto id = task.first; auto id = task.first;

@ -0,0 +1,28 @@
#pragma once
#include "modules/Persistence.h"
#include "df/job_item_vector_id.h"
#include <deque>
typedef std::deque<std::pair<int32_t, int>> Bucket;
typedef std::map<df::job_item_vector_id, std::map<std::string, Bucket>> Tasks;
extern const std::string BLD_CONFIG_KEY;
enum ConfigValues {
CONFIG_BLOCKS = 1,
CONFIG_BOULDERS = 2,
CONFIG_LOGS = 3,
CONFIG_BARS = 4,
};
enum BuildingConfigValues {
BLD_CONFIG_ID = 0,
};
int get_config_val(DFHack::PersistentDataItem &c, int index);
bool get_config_bool(DFHack::PersistentDataItem &c, int index);
void set_config_val(DFHack::PersistentDataItem &c, int index, int value);
void set_config_bool(DFHack::PersistentDataItem &c, int index, bool value);

@ -0,0 +1,275 @@
#include "plannedbuilding.h"
#include "buildingplan.h"
#include "Debug.h"
#include "modules/Items.h"
#include "modules/Job.h"
#include "modules/Materials.h"
#include "df/building_design.h"
#include "df/item.h"
#include "df/job.h"
#include "df/job_item.h"
#include "df/world.h"
#include <unordered_map>
using std::map;
using std::string;
using std::unordered_map;
namespace DFHack {
DBG_EXTERN(buildingplan, cycle);
}
using namespace DFHack;
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);
F(spider_web);
#undef F
whole = flags.whole;
}
};
static bool itemPassesScreen(df::item * item) {
static const BadFlags bad_flags;
return !(item->flags.whole & bad_flags.whole)
&& !item->isAssignedToStockpile();
}
static bool matchesFilters(df::item * item, df::job_item * job_item) {
// 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 Job::isSuitableItem(
job_item, item->getType(), item->getSubtype())
&& Job::isSuitableMaterial(
job_item, item->getMaterial(), item->getMaterialIndex(),
item->getType());
}
static bool isJobReady(color_ostream &out, df::job * job) {
int needed_items = 0;
for (auto job_item : job->job_items) { needed_items += job_item->quantity; }
if (needed_items) {
DEBUG(cycle,out).print("building needs %d more item(s)\n", 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(color_ostream &out, df::building * bld) {
DEBUG(cycle,out).print("finalizing building %d\n", 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. 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() == df::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();
}
static df::building * popInvalidTasks(color_ostream &out, Bucket &task_queue,
unordered_map<int32_t, PlannedBuilding> &planned_buildings) {
while (!task_queue.empty()) {
auto & task = task_queue.front();
auto id = task.first;
if (planned_buildings.count(id) > 0) {
auto bld = planned_buildings.at(id).getBuildingIfValidOrRemoveIfNot(out);
if (bld && bld->jobs[0]->job_items[task.second]->quantity)
return bld;
}
DEBUG(cycle,out).print("discarding invalid task: bld=%d, job_item_idx=%d\n", id, task.second);
task_queue.pop_front();
}
return NULL;
}
static void doVector(color_ostream &out, df::job_item_vector_id vector_id,
map<string, Bucket> &buckets,
unordered_map<int32_t, PlannedBuilding> &planned_buildings) {
auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id);
auto item_vector = df::global::world->items.other[other_id];
DEBUG(cycle,out).print("matching %zu item(s) in vector %s against %zu filter bucket(s)\n",
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;
auto bld = popInvalidTasks(out, task_queue, planned_buildings);
if (!bld) {
DEBUG(cycle,out).print("removing empty bucket: %s/%s; %zu bucket(s) left\n",
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 job = bld->jobs[0];
auto filter_idx = task.second;
if (matchesFilters(item, job->job_items[filter_idx])
&& Job::attachJobItem(job, item,
df::job_item_ref::Hauled, filter_idx))
{
MaterialInfo material;
material.decode(item);
ItemTypeInfo item_type;
item_type.decode(item);
DEBUG(cycle,out).print("attached %s %s to filter %d for %s(%d): %s/%s\n",
material.toString().c_str(),
item_type.toString().c_str(),
filter_idx,
ENUM_KEY_STR(building_type, bld->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_front();
if (isJobReady(out, job)) {
finalizeBuilding(out, bld);
planned_buildings.at(id).remove(out);
}
if (task_queue.empty()) {
DEBUG(cycle,out).print(
"removing empty item bucket: %s/%s; %zu left\n",
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 buildingplan_cycle(color_ostream &out, Tasks &tasks,
unordered_map<int32_t, PlannedBuilding> &planned_buildings) {
static const VectorsToScanLast vectors_to_scan_last;
DEBUG(cycle,out).print(
"running buildingplan cycle for %zu registered buildings\n",
planned_buildings.size());
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(out, vector_id, buckets, planned_buildings);
if (buckets.empty()) {
DEBUG(cycle,out).print("removing empty vector: %s; %zu vector(s) left\n",
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(out, vector_id, buckets, planned_buildings);
if (buckets.empty()) {
DEBUG(cycle,out).print("removing empty vector: %s; %zu vector(s) left\n",
ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(),
tasks.size() - 1);
tasks.erase(vector_id);
}
}
DEBUG(cycle,out).print("cycle done; %zu registered building(s) left\n",
planned_buildings.size());
}

@ -0,0 +1,36 @@
#include "plannedbuilding.h"
#include "buildingplan.h"
#include "Debug.h"
#include "modules/World.h"
namespace DFHack {
DBG_EXTERN(buildingplan, status);
DBG_EXTERN(buildingplan, cycle);
}
using namespace DFHack;
PlannedBuilding::PlannedBuilding(color_ostream &out, df::building *building)
: id(building->id) {
DEBUG(status,out).print("creating persistent data for building %d\n", id);
bld_config = World::AddPersistentData(BLD_CONFIG_KEY);
set_config_val(bld_config, BLD_CONFIG_ID, id);
}
PlannedBuilding::PlannedBuilding(PersistentDataItem &bld_config)
: id(get_config_val(bld_config, BLD_CONFIG_ID)), bld_config(bld_config) { }
// 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.
df::building * PlannedBuilding::getBuildingIfValidOrRemoveIfNot(color_ostream &out) {
auto bld = df::building::find(id);
bool valid = bld && bld->getBuildStage() == 0;
if (!valid) {
remove(out);
return NULL;
}
return bld;
}

@ -0,0 +1,25 @@
#pragma once
#include "Core.h"
#include "modules/Persistence.h"
#include "df/building.h"
class PlannedBuilding {
public:
const df::building::key_field_type id;
PlannedBuilding(DFHack::color_ostream &out, df::building *building);
PlannedBuilding(DFHack::PersistentDataItem &bld_config);
void remove(DFHack::color_ostream &out);
// 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.
df::building * getBuildingIfValidOrRemoveIfNot(DFHack::color_ostream &out);
private:
DFHack::PersistentDataItem bld_config;
};