infrastructure for item filtering

develop
Myk Taylor 2023-02-21 18:05:15 -08:00
parent a0798178a6
commit 097e955796
No known key found for this signature in database
8 changed files with 393 additions and 88 deletions

@ -73,8 +73,9 @@ struct BuildingTypeKeyHash {
static PersistentDataItem config;
// for use in counting available materials for the UI
static unordered_map<BuildingTypeKey, vector<df::job_item *>, BuildingTypeKeyHash> job_item_repo;
static unordered_map<BuildingTypeKey, vector<df::job_item *>, BuildingTypeKeyHash> job_item_cache;
static unordered_map<BuildingTypeKey, HeatSafety, BuildingTypeKeyHash> cur_heat_safety;
static unordered_map<BuildingTypeKey, vector<ItemFilter>, BuildingTypeKeyHash> cur_item_filters;
// building id -> PlannedBuilding
static unordered_map<int32_t, PlannedBuilding> planned_buildings;
// vector id -> filter bucket -> queue of (building id, job_item index)
@ -96,6 +97,87 @@ void PlannedBuilding::remove(color_ostream &out) {
static const int32_t CYCLE_TICKS = 600; // twice per game day
static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle
static bool call_buildingplan_lua(color_ostream *out, const char *fn_name,
int nargs = 0, int nres = 0,
Lua::LuaLambda && args_lambda = Lua::DEFAULT_LUA_LAMBDA,
Lua::LuaLambda && res_lambda = Lua::DEFAULT_LUA_LAMBDA) {
DEBUG(status).print("calling buildingplan lua function: '%s'\n", fn_name);
CoreSuspender guard;
auto L = Lua::Core::State;
Lua::StackUnwinder top(L);
if (!out)
out = &Core::getInstance().getConsole();
return Lua::CallLuaModuleFunction(*out, L, "plugins.buildingplan", fn_name,
nargs, nres,
std::forward<Lua::LuaLambda&&>(args_lambda),
std::forward<Lua::LuaLambda&&>(res_lambda));
}
static int get_num_filters(color_ostream &out, BuildingTypeKey key) {
int num_filters = 0;
if (!call_buildingplan_lua(&out, "get_num_filters", 3, 1,
[&](lua_State *L) {
Lua::Push(L, std::get<0>(key));
Lua::Push(L, std::get<1>(key));
Lua::Push(L, std::get<2>(key));
},
[&](lua_State *L) {
num_filters = lua_tonumber(L, -1);
})) {
return 0;
}
return num_filters;
}
static vector<df::job_item *> & get_job_items(color_ostream &out, BuildingTypeKey key) {
if (job_item_cache.count(key))
return job_item_cache[key];
const int num_filters = get_num_filters(out, key);
auto &jitems = job_item_cache[key];
for (int index = 0; index < num_filters; ++index) {
bool failed = false;
if (!call_buildingplan_lua(&out, "get_job_item", 4, 1,
[&](lua_State *L) {
Lua::Push(L, std::get<0>(key));
Lua::Push(L, std::get<1>(key));
Lua::Push(L, std::get<2>(key));
Lua::Push(L, index+1);
},
[&](lua_State *L) {
df::job_item *jitem = Lua::GetDFObject<df::job_item>(L, -1);
DEBUG(status,out).print("retrieving job_item for (%d, %d, %d) index=%d: %p\n",
std::get<0>(key), std::get<1>(key), std::get<2>(key), index, jitem);
if (!jitem)
failed = true;
else
jitems.emplace_back(jitem);
}) || failed) {
jitems.clear();
break;
}
}
return jitems;
}
static HeatSafety get_heat_safety_filter(const BuildingTypeKey &key) {
if (cur_heat_safety.count(key))
return cur_heat_safety.at(key);
return HEAT_SAFETY_ANY;
}
static vector<ItemFilter> & get_item_filters(color_ostream &out, const BuildingTypeKey &key) {
if (cur_item_filters.count(key))
return cur_item_filters[key];
vector<ItemFilter> &filters = cur_item_filters[key];
filters.resize(get_job_items(out, key).size());
return filters;
}
static command_result do_command(color_ostream &out, vector<string> &parameters);
void buildingplan_cycle(color_ostream &out, Tasks &tasks,
unordered_map<int32_t, PlannedBuilding> &planned_buildings);
@ -149,37 +231,19 @@ static void validate_config(color_ostream &out, bool verbose = false) {
set_config_bool(config, CONFIG_BARS, false);
}
static bool call_buildingplan_lua(color_ostream *out, const char *fn_name,
int nargs = 0, int nres = 0,
Lua::LuaLambda && args_lambda = Lua::DEFAULT_LUA_LAMBDA,
Lua::LuaLambda && res_lambda = Lua::DEFAULT_LUA_LAMBDA) {
DEBUG(status).print("calling buildingplan lua function: '%s'\n", fn_name);
CoreSuspender guard;
auto L = Lua::Core::State;
Lua::StackUnwinder top(L);
if (!out)
out = &Core::getInstance().getConsole();
return Lua::CallLuaModuleFunction(*out, L, "plugins.buildingplan", fn_name,
nargs, nres,
std::forward<Lua::LuaLambda&&>(args_lambda),
std::forward<Lua::LuaLambda&&>(res_lambda));
}
static void clear_state(color_ostream &out) {
call_buildingplan_lua(&out, "signal_reset");
call_buildingplan_lua(&out, "reload_cursors");
planned_buildings.clear();
tasks.clear();
for (auto &entry : job_item_repo) {
cur_heat_safety.clear();
cur_item_filters.clear();
for (auto &entry : job_item_cache ) {
for (auto &jitem : entry.second) {
delete jitem;
}
}
job_item_repo.clear();
job_item_cache.clear();
}
DFhackCExport command_result plugin_load_data (color_ostream &out) {
@ -199,7 +263,15 @@ DFhackCExport command_result plugin_load_data (color_ostream &out) {
const size_t num_building_configs = building_configs.size();
for (size_t idx = 0; idx < num_building_configs; ++idx) {
PlannedBuilding pb(out, building_configs[idx]);
registerPlannedBuilding(out, pb);
df::building *bld = df::building::find(pb.id);
if (!bld) {
WARN(status).print("cannot find building %d; halting load\n", pb.id);
}
BuildingTypeKey key(bld->getType(), bld->getSubtype(), bld->getCustomType());
if (pb.item_filters.size() != get_item_filters(out, key).size())
WARN(status).print("loaded state for building %d doesn't match world\n", pb.id);
else
registerPlannedBuilding(out, pb);
}
return CR_OK;
@ -438,30 +510,12 @@ static bool setSetting(color_ostream &out, string name, bool value) {
static bool isPlannableBuilding(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom) {
DEBUG(status,out).print("entering isPlannableBuilding\n");
int num_filters = 0;
if (!call_buildingplan_lua(&out, "get_num_filters", 3, 1,
[&](lua_State *L) {
Lua::Push(L, type);
Lua::Push(L, subtype);
Lua::Push(L, custom);
},
[&](lua_State *L) {
num_filters = lua_tonumber(L, -1);
})) {
return false;
}
return num_filters >= 1;
return get_num_filters(out, BuildingTypeKey(type, subtype, custom)) >= 1;
}
static bool isPlannedBuilding(color_ostream &out, df::building *bld) {
TRACE(status,out).print("entering isPlannedBuilding\n");
return bld && planned_buildings.count(bld->id) > 0;
}
static HeatSafety get_heat_safety_filter(const BuildingTypeKey &key) {
if (cur_heat_safety.count(key))
return cur_heat_safety.at(key);
return HEAT_SAFETY_ANY;
return bld && planned_buildings.count(bld->id);
}
static bool addPlannedBuilding(color_ostream &out, df::building *bld) {
@ -471,7 +525,7 @@ static bool addPlannedBuilding(color_ostream &out, df::building *bld) {
bld->getCustomType()))
return false;
BuildingTypeKey key(bld->getType(), bld->getSubtype(), bld->getCustomType());
PlannedBuilding pb(out, bld, get_heat_safety_filter(key));
PlannedBuilding pb(out, bld, get_heat_safety_filter(key), get_item_filters(out, key));
return registerPlannedBuilding(out, pb);
}
@ -492,30 +546,10 @@ static int scanAvailableItems(color_ostream &out, df::building_type type, int16_
type, subtype, custom, index);
BuildingTypeKey key(type, subtype, custom);
HeatSafety heat = get_heat_safety_filter(key);
auto &job_items = job_item_repo[key];
if (index >= (int)job_items.size()) {
for (int i = job_items.size(); i <= index; ++i) {
bool failed = false;
if (!call_buildingplan_lua(&out, "get_job_item", 4, 1,
[&](lua_State *L) {
Lua::Push(L, type);
Lua::Push(L, subtype);
Lua::Push(L, custom);
Lua::Push(L, index+1);
},
[&](lua_State *L) {
df::job_item *jitem = Lua::GetDFObject<df::job_item>(L, -1);
DEBUG(status,out).print("retrieving job_item for index=%d: %p\n",
index, jitem);
if (!jitem)
failed = true;
else
job_items.emplace_back(jitem);
}) || failed) {
return 0;
}
}
}
auto &job_items = get_job_items(out, key);
if (job_items.size() <= index)
return 0;
auto &item_filters = get_item_filters(out, key);
auto &jitem = job_items[index];
auto vector_ids = getVectorIds(out, jitem);
@ -524,7 +558,7 @@ static int scanAvailableItems(color_ostream &out, df::building_type type, int16_
for (auto vector_id : vector_ids) {
auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id);
for (auto &item : df::global::world->items.other[other_id]) {
if (itemPassesScreen(item) && matchesFilters(item, jitem, heat)) {
if (itemPassesScreen(item) && matchesFilters(item, jitem, heat, item_filters[index])) {
if (item_ids)
item_ids->emplace_back(item->id);
++count;

@ -1,5 +1,7 @@
#pragma once
#include "itemfilter.h"
#include "modules/Persistence.h"
#include "df/building.h"
@ -38,6 +40,6 @@ void set_config_bool(DFHack::PersistentDataItem &c, int index, bool value);
std::vector<df::job_item_vector_id> getVectorIds(DFHack::color_ostream &out, df::job_item *job_item);
bool itemPassesScreen(df::item * item);
bool matchesFilters(df::item * item, df::job_item * job_item, HeatSafety heat);
bool matchesFilters(df::item * item, df::job_item * job_item, HeatSafety heat, const ItemFilter &item_filter);
bool isJobReady(DFHack::color_ostream &out, const std::vector<df::job_item *> &jitems);
void finalizeBuilding(DFHack::color_ostream &out, df::building *bld);

@ -46,7 +46,7 @@ bool itemPassesScreen(df::item * item) {
&& !item->isAssignedToStockpile();
}
bool matchesFilters(df::item * item, df::job_item * job_item, HeatSafety heat) {
bool matchesFilters(df::item * item, df::job_item * job_item, HeatSafety heat, 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;
@ -76,7 +76,8 @@ bool matchesFilters(df::item * item, df::job_item * job_item, HeatSafety heat) {
&jitem, item->getType(), item->getSubtype())
&& Job::isSuitableMaterial(
&jitem, item->getMaterial(), item->getMaterialIndex(),
item->getType());
item->getType())
&& item_filter.matches(item);
}
bool isJobReady(color_ostream &out, const std::vector<df::job_item *> &jitems) {
@ -180,7 +181,9 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id,
auto id = task.first;
auto job = bld->jobs[0];
auto filter_idx = task.second;
if (matchesFilters(item, job->job_items[filter_idx], planned_buildings.at(id).heat_safety)
auto &pb = planned_buildings.at(id);
if (matchesFilters(item, job->job_items[filter_idx], pb.heat_safety,
pb.item_filters[filter_idx])
&& Job::attachJobItem(job, item,
df::job_item_ref::Hauled, filter_idx))
{

@ -0,0 +1,189 @@
#include "itemfilter.h"
#include "Debug.h"
#include "df/item.h"
using namespace DFHack;
namespace DFHack {
DBG_EXTERN(buildingplan, status);
}
ItemFilter::ItemFilter() {
clear();
}
void ItemFilter::clear() {
min_quality = df::item_quality::Ordinary;
max_quality = df::item_quality::Masterful;
decorated_only = false;
mat_mask.whole = 0;
materials.clear();
}
bool ItemFilter::isEmpty() {
return min_quality == df::item_quality::Ordinary
&& max_quality == df::item_quality::Masterful
&& !decorated_only
&& !mat_mask.whole
&& materials.empty();
}
static bool deserializeMaterialMask(std::string ser, df::dfhack_material_category mat_mask) {
if (ser.empty())
return true;
if (!parseJobMaterialCategory(&mat_mask, ser)) {
DEBUG(status).print("invalid job material category serialization: '%s'", ser.c_str());
return false;
}
return true;
}
static bool deserializeMaterials(std::string ser, std::vector<DFHack::MaterialInfo> &materials) {
if (ser.empty())
return true;
std::vector<std::string> mat_names;
split_string(&mat_names, ser, ",");
for (auto m = mat_names.begin(); m != mat_names.end(); m++) {
DFHack::MaterialInfo material;
if (!material.find(*m) || !material.isValid()) {
DEBUG(status).print("invalid material name serialization: '%s'", ser.c_str());
return false;
}
materials.push_back(material);
}
return true;
}
ItemFilter::ItemFilter(std::string serialized) {
clear();
std::vector<std::string> tokens;
split_string(&tokens, serialized, "/");
if (tokens.size() != 5) {
DEBUG(status).print("invalid ItemFilter serialization: '%s'", serialized.c_str());
return;
}
if (!deserializeMaterialMask(tokens[0], mat_mask) || !deserializeMaterials(tokens[1], materials))
return;
setMinQuality(atoi(tokens[2].c_str()));
setMaxQuality(atoi(tokens[3].c_str()));
decorated_only = static_cast<bool>(atoi(tokens[4].c_str()));
}
// format: mat,mask,elements/materials,list/minq/maxq/decorated
std::string ItemFilter::serialize() const {
std::ostringstream ser;
ser << bitfield_to_string(mat_mask, ",") << "/";
if (!materials.empty()) {
ser << materials[0].getToken();
for (size_t i = 1; i < materials.size(); ++i)
ser << "," << materials[i].getToken();
}
ser << "/" << static_cast<int>(min_quality);
ser << "/" << static_cast<int>(max_quality);
ser << "/" << static_cast<int>(decorated_only);
return ser.str();
}
static void clampItemQuality(df::item_quality *quality) {
if (*quality > df::item_quality::Artifact) {
DEBUG(status).print("clamping quality to Artifact");
*quality = df::item_quality::Artifact;
}
if (*quality < df::item_quality::Ordinary) {
DEBUG(status).print("clamping quality to Ordinary");
*quality = df::item_quality::Ordinary;
}
}
void ItemFilter::setMinQuality(int quality) {
min_quality = static_cast<df::item_quality>(quality);
clampItemQuality(&min_quality);
if (max_quality < min_quality)
max_quality = min_quality;
}
void ItemFilter::setMaxQuality(int quality) {
max_quality = static_cast<df::item_quality>(quality);
clampItemQuality(&max_quality);
if (max_quality < min_quality)
min_quality = max_quality;
}
void ItemFilter::setDecoratedOnly(bool decorated) {
decorated_only = decorated;
}
void ItemFilter::setMaterialMask(uint32_t mask) {
mat_mask.whole = mask;
}
void ItemFilter::setMaterials(const std::vector<DFHack::MaterialInfo> &materials) {
this->materials = materials;
}
std::string ItemFilter::getMinQuality() const {
return ENUM_KEY_STR(item_quality, min_quality);
}
std::string ItemFilter::getMaxQuality() const {
return ENUM_KEY_STR(item_quality, max_quality);
}
bool ItemFilter::getDecoratedOnly() const {
return decorated_only;
}
uint32_t ItemFilter::getMaterialMask() const {
return mat_mask.whole;
}
static std::string material_to_string_fn(const MaterialInfo &m) { return m.toString(); }
std::vector<std::string> ItemFilter::getMaterials() const {
std::vector<std::string> descriptions;
transform_(materials, descriptions, material_to_string_fn);
if (descriptions.size() == 0)
bitfield_to_string(&descriptions, mat_mask);
if (descriptions.size() == 0)
descriptions.push_back("any");
return descriptions;
}
static bool matchesMask(DFHack::MaterialInfo &mat, df::dfhack_material_category mat_mask) {
return mat_mask.whole ? mat.matches(mat_mask) : true;
}
bool ItemFilter::matches(df::dfhack_material_category mask) const {
return mask.whole & mat_mask.whole;
}
bool ItemFilter::matches(DFHack::MaterialInfo &material) const {
for (auto it = materials.begin(); it != materials.end(); ++it)
if (material.matches(*it))
return true;
return false;
}
bool ItemFilter::matches(df::item *item) const {
if (item->getQuality() < min_quality || item->getQuality() > max_quality)
return false;
if (decorated_only && !item->hasImprovements())
return false;
auto imattype = item->getActualMaterial();
auto imatindex = item->getActualMaterialIndex();
auto item_mat = DFHack::MaterialInfo(imattype, imatindex);
return (materials.size() == 0) ? matchesMask(item_mat, mat_mask) : matches(item_mat);
}

@ -1 +1,39 @@
#pragma once
#include "modules/Materials.h"
#include "df/dfhack_material_category.h"
#include "df/item_quality.h"
class ItemFilter {
public:
ItemFilter();
ItemFilter(std::string serialized);
void clear();
bool isEmpty();
std::string serialize() const;
void setMinQuality(int quality);
void setMaxQuality(int quality);
void setDecoratedOnly(bool decorated);
void setMaterialMask(uint32_t mask);
void setMaterials(const std::vector<DFHack::MaterialInfo> &materials);
std::string getMinQuality() const;
std::string getMaxQuality() const;
bool getDecoratedOnly() const;
uint32_t getMaterialMask() const;
std::vector<std::string> getMaterials() const;
bool matches(df::dfhack_material_category mask) const;
bool matches(DFHack::MaterialInfo &material) const;
bool matches(df::item *item) const;
private:
df::item_quality min_quality;
df::item_quality max_quality;
bool decorated_only;
df::dfhack_material_category mat_mask;
std::vector<DFHack::MaterialInfo> materials;
};

@ -31,14 +31,18 @@ static vector<vector<df::job_item_vector_id>> get_vector_ids(color_ostream &out,
return ret;
}
static vector<vector<df::job_item_vector_id>> deserialize(color_ostream &out, PersistentDataItem &bld_config) {
static vector<vector<df::job_item_vector_id>> deserialize_vector_ids(color_ostream &out, PersistentDataItem &bld_config) {
vector<vector<df::job_item_vector_id>> ret;
DEBUG(status,out).print("deserializing state for building %d: %s\n",
get_config_val(bld_config, BLD_CONFIG_ID), bld_config.val().c_str());
vector<string> rawstrs;
split_string(&rawstrs, bld_config.val(), "|");
const string &serialized = rawstrs[0];
DEBUG(status,out).print("deserializing vector ids for building %d: %s\n",
get_config_val(bld_config, BLD_CONFIG_ID), serialized.c_str());
vector<string> joined;
split_string(&joined, bld_config.val(), "|");
split_string(&joined, serialized, ";");
for (auto &str : joined) {
vector<string> lst;
split_string(&lst, str, ",");
@ -54,28 +58,60 @@ static vector<vector<df::job_item_vector_id>> deserialize(color_ostream &out, Pe
return ret;
}
static string serialize(const vector<vector<df::job_item_vector_id>> &vector_ids) {
static std::vector<ItemFilter> deserialize_item_filters(color_ostream &out, PersistentDataItem &bld_config) {
std::vector<ItemFilter> ret;
vector<string> rawstrs;
split_string(&rawstrs, bld_config.val(), "|");
if (rawstrs.size() < 2)
return ret;
const string &serialized = rawstrs[1];
DEBUG(status,out).print("deserializing item filters for building %d: %s\n",
get_config_val(bld_config, BLD_CONFIG_ID), serialized.c_str());
vector<string> filterstrs;
split_string(&filterstrs, serialized, ";");
for (auto &str : filterstrs) {
ret.emplace_back(str);
}
return ret;
}
static string serialize(const vector<vector<df::job_item_vector_id>> &vector_ids, const vector<ItemFilter> &item_filters) {
vector<string> joined;
for (auto &vec_list : vector_ids) {
joined.emplace_back(join_strings(",", vec_list));
}
return join_strings("|", joined);
std::ostringstream out;
out << join_strings(";", joined) << "|";
joined.clear();
for (auto &filter : item_filters) {
joined.emplace_back(filter.serialize());
}
out << join_strings(";", joined);
return out.str();
}
PlannedBuilding::PlannedBuilding(color_ostream &out, df::building *bld, HeatSafety heat)
: id(bld->id), vector_ids(get_vector_ids(out, id)), heat_safety(heat) {
PlannedBuilding::PlannedBuilding(color_ostream &out, df::building *bld, HeatSafety heat, const vector<ItemFilter> &item_filters)
: id(bld->id), vector_ids(get_vector_ids(out, id)), heat_safety(heat),
item_filters(item_filters) {
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);
set_config_val(bld_config, BLD_CONFIG_HEAT, heat_safety);
bld_config.val() = serialize(vector_ids);
bld_config.val() = serialize(vector_ids, item_filters);
DEBUG(status,out).print("serialized state for building %d: %s\n", id, bld_config.val().c_str());
}
PlannedBuilding::PlannedBuilding(color_ostream &out, PersistentDataItem &bld_config)
: id(get_config_val(bld_config, BLD_CONFIG_ID)),
vector_ids(deserialize(out, bld_config)),
vector_ids(deserialize_vector_ids(out, bld_config)),
heat_safety((HeatSafety)get_config_val(bld_config, BLD_CONFIG_HEAT)),
item_filters(deserialize_item_filters(out, bld_config)),
bld_config(bld_config) { }
// Ensure the building still exists and is in a valid state. It can disappear

@ -1,6 +1,7 @@
#pragma once
#include "buildingplan.h"
#include "itemfilter.h"
#include "Core.h"
@ -18,7 +19,9 @@ public:
const HeatSafety heat_safety;
PlannedBuilding(DFHack::color_ostream &out, df::building *bld, HeatSafety heat);
const std::vector<ItemFilter> item_filters;
PlannedBuilding(DFHack::color_ostream &out, df::building *bld, HeatSafety heat, const std::vector<ItemFilter> &item_filters);
PlannedBuilding(DFHack::color_ostream &out, DFHack::PersistentDataItem &bld_config);
void remove(DFHack::color_ostream &out);

@ -1292,7 +1292,7 @@ InspectorOverlay.ATTRS{
default_pos={x=-41,y=14},
default_enabled=true,
viewscreens='dwarfmode/ViewSheets/BUILDING',
frame={w=30, h=14},
frame={w=30, h=15},
frame_style=gui.MEDIUM_FRAME,
frame_background=gui.CLEAR_PEN,
}
@ -1308,12 +1308,12 @@ function InspectorOverlay:init()
InspectorLine{view_id='item3', frame={t=6, l=0}, idx=3},
InspectorLine{view_id='item4', frame={t=8, l=0}, idx=4},
widgets.HotkeyLabel{
frame={t=10, l=0},
frame={t=11, l=0},
label='adjust filters',
key='CUSTOM_CTRL_F',
},
widgets.HotkeyLabel{
frame={t=11, l=0},
frame={t=12, l=0},
label='make top priority',
key='CUSTOM_CTRL_T',
on_activate=self:callback('make_top_priority'),