dfhack/plugins/buildingplan/buildingplan_cycle.cpp

320 lines
12 KiB
C++

2023-02-13 19:45:26 -07:00
#include "plannedbuilding.h"
#include "buildingplan.h"
#include "Debug.h"
#include "modules/Items.h"
#include "modules/Job.h"
2023-02-27 00:06:25 -07:00
#include "modules/Maps.h"
2023-02-13 19:45:26 -07:00
#include "modules/Materials.h"
#include "df/building_design.h"
#include "df/item.h"
#include "df/item_slabst.h"
2023-02-13 19:45:26 -07:00
#include "df/job.h"
2023-03-02 06:28:12 -07:00
#include "df/map_block.h"
2023-02-13 19:45:26 -07:00
#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;
}
};
bool itemPassesScreen(df::item * item) {
2023-02-13 19:45:26 -07:00
static const BadFlags bad_flags;
return !(item->flags.whole & bad_flags.whole)
&& !item->isAssignedToStockpile();
}
df::job_item getJobItemWithHeatSafety(const df::job_item *job_item, HeatSafety heat) {
df::job_item jitem = *job_item;
if (heat >= HEAT_SAFETY_MAGMA) {
jitem.flags2.bits.magma_safe = true;
jitem.flags2.bits.fire_safe = false;
} else if (heat == HEAT_SAFETY_FIRE && !jitem.flags2.bits.magma_safe)
jitem.flags2.bits.fire_safe = true;
return jitem;
}
bool matchesFilters(df::item * item, const df::job_item * job_item, HeatSafety heat, const ItemFilter &item_filter, const std::set<string> &specials) {
2023-02-13 19:45:26 -07:00
// 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;
if (item->getType() == df::item_type::SLAB && specials.count("engraved")
&& static_cast<df::item_slabst *>(item)->engraving_type != df::slab_engraving_type::Memorial)
return false;
df::job_item jitem = getJobItemWithHeatSafety(job_item, heat);
2023-02-19 01:57:30 -07:00
2023-02-13 19:45:26 -07:00
return Job::isSuitableItem(
2023-02-19 01:57:30 -07:00
&jitem, item->getType(), item->getSubtype())
2023-02-13 19:45:26 -07:00
&& Job::isSuitableMaterial(
2023-02-19 01:57:30 -07:00
&jitem, item->getMaterial(), item->getMaterialIndex(),
2023-02-21 19:05:15 -07:00
item->getType())
&& item_filter.matches(item);
2023-02-13 19:45:26 -07:00
}
bool isJobReady(color_ostream &out, const std::vector<df::job_item *> &jitems) {
2023-02-13 19:45:26 -07:00
int needed_items = 0;
for (auto job_item : jitems) { needed_items += job_item->quantity; }
2023-02-13 19:45:26 -07:00
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.
void finalizeBuilding(color_ostream &out, df::building *bld) {
2023-02-13 19:45:26 -07:00
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;
}
// This is tricky. we want to choose an item that can be brought to the job site, but that's not
// necessarily the same as job->pos. it could be many tiles off in any direction (e.g. for bridges), or
2023-03-02 06:28:12 -07:00
// up or down (e.g. for stairs). For now, just return if the item is on a walkable tile.
static bool isAccessibleFrom(color_ostream &out, df::item *item, df::job *job) {
df::coord item_pos = Items::getPosition(item);
df::map_block *block = Maps::getTileBlock(item_pos);
bool is_walkable = false;
if (block) {
uint16_t walkability_group = index_tile(block->walkable, item_pos);
is_walkable = walkability_group != 0;
TRACE(cycle,out).print("item %d in walkability_group %u at (%d,%d,%d) is %saccessible from job site\n",
item->id, walkability_group, item_pos.x, item_pos.y, item_pos.z, is_walkable ? "" : "not ");
}
return is_walkable;
2023-02-27 00:06:25 -07:00
}
2023-02-13 19:45:26 -07:00
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(); ) {
TRACE(cycle,out).print("scanning bucket: %s/%s\n",
ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), bucket_it->first.c_str());
2023-02-13 19:45:26 -07:00
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 & jitems = job->job_items;
const size_t num_filters = jitems.size();
2023-02-13 19:45:26 -07:00
auto filter_idx = task.second;
const int rev_filter_idx = num_filters - (filter_idx+1);
2023-02-21 19:05:15 -07:00
auto &pb = planned_buildings.at(id);
2023-03-02 06:28:12 -07:00
if (isAccessibleFrom(out, item, job)
&& matchesFilters(item, jitems[filter_idx], pb.heat_safety,
pb.item_filters[rev_filter_idx], pb.specials)
2023-02-27 00:06:25 -07:00
&& Job::attachJobItem(job, item,
2023-02-13 19:45:26 -07:00
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.
--jitems[filter_idx]->quantity;
2023-02-13 19:45:26 -07:00
task_queue.pop_front();
if (isJobReady(out, jitems)) {
2023-02-13 19:45:26 -07:00
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);
vectors.push_back(df::job_item_vector_id::IN_PLAY);
2023-02-13 19:45:26 -07:00
}
};
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 a few elements
2023-02-13 19:45:26 -07:00
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());
}