prep buildingplan for core algorithm changes

Lots of refactoring and reorganizing, with only cosmetic player-visible changes.

- show quickfort mode hotlkey label regardless of whether the current building type has buildingplan enabled. before, it was only shown after the user enabled buildingplan for the current building. this eliminates the extra step when enabling quickfort mode, which force-enables all building types.
- changed signature of lua-exported isPlannableBuilding to take subtype
  and custom type in addition to building type. this is only used by
  quickfort, and it already sends all three params in preparation for
  this change
- added lua-exported scheduleCycle(), which is like doCycle(), but only
  takes effect on the next non-paused frame. this lets quickfort
  run only one buildingplan cycle regardless of how many #build
  blueprints were run
- declared a few dfhack library methods and params const so buildingplan
  could call them from const methods
- converted buildingplan internal debug logging fn to have a printf api
- reshaped buildingplan-planner API and refactored implementation in
  preparation for upcoming core algorithm changes for supporing all
  building types (no externally-visible functionality changes)
  - changed df::building_type params to type, subtype, custom tuple keys
  - introduced capability to return multiple filters per building type
    (though the current buildings all only have one filter per)
- split monolith hook functions in buildingplan.cpp into one per scope.
  this significantly cleans up the code and preps the hooks to handle
  iterating through multiple item filters.
- got rid of send_key function and replaced with better reporting of
  whether keys have been handled
develop
Myk Taylor 2020-10-16 13:52:23 -07:00
parent 895fa59c79
commit 82013c0c5e
11 changed files with 737 additions and 530 deletions

@ -3702,9 +3702,10 @@ buildingplan
Native functions:
* ``bool isPlannableBuilding(df::building_type type)`` returns whether the building type is handled by buildingplan
* ``bool isPlannableBuilding(df::building_type type, int16_t subtype, int32_t custom)`` returns whether the building type is handled by buildingplan
* ``void addPlannedBuilding(df::building *bld)`` suspends the building jobs and adds the building to the monitor list
* ``void doCycle()`` runs a check for whether buildlings in the monitor list can be assigned items and unsuspended. This method runs automatically twice a game day, so you only need to call it directly if you want buildingplan to do a check right now.
* ``void scheduleCycle()`` schedules a cycle to be run during the next non-paused game frame. Can be called multiple times while the game is paused and only one cycle will be scheduled.
burrows
=======

@ -136,8 +136,8 @@ namespace DFHack
return findProduct(info.material, name);
}
std::string getToken();
std::string toString(uint16_t temp = 10015, bool named = true);
std::string getToken() const;
std::string toString(uint16_t temp = 10015, bool named = true) const;
bool isAnyCloth();

@ -305,7 +305,7 @@ bool MaterialInfo::findProduct(df::material *material, const std::string &name)
return decode(-1);
}
std::string MaterialInfo::getToken()
std::string MaterialInfo::getToken() const
{
if (isNone())
return "NONE";
@ -333,7 +333,7 @@ std::string MaterialInfo::getToken()
}
}
std::string MaterialInfo::toString(uint16_t temp, bool named)
std::string MaterialInfo::toString(uint16_t temp, bool named) const
{
if (isNone())
return "any";

@ -1,16 +1,22 @@
#include "buildingplan-lib.h"
#include <cstdarg>
#include "Core.h"
using namespace DFHack;
bool show_debugging = false;
void debug(const std::string &msg)
void debug(const char *fmt, ...)
{
if (!show_debugging)
return;
color_ostream_proxy out(Core::getInstance().getConsole());
out << "DEBUG: " << msg << endl;
out.print("DEBUG(buildingplan): ");
va_list args;
va_start(args, fmt);
out.vprint(fmt, args);
va_end(args);
out.print("\n");
}

@ -3,6 +3,6 @@
#include "buildingplan-planner.h"
#include "buildingplan-rooms.h"
void debug(const std::string &msg);
void debug(const char *fmt, ...) Wformat(printf,1,2);
extern bool show_debugging;

@ -1,185 +1,269 @@
#include <functional>
#include <climits> // for CHAR_BIT
#include "df/building_design.h"
#include "df/building_doorst.h"
#include "df/building_type.h"
#include "df/general_ref_building_holderst.h"
#include "df/job_item.h"
#include "df/building_doorst.h"
#include "df/building_design.h"
#include "df/ui_build_selector.h"
#include "modules/Job.h"
#include "modules/Buildings.h"
#include "modules/Gui.h"
#include "modules/Job.h"
#include "uicommon.h"
#include "buildingplan-planner.h"
#include "buildingplan-lib.h"
#include "uicommon.h"
/*
* ItemFilter
*/
static const std::string planned_building_persistence_key_v1 = "buildingplan/constraints";
ItemFilter::ItemFilter() :
min_quality(df::item_quality::Ordinary),
max_quality(df::item_quality::Artifact),
decorated_only(false),
valid(true)
{
clear(); // mat_mask is not cleared by default (see issue #1047)
}
/*
* ItemFilter
*/
bool ItemFilter::matchesMask(DFHack::MaterialInfo &mat)
ItemFilter::ItemFilter()
{
return (mat_mask.whole) ? mat.matches(mat_mask) : true;
clear();
}
bool ItemFilter::matches(const df::dfhack_material_category mask) const
void ItemFilter::clear()
{
return mask.whole & mat_mask.whole;
min_quality = df::item_quality::Ordinary;
max_quality = df::item_quality::Masterful;
decorated_only = false;
clearMaterialMask();
materials.clear();
}
bool ItemFilter::matches(DFHack::MaterialInfo &material) const
bool ItemFilter::deserialize(PersistentDataItem &config)
{
for (auto it = materials.begin(); it != materials.end(); ++it)
if (material.matches(*it))
return true;
return false;
}
clear();
bool ItemFilter::matches(df::item *item)
{
if (item->getQuality() < min_quality || item->getQuality() > max_quality)
std::vector<std::string> tokens;
split_string(&tokens, config.val(), "/");
if (tokens.size() != 2)
{
debug("invalid ItemFilter serialization: '%s'", config.val().c_str());
return false;
}
if (decorated_only && !item->hasImprovements())
if (!deserializeMaterialMask(tokens[0]) || !deserializeMaterials(tokens[1]))
return false;
auto imattype = item->getActualMaterial();
auto imatindex = item->getActualMaterialIndex();
auto item_mat = DFHack::MaterialInfo(imattype, imatindex);
return (materials.size() == 0) ? matchesMask(item_mat) : matches(item_mat);
setMinQuality(config.ival(2) - 1);
setMaxQuality(config.ival(4) - 1);
decorated_only = config.ival(3) - 1;
return true;
}
std::string material_to_string_fn(DFHack::MaterialInfo m) { return m.toString(); }
bool ItemFilter::deserializeMaterialMask(std::string ser)
{
if (ser.empty())
return true;
std::vector<std::string> ItemFilter::getMaterialFilterAsVector()
if (!parseJobMaterialCategory(&mat_mask, ser))
{
debug("invalid job material category serialization: '%s'", ser.c_str());
return false;
}
return true;
}
bool ItemFilter::deserializeMaterials(std::string ser)
{
std::vector<std::string> descriptions;
if (ser.empty())
return true;
transform_(materials, descriptions, material_to_string_fn);
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("invalid material name serialization: '%s'", ser.c_str());
return false;
}
materials.push_back(material);
}
return true;
}
if (descriptions.size() == 0)
bitfield_to_string(&descriptions, mat_mask);
void ItemFilter::serialize(PersistentDataItem &config) 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();
}
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;
}
if (descriptions.size() == 0)
descriptions.push_back("any");
void ItemFilter::clearMaterialMask()
{
mat_mask.whole = 0;
}
return descriptions;
void ItemFilter::addMaterialMask(uint32_t mask)
{
mat_mask.whole |= mask;
}
std::string ItemFilter::getMaterialFilterAsSerial()
void ItemFilter::setMaterials(std::vector<DFHack::MaterialInfo> materials)
{
std::string str;
this->materials = materials;
}
str.append(bitfield_to_string(mat_mask, ","));
str.append("/");
if (materials.size() > 0)
static void clampItemQuality(df::item_quality *quality)
{
if (*quality > item_quality::Artifact)
{
for (size_t i = 0; i < materials.size(); i++)
str.append(materials[i].getToken() + ",");
if (str[str.size()-1] == ',')
str.resize(str.size () - 1);
debug("clamping quality to Artifact");
*quality = item_quality::Artifact;
}
if (*quality < item_quality::Ordinary)
{
debug("clamping quality to Ordinary");
*quality = item_quality::Ordinary;
}
}
return str;
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;
}
bool ItemFilter::parseSerializedMaterialTokens(std::string str)
void ItemFilter::setMaxQuality(int quality)
{
valid = false;
std::vector<std::string> tokens;
split_string(&tokens, str, "/");
max_quality = static_cast<df::item_quality>(quality);
clampItemQuality(&max_quality);
if (max_quality < min_quality)
min_quality = max_quality;
}
if (tokens.size() > 0 && !tokens[0].empty())
{
if (!parseJobMaterialCategory(&mat_mask, tokens[0]))
return false;
}
void ItemFilter::incMinQuality() { setMinQuality(min_quality + 1); }
void ItemFilter::decMinQuality() { setMinQuality(min_quality - 1); }
void ItemFilter::incMaxQuality() { setMaxQuality(max_quality + 1); }
void ItemFilter::decMaxQuality() { setMaxQuality(max_quality - 1); }
if (tokens.size() > 1 && !tokens[1].empty())
{
std::vector<std::string> mat_names;
split_string(&mat_names, tokens[1], ",");
for (auto m = mat_names.begin(); m != mat_names.end(); m++)
{
DFHack::MaterialInfo material;
if (!material.find(*m) || !material.isValid())
return false;
void ItemFilter::toggleDecoratedOnly() { decorated_only = !decorated_only; }
materials.push_back(material);
}
}
static std::string material_to_string_fn(const MaterialInfo &m) { return m.toString(); }
valid = true;
return true;
uint32_t ItemFilter::getMaterialMask() const { return mat_mask.whole; }
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;
}
std::string ItemFilter::getMinQuality()
std::string ItemFilter::getMinQuality() const
{
return ENUM_KEY_STR(item_quality, min_quality);
}
std::string ItemFilter::getMaxQuality()
std::string ItemFilter::getMaxQuality() const
{
return ENUM_KEY_STR(item_quality, max_quality);
}
bool ItemFilter::isValid()
bool ItemFilter::getDecoratedOnly() const
{
return valid;
return decorated_only;
}
void ItemFilter::clear()
bool ItemFilter::matchesMask(DFHack::MaterialInfo &mat) const
{
mat_mask.whole = 0;
materials.clear();
return mat_mask.whole ? mat.matches(mat_mask) : true;
}
/*
* PlannedBuilding
*/
bool ItemFilter::matches(df::dfhack_material_category mask) const
{
return mask.whole & mat_mask.whole;
}
PlannedBuilding::PlannedBuilding(df::building *building, ItemFilter *filter)
bool ItemFilter::matches(DFHack::MaterialInfo &material) const
{
this->building = building;
this->filter = *filter;
pos = df::coord(building->centerx, building->centery, building->z);
config = DFHack::World::AddPersistentData("buildingplan/constraints");
config.val() = filter->getMaterialFilterAsSerial();
config.ival(1) = building->id;
config.ival(2) = filter->min_quality + 1;
config.ival(3) = static_cast<int>(filter->decorated_only) + 1;
config.ival(4) = filter->max_quality + 1;
for (auto it = materials.begin(); it != materials.end(); ++it)
if (material.matches(*it))
return true;
return false;
}
PlannedBuilding::PlannedBuilding(PersistentDataItem &config, color_ostream &out)
bool ItemFilter::matches(df::item *item) const
{
this->config = config;
if (item->getQuality() < min_quality || item->getQuality() > max_quality)
return false;
if (!filter.parseSerializedMaterialTokens(config.val()))
{
out.printerr("Buildingplan: Cannot parse filter: %s\nDiscarding.", config.val().c_str());
return;
}
if (decorated_only && !item->hasImprovements())
return false;
building = df::building::find(config.ival(1));
if (!building)
return;
auto imattype = item->getActualMaterial();
auto imatindex = item->getActualMaterialIndex();
auto item_mat = DFHack::MaterialInfo(imattype, imatindex);
return (materials.size() == 0) ? matchesMask(item_mat) : matches(item_mat);
}
/*
* PlannedBuilding
*/
pos = df::coord(building->centerx, building->centery, building->z);
filter.min_quality = static_cast<df::item_quality>(config.ival(2) - 1);
filter.max_quality = static_cast<df::item_quality>(config.ival(4) - 1);
filter.decorated_only = config.ival(3) - 1;
static std::vector<ItemFilter> deserializeFilters(PersistentDataItem &config)
{
// simplified implementation while we can assume there is only one filter
std::vector<ItemFilter> ret;
ItemFilter itemFilter;
itemFilter.deserialize(config);
ret.push_back(itemFilter);
return ret;
}
static size_t getNumFilters(BuildingTypeKey key)
{
// TODO: get num filters in Lua when we handle all building types
return 1;
}
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_v1);
config.ival(1) = building_id;
// assume all filter vectors are length 1 for now
filters[0].serialize(config);
}
PlannedBuilding::PlannedBuilding(PersistentDataItem &config)
: config(config),
building(df::building::find(config.ival(1))),
building_id(config.ival(1)),
filters(deserializeFilters(config))
{ }
bool PlannedBuilding::assignClosestItem(std::vector<df::item *> *items_vector)
{
decltype(items_vector->begin()) closest_item;
@ -187,7 +271,7 @@ bool PlannedBuilding::assignClosestItem(std::vector<df::item *> *items_vector)
for (auto item_iter = items_vector->begin(); item_iter != items_vector->end(); item_iter++)
{
auto item = *item_iter;
if (!filter.matches(item))
if (!filters[0].matches(item))
continue;
auto pos = item->pos;
@ -255,68 +339,83 @@ bool PlannedBuilding::assignItem(df::item *item)
return true;
}
bool PlannedBuilding::isValid()
// 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.
bool PlannedBuilding::isValid() const
{
bool valid = filter.isValid() &&
building && Buildings::findAtTile(pos) == building &&
building->getBuildStage() == 0;
if (!valid)
remove();
return valid;
return building && df::building::find(building_id)
&& building->getBuildStage() == 0;
}
df::building_type PlannedBuilding::getType()
void PlannedBuilding::remove()
{
return building->getType();
DFHack::World::DeletePersistentData(config);
building = NULL;
}
bool PlannedBuilding::isCurrentlySelectedBuilding()
df::building * PlannedBuilding::getBuilding()
{
return isValid() && (building == df::global::world->selected_building);
return building;
}
ItemFilter *PlannedBuilding::getFilter()
const std::vector<ItemFilter> & PlannedBuilding::getFilters() const
{
return &filter;
return filters;
}
void PlannedBuilding::remove()
/*
* BuildingTypeKey
*/
BuildingTypeKey toBuildingTypeKey(
df::building_type btype, int16_t subtype, int32_t custom)
{
DFHack::World::DeletePersistentData(config);
return std::make_tuple(btype, subtype, custom);
}
/*
* Planner
*/
BuildingTypeKey toBuildingTypeKey(df::building *bld)
{
return std::make_tuple(
bld->getType(), bld->getSubtype(), bld->getCustomType());
}
Planner::Planner() { }
BuildingTypeKey toBuildingTypeKey(df::ui_build_selector *uibs)
{
return std::make_tuple(
uibs->building_type, uibs->building_subtype, uibs->custom_type);
}
bool Planner::isPlannableBuilding(const df::building_type type) const
// rotates a size_t value left by count bits
// assumes count is not 0 or >= size_t_bits
// replace this with std::rotl when we move to C++20
static std::size_t rotl_size_t(size_t val, uint32_t count)
{
return item_for_building_type.find(type) != item_for_building_type.end();
static const int size_t_bits = CHAR_BIT * sizeof(std::size_t);
return val << count | val >> (size_t_bits - count);
}
void Planner::reset(color_ostream &out)
std::size_t BuildingTypeKeyHash::operator() (const BuildingTypeKey & key) const
{
planned_buildings.clear();
std::vector<PersistentDataItem> items;
DFHack::World::GetPersistentData(&items, "buildingplan/constraints");
// cast first param to appease gcc-4.8, which is missing the enum
// specializations for std::hash
std::size_t h1 = std::hash<int32_t>()(static_cast<int32_t>(std::get<0>(key)));
std::size_t h2 = std::hash<int16_t>()(std::get<1>(key));
std::size_t h3 = std::hash<int32_t>()(std::get<2>(key));
for (auto i = items.begin(); i != items.end(); i++)
{
PlannedBuilding pb(*i, out);
if (pb.isValid())
planned_buildings.push_back(pb);
}
return h1 ^ rotl_size_t(h2, 8) ^ rotl_size_t(h3, 16);
}
/*
* Planner
*/
void Planner::initialize()
{
#define add_building_type(btype, itype) \
item_for_building_type[df::building_type::btype] = df::item_type::itype; \
default_item_filters[df::building_type::btype] = ItemFilter(); \
available_item_vectors[df::item_type::itype] = std::vector<df::item *>(); \
is_relevant_item_type[df::item_type::itype] = true; \
@ -354,53 +453,82 @@ void Planner::initialize()
#undef add_building_type
}
void Planner::addPlannedBuilding(df::building *bld)
void Planner::reset()
{
for (auto iter = bld->jobs.begin(); iter != bld->jobs.end(); iter++)
debug("resetting Planner state");
default_item_filters.clear();
planned_buildings.clear();
std::vector<PersistentDataItem> items;
DFHack::World::GetPersistentData(&items, planned_building_persistence_key_v1);
debug("found data for %zu planned buildings", items.size());
for (auto i = items.begin(); i != items.end(); i++)
{
(*iter)->flags.bits.suspend = true;
}
PlannedBuilding pb(*i);
if (!pb.isValid())
{
pb.remove();
continue;
}
PlannedBuilding pb(bld, &default_item_filters[bld->getType()]);
planned_buildings.push_back(pb);
planned_buildings.push_back(pb);
}
}
void Planner::doCycle()
void Planner::addPlannedBuilding(df::building *bld)
{
debug("Running Cycle");
if (planned_buildings.size() == 0)
auto item_filters = getItemFilters(toBuildingTypeKey(bld)).get();
// not a supported type
if (item_filters.empty())
{
debug("failed to add building: unsupported type");
return;
}
debug("Planned count: " + int_to_string(planned_buildings.size()));
// protect against multiple registrations
if (getPlannedBuilding(bld))
{
debug("building already registered");
return;
}
gather_available_items();
for (auto building_iter = planned_buildings.begin(); building_iter != planned_buildings.end();)
PlannedBuilding pb(bld, item_filters);
if (pb.isValid())
{
if (building_iter->isValid())
{
if (show_debugging)
debug(std::string("Trying to allocate ") + enum_item_key_str(building_iter->getType()));
for (auto job : bld->jobs)
job->flags.bits.suspend = true;
auto required_item_type = item_for_building_type[building_iter->getType()];
auto items_vector = &available_item_vectors[required_item_type];
if (items_vector->size() == 0 || !building_iter->assignClosestItem(items_vector))
{
debug("Unable to allocate an item");
++building_iter;
continue;
}
}
debug("Removing building plan");
building_iter = planned_buildings.erase(building_iter);
planned_buildings.push_back(pb);
}
else
{
pb.remove();
}
}
PlannedBuilding * Planner::getPlannedBuilding(df::building *bld)
{
for (auto & pb : planned_buildings)
{
if (pb.getBuilding() == bld)
return &pb;
}
return NULL;
}
bool Planner::isPlannableBuilding(BuildingTypeKey key)
{
return item_for_building_type.count(std::get<0>(key)) > 0;
}
bool Planner::allocatePlannedBuilding(df::building_type type)
bool Planner::allocatePlannedBuilding(BuildingTypeKey key)
{
coord32_t cursor;
if (!DFHack::Gui::getCursorCoords(cursor.x, cursor.y, cursor.z))
return false;
auto type = std::get<0>(key);
auto newinst = Buildings::allocInstance(cursor.get_coord16(), type);
if (!newinst)
return false;
@ -430,53 +558,48 @@ bool Planner::allocatePlannedBuilding(df::building_type type)
return true;
}
PlannedBuilding *Planner::getSelectedPlannedBuilding()
Planner::ItemFiltersWrapper Planner::getItemFilters(BuildingTypeKey key)
{
for (auto building_iter = planned_buildings.begin(); building_iter != planned_buildings.end(); building_iter++)
{
if (building_iter->isCurrentlySelectedBuilding())
{
return &(*building_iter);
}
}
static std::vector<ItemFilter> empty_vector;
static const ItemFiltersWrapper empty_ret(empty_vector);
return nullptr;
size_t nfilters = getNumFilters(key);
if (nfilters < 1)
return empty_ret;
while (default_item_filters[key].size() < nfilters)
default_item_filters[key].push_back(ItemFilter());
return ItemFiltersWrapper(default_item_filters[key]);
}
void Planner::removeSelectedPlannedBuilding() { getSelectedPlannedBuilding()->remove(); }
ItemFilter *Planner::getDefaultItemFilterForType(df::building_type type) { return &default_item_filters[type]; }
void Planner::adjustMinQuality(df::building_type type, int amount)
void Planner::doCycle()
{
auto min_quality = &getDefaultItemFilterForType(type)->min_quality;
*min_quality = static_cast<df::item_quality>(*min_quality + amount);
boundsCheckItemQuality(min_quality);
auto max_quality = &getDefaultItemFilterForType(type)->max_quality;
if (*min_quality > *max_quality)
(*max_quality) = *min_quality;
debug("Running Cycle");
if (planned_buildings.size() == 0)
return;
}
debug("Planned count: %zu", planned_buildings.size());
void Planner::adjustMaxQuality(df::building_type type, int amount)
{
auto max_quality = &getDefaultItemFilterForType(type)->max_quality;
*max_quality = static_cast<df::item_quality>(*max_quality + amount);
boundsCheckItemQuality(max_quality);
auto min_quality = &getDefaultItemFilterForType(type)->min_quality;
if (*max_quality < *min_quality)
(*min_quality) = *max_quality;
}
gather_available_items();
for (auto building_iter = planned_buildings.begin(); building_iter != planned_buildings.end();)
{
if (building_iter->isValid())
{
auto type = building_iter->getBuilding()->getType();
debug("Trying to allocate %s", enum_item_key_str(type));
void Planner::boundsCheckItemQuality(item_quality::item_quality *quality)
{
*quality = static_cast<df::item_quality>(*quality);
if (*quality > item_quality::Artifact)
(*quality) = item_quality::Artifact;
if (*quality < item_quality::Ordinary)
(*quality) = item_quality::Ordinary;
auto required_item_type = item_for_building_type[type];
auto items_vector = &available_item_vectors[required_item_type];
if (items_vector->size() == 0 || !building_iter->assignClosestItem(items_vector))
{
debug("Unable to allocate an item");
++building_iter;
continue;
}
}
debug("Removing building plan");
building_iter->remove();
building_iter = planned_buildings.erase(building_iter);
}
}
void Planner::gather_available_items()

@ -1,99 +1,131 @@
#pragma once
#include "df/item_quality.h"
#include <unordered_map>
#include "df/building.h"
#include "df/dfhack_material_category.h"
#include "df/item_quality.h"
#include "df/job_item.h"
#include "modules/Materials.h"
#include "modules/Persistence.h"
struct ItemFilter
class ItemFilter
{
public:
ItemFilter();
void clear();
bool deserialize(DFHack::PersistentDataItem &config);
void serialize(DFHack::PersistentDataItem &config) const;
void addMaterialMask(uint32_t mask);
void clearMaterialMask();
void setMaterials(std::vector<DFHack::MaterialInfo> materials);
void incMinQuality();
void decMinQuality();
void incMaxQuality();
void decMaxQuality();
void toggleDecoratedOnly();
uint32_t getMaterialMask() const;
std::vector<std::string> getMaterials() const;
std::string getMinQuality() const;
std::string getMaxQuality() const;
bool getDecoratedOnly() const;
bool matches(df::dfhack_material_category mask) const;
bool matches(DFHack::MaterialInfo &material) const;
bool matches(df::item *item) const;
private:
df::dfhack_material_category mat_mask;
std::vector<DFHack::MaterialInfo> materials;
df::item_quality min_quality;
df::item_quality max_quality;
bool decorated_only;
ItemFilter();
bool matchesMask(DFHack::MaterialInfo &mat);
bool matches(const df::dfhack_material_category mask) const;
bool matches(DFHack::MaterialInfo &material) const;
bool matches(df::item *item);
std::vector<std::string> getMaterialFilterAsVector();
std::string getMaterialFilterAsSerial();
bool parseSerializedMaterialTokens(std::string str);
std::string getMinQuality();
std::string getMaxQuality();
bool isValid();
void clear();
private:
bool valid;
bool deserializeMaterialMask(std::string ser);
bool deserializeMaterials(std::string ser);
void setMinQuality(int quality);
void setMaxQuality(int quality);
bool matchesMask(DFHack::MaterialInfo &mat) const;
};
class PlannedBuilding
{
public:
PlannedBuilding(df::building *building, ItemFilter *filter);
PlannedBuilding(DFHack::PersistentDataItem &config, DFHack::color_ostream &out);
PlannedBuilding(df::building *building, const std::vector<ItemFilter> &filters);
PlannedBuilding(DFHack::PersistentDataItem &config);
bool assignClosestItem(std::vector<df::item *> *items_vector);
bool assignItem(df::item *item);
bool isValid();
bool isValid() const;
void remove();
df::building_type getType();
bool isCurrentlySelectedBuilding();
ItemFilter *getFilter();
df::building * getBuilding();
const std::vector<ItemFilter> & getFilters() const;
private:
df::building *building;
DFHack::PersistentDataItem config;
df::coord pos;
ItemFilter filter;
df::building *building;
df::building::key_field_type building_id;
std::vector<ItemFilter> filters;
};
// building type, subtype, custom
typedef std::tuple<df::building_type, int16_t, int32_t> BuildingTypeKey;
BuildingTypeKey toBuildingTypeKey(
df::building_type btype, int16_t subtype, int32_t custom);
BuildingTypeKey toBuildingTypeKey(df::building *bld);
BuildingTypeKey toBuildingTypeKey(df::ui_build_selector *uibs);
struct BuildingTypeKeyHash
{
std::size_t operator() (const BuildingTypeKey & key) const;
};
class Planner
{
public:
Planner();
bool isPlannableBuilding(const df::building_type type) const;
void reset(DFHack::color_ostream &out);
class ItemFiltersWrapper
{
public:
ItemFiltersWrapper(std::vector<ItemFilter> & item_filters)
: item_filters(item_filters) { }
std::vector<ItemFilter>::reverse_iterator rbegin() const { return item_filters.rbegin(); }
std::vector<ItemFilter>::reverse_iterator rend() const { return item_filters.rend(); }
const std::vector<ItemFilter> & get() const { return item_filters; }
private:
std::vector<ItemFilter> &item_filters;
};
void initialize();
void reset();
bool allocatePlannedBuilding(BuildingTypeKey key);
void addPlannedBuilding(df::building *bld);
PlannedBuilding *getPlannedBuilding(df::building *bld);
void doCycle();
bool allocatePlannedBuilding(df::building_type type);
PlannedBuilding *getSelectedPlannedBuilding();
bool isPlannableBuilding(BuildingTypeKey key);
void removeSelectedPlannedBuilding();
// returns an empty vector if the type is not supported
ItemFiltersWrapper getItemFilters(BuildingTypeKey key);
ItemFilter *getDefaultItemFilterForType(df::building_type type);
void adjustMinQuality(df::building_type type, int amount);
void adjustMaxQuality(df::building_type type, int amount);
void doCycle();
private:
std::map<df::building_type, df::item_type> item_for_building_type;
std::map<df::building_type, ItemFilter> default_item_filters;
std::unordered_map<BuildingTypeKey,
std::vector<ItemFilter>,
BuildingTypeKeyHash> default_item_filters;
std::map<df::item_type, std::vector<df::item *>> available_item_vectors;
std::map<df::item_type, bool> is_relevant_item_type; //Needed for fast check when looping over all items
std::vector<PlannedBuilding> planned_buildings;
void boundsCheckItemQuality(df::enums::item_quality::item_quality *quality);
void gather_available_items();
};

@ -1,5 +1,5 @@
#include "buildingplan-lib.h"
#include <unordered_map>
#include "df/entity_position.h"
#include "df/interface_key.h"
#include "df/ui_build_selector.h"
@ -14,6 +14,7 @@
#include "uicommon.h"
#include "listcolumn.h"
#include "buildingplan-lib.h"
DFHACK_PLUGIN("buildingplan");
#define PLUGIN_VERSION 0.15
@ -24,18 +25,15 @@ REQUIRE_GLOBAL(world);
#define MAX_MASK 10
#define MAX_MATERIAL 21
using namespace DFHack;
using namespace df::enums;
bool show_help = false;
bool quickfort_mode = false;
bool in_dummy_screen = false;
std::map<df::building_type, bool> planmode_enabled;
std::unordered_map<BuildingTypeKey, bool, BuildingTypeKeyHash> planmode_enabled;
class ViewscreenChooseMaterial : public dfhack_viewscreen
{
public:
ViewscreenChooseMaterial(ItemFilter *filter);
ViewscreenChooseMaterial(ItemFilter &filter);
void feed(set<df::interface_key> *input);
@ -47,14 +45,12 @@ private:
ListColumn<df::dfhack_material_category> masks_column;
ListColumn<MaterialInfo> materials_column;
int selected_column;
ItemFilter *filter;
df::building_type btype;
ItemFilter &filter;
void addMaskEntry(df::dfhack_material_category &mask, const std::string &text)
{
auto entry = ListEntry<df::dfhack_material_category>(pad_string(text, MAX_MASK, false), mask);
if (filter->matches(mask))
if (filter.matches(mask))
entry.selected = true;
masks_column.add(entry);
@ -131,7 +127,7 @@ private:
material.decode(DFHack::MaterialInfo::PLANT_BASE+j, i);
auto name = material.toString();
ListEntry<MaterialInfo> entry(pad_string(name, MAX_MATERIAL, false), material);
if (filter->matches(material))
if (filter.matches(material))
entry.selected = true;
materials_column.add(entry);
@ -147,7 +143,7 @@ private:
if (!selected_category.whole || material.matches(selected_category))
{
ListEntry<MaterialInfo> entry(pad_string(name, MAX_MATERIAL, false), material);
if (filter->matches(material))
if (filter.matches(material))
entry.selected = true;
materials_column.add(entry);
@ -167,9 +163,10 @@ private:
}
};
DFHack::MaterialInfo &material_info_identity_fn(DFHack::MaterialInfo &m) { return m; }
const DFHack::MaterialInfo &material_info_identity_fn(const DFHack::MaterialInfo &m) { return m; }
ViewscreenChooseMaterial::ViewscreenChooseMaterial(ItemFilter *filter)
ViewscreenChooseMaterial::ViewscreenChooseMaterial(ItemFilter &filter)
: filter(filter)
{
selected_column = 0;
masks_column.setTitle("Type");
@ -179,7 +176,6 @@ ViewscreenChooseMaterial::ViewscreenChooseMaterial(ItemFilter *filter)
materials_column.left_margin = MAX_MASK + 3;
materials_column.setTitle("Material");
materials_column.multiselect = true;
this->filter = filter;
masks_column.changeHighlight(0);
@ -217,7 +213,7 @@ void ViewscreenChooseMaterial::feed(set<df::interface_key> *input)
}
if (input->count(interface_key::CUSTOM_SHIFT_C))
{
filter->clear();
filter.clear();
masks_column.clearSelection();
materials_column.clearSelection();
populateMaterials();
@ -225,17 +221,18 @@ void ViewscreenChooseMaterial::feed(set<df::interface_key> *input)
else if (input->count(interface_key::SEC_SELECT))
{
// Convert list selections to material filters
filter->mat_mask.whole = 0;
filter->materials.clear();
filter.clearMaterialMask();
// Category masks
auto masks = masks_column.getSelectedElems();
for (auto it = masks.begin(); it != masks.end(); ++it)
filter->mat_mask.whole |= it->whole;
filter.addMaterialMask(it->whole);
// Specific materials
auto materials = materials_column.getSelectedElems();
transform_(materials, filter->materials, material_info_identity_fn);
std::vector<DFHack::MaterialInfo> materialInfos;
transform_(materials, materialInfos, material_info_identity_fn);
filter.setMaterials(materialInfos);
Screen::dismiss(this);
}
@ -285,179 +282,167 @@ void ViewscreenChooseMaterial::render()
}
//START Viewscreen Hook
static bool is_planmode_enabled(df::building_type type)
static bool is_planmode_enabled(BuildingTypeKey key)
{
return planmode_enabled[type] || quickfort_mode;
return planmode_enabled[key] || quickfort_mode;
}
struct buildingplan_hook : public df::viewscreen_dwarfmodest
struct buildingplan_query_hook : public df::viewscreen_dwarfmodest
{
//START UI Methods
typedef df::viewscreen_dwarfmodest interpose_base;
void send_key(const df::interface_key &key)
{
set< df::interface_key > keys;
keys.insert(key);
this->feed(&keys);
}
bool isInPlannedBuildingQueryMode()
{
return (ui->main.mode == df::ui_sidebar_mode::QueryBuilding ||
ui->main.mode == df::ui_sidebar_mode::BuildingItems) &&
planner.getSelectedPlannedBuilding();
planner.getPlannedBuilding(world->selected_building);
}
bool isInPlannedBuildingPlacementMode()
bool handleInput(set<df::interface_key> *input)
{
return ui->main.mode == ui_sidebar_mode::Build &&
df::global::ui_build_selector &&
df::global::ui_build_selector->stage < 2 &&
planner.isPlannableBuilding(ui_build_selector->building_type);
}
if (!isInPlannedBuildingQueryMode())
return false;
std::vector<Units::NoblePosition> getNoblePositionOfSelectedBuildingOwner()
{
std::vector<Units::NoblePosition> np;
if (ui->main.mode != df::ui_sidebar_mode::QueryBuilding ||
!world->selected_building ||
!world->selected_building->owner)
if (input->count(interface_key::SUSPENDBUILDING))
return true; // Don't unsuspend planned buildings
if (input->count(interface_key::DESTROYBUILDING))
{
return np;
// remove persistent data and allow the parent to handle the key
// so the building is removed
planner.getPlannedBuilding(world->selected_building)->remove();
}
switch (world->selected_building->getType())
{
case building_type::Bed:
case building_type::Chair:
case building_type::Table:
break;
default:
return np;
}
return false;
}
return getUniqueNoblePositions(world->selected_building->owner);
DEFINE_VMETHOD_INTERPOSE(void, feed, (set<df::interface_key> *input))
{
if (!handleInput(input))
INTERPOSE_NEXT(feed)(input);
}
bool isInNobleRoomQueryMode()
DEFINE_VMETHOD_INTERPOSE(void, render, ())
{
if (getNoblePositionOfSelectedBuildingOwner().size() > 0)
return canReserveRoom(world->selected_building);
else
return false;
INTERPOSE_NEXT(render)();
if (!isInPlannedBuildingQueryMode())
return;
// Hide suspend toggle option
auto dims = Gui::getDwarfmodeViewDims();
int left_margin = dims.menu_x1 + 1;
int x = left_margin;
int y = 20;
Screen::Pen pen(' ', COLOR_BLACK);
Screen::fillRect(pen, x, y, dims.menu_x2, y);
// all current buildings only have one filter
auto & filter = planner.getPlannedBuilding(world->selected_building)->getFilters()[0];
y = 24;
OutputString(COLOR_WHITE, x, y, "Planned Building Filter", true, left_margin);
++y;
OutputString(COLOR_BROWN, x, y, "Min Quality: ", false, left_margin);
OutputString(COLOR_BLUE, x, y, filter.getMinQuality(), true, left_margin);
OutputString(COLOR_BROWN, x, y, "Max Quality: ", false, left_margin);
OutputString(COLOR_BLUE, x, y, filter.getMaxQuality(), true, left_margin);
if (filter.getDecoratedOnly())
OutputString(COLOR_BLUE, x, y, "Decorated Only", true, left_margin);
OutputString(COLOR_BROWN, x, y, "Materials:", true, left_margin);
auto filters = filter.getMaterials();
for (auto it = filters.begin(); it != filters.end(); ++it)
OutputString(COLOR_BLUE, x, y, "*" + *it, true, left_margin);
}
};
struct buildingplan_place_hook : public df::viewscreen_dwarfmodest
{
typedef df::viewscreen_dwarfmodest interpose_base;
bool isInPlannedBuildingPlacementMode()
{
return ui->main.mode == ui_sidebar_mode::Build &&
df::global::ui_build_selector &&
df::global::ui_build_selector->stage < 2 &&
planner.isPlannableBuilding(toBuildingTypeKey(ui_build_selector));
}
bool handleInput(set<df::interface_key> *input)
{
if (isInPlannedBuildingPlacementMode())
if (!isInPlannedBuildingPlacementMode())
{
auto type = ui_build_selector->building_type;
if (input->count(interface_key::CUSTOM_SHIFT_P))
{
planmode_enabled[type] = !planmode_enabled[type];
if (!is_planmode_enabled(type))
{
Gui::refreshSidebar();
in_dummy_screen = false;
}
return true;
}
else if (input->count(interface_key::CUSTOM_P) ||
input->count(interface_key::CUSTOM_F) ||
input->count(interface_key::CUSTOM_D) ||
input->count(interface_key::CUSTOM_N))
{
show_help = true;
}
show_help = false;
return false;
}
if (is_planmode_enabled(type))
if (in_dummy_screen)
{
if (input->count(interface_key::SELECT) || input->count(interface_key::SEC_SELECT)
|| input->count(interface_key::LEAVESCREEN))
{
if (quickfort_mode && in_dummy_screen)
{
if (input->count(interface_key::SELECT) || input->count(interface_key::SEC_SELECT)
|| input->count(interface_key::LEAVESCREEN))
{
in_dummy_screen = false;
send_key(interface_key::LEAVESCREEN);
}
return true;
}
if (input->count(interface_key::SELECT))
{
if (ui_build_selector->errors.size() == 0 && planner.allocatePlannedBuilding(type))
{
Gui::refreshSidebar();
if (quickfort_mode)
{
in_dummy_screen = true;
}
}
return true;
}
else if (input->count(interface_key::CUSTOM_SHIFT_F))
{
quickfort_mode = !quickfort_mode;
}
else if (input->count(interface_key::CUSTOM_SHIFT_M))
{
Screen::show(dts::make_unique<ViewscreenChooseMaterial>(planner.getDefaultItemFilterForType(type)), plugin_self);
}
else if (input->count(interface_key::CUSTOM_Q))
{
planner.adjustMinQuality(type, -1);
}
else if (input->count(interface_key::CUSTOM_W))
{
planner.adjustMinQuality(type, 1);
}
else if (input->count(interface_key::CUSTOM_SHIFT_Q))
{
planner.adjustMaxQuality(type, -1);
}
else if (input->count(interface_key::CUSTOM_SHIFT_W))
{
planner.adjustMaxQuality(type, 1);
}
else if (input->count(interface_key::CUSTOM_SHIFT_D))
{
planner.getDefaultItemFilterForType(type)->decorated_only =
!planner.getDefaultItemFilterForType(type)->decorated_only;
}
in_dummy_screen = false;
// pass LEAVESCREEN up to parent view
input->clear();
input->insert(interface_key::LEAVESCREEN);
return false;
}
return true;
}
else if (isInPlannedBuildingQueryMode())
if (input->count(interface_key::CUSTOM_P) ||
input->count(interface_key::CUSTOM_F) ||
input->count(interface_key::CUSTOM_D) ||
input->count(interface_key::CUSTOM_M))
{
if (input->count(interface_key::SUSPENDBUILDING))
{
return true; // Don't unsuspend planned buildings
}
else if (input->count(interface_key::DESTROYBUILDING))
{
planner.removeSelectedPlannedBuilding(); // Remove persistent data
}
show_help = true;
}
BuildingTypeKey key = toBuildingTypeKey(ui_build_selector);
if (input->count(interface_key::CUSTOM_SHIFT_P))
{
planmode_enabled[key] = !planmode_enabled[key];
if (!is_planmode_enabled(key))
Gui::refreshSidebar();
return true;
}
else if (isInNobleRoomQueryMode())
if (input->count(interface_key::CUSTOM_SHIFT_F))
{
if (Gui::inRenameBuilding())
return false;
auto np = getNoblePositionOfSelectedBuildingOwner();
df::interface_key last_token = get_string_key(input);
if (last_token >= interface_key::STRING_A048 && last_token <= interface_key::STRING_A058)
quickfort_mode = !quickfort_mode;
return true;
}
if (!is_planmode_enabled(key))
return false;
if (input->count(interface_key::SELECT))
{
if (ui_build_selector->errors.size() == 0 && planner.allocatePlannedBuilding(key))
{
size_t selection = last_token - interface_key::STRING_A048;
if (np.size() < selection)
return false;
roomMonitor.toggleRoomForPosition(world->selected_building->id, np.at(selection-1).position->code);
return true;
Gui::refreshSidebar();
if (quickfort_mode)
in_dummy_screen = true;
}
return true;
}
return false;
// all current buildings only have one filter
auto filter = planner.getItemFilters(key).rbegin();
if (input->count(interface_key::CUSTOM_SHIFT_M))
Screen::show(dts::make_unique<ViewscreenChooseMaterial>(*filter), plugin_self);
else if (input->count(interface_key::CUSTOM_Q))
filter->decMinQuality();
else if (input->count(interface_key::CUSTOM_W))
filter->incMinQuality();
else if (input->count(interface_key::CUSTOM_SHIFT_Q))
filter->decMaxQuality();
else if (input->count(interface_key::CUSTOM_SHIFT_W))
filter->incMaxQuality();
else if (input->count(interface_key::CUSTOM_SHIFT_D))
filter->toggleDecoratedOnly();
else
return false;
return true;
}
DEFINE_VMETHOD_INTERPOSE(void, feed, (set<df::interface_key> *input))
@ -469,133 +454,176 @@ struct buildingplan_hook : public df::viewscreen_dwarfmodest
DEFINE_VMETHOD_INTERPOSE(void, render, ())
{
bool plannable = isInPlannedBuildingPlacementMode();
if (plannable && is_planmode_enabled(ui_build_selector->building_type))
BuildingTypeKey key = toBuildingTypeKey(ui_build_selector);
if (plannable && is_planmode_enabled(key))
{
if (ui_build_selector->stage < 1)
{
// No materials but turn on cursor
ui_build_selector->stage = 1;
}
for (auto iter = ui_build_selector->errors.begin(); iter != ui_build_selector->errors.end();)
for (auto iter = ui_build_selector->errors.begin();
iter != ui_build_selector->errors.end();)
{
//FIXME Hide bags
if (((*iter)->find("Needs") != string::npos && **iter != "Needs adjacent wall") ||
(*iter)->find("No access") != string::npos)
{
// FIXME Hide bags
if (((*iter)->find("Needs") != string::npos
&& **iter != "Needs adjacent wall")
|| (*iter)->find("No access") != string::npos)
iter = ui_build_selector->errors.erase(iter);
}
else
{
++iter;
}
}
}
INTERPOSE_NEXT(render)();
if (!plannable)
return;
auto dims = Gui::getDwarfmodeViewDims();
int left_margin = dims.menu_x1 + 1;
int x = left_margin;
auto type = ui_build_selector->building_type;
if (plannable)
if (in_dummy_screen)
{
if (quickfort_mode && in_dummy_screen)
{
Screen::Pen pen(' ',COLOR_BLACK);
int y = dims.y1 + 1;
Screen::fillRect(pen, x, y, dims.menu_x2, y + 20);
Screen::Pen pen(' ',COLOR_BLACK);
int y = dims.y1 + 1;
Screen::fillRect(pen, x, y, dims.menu_x2, y + 20);
++y;
++y;
OutputString(COLOR_BROWN, x, y, "Quickfort Placeholder", true, left_margin);
OutputString(COLOR_WHITE, x, y, "Enter, Shift-Enter or Esc", true, left_margin);
}
else
{
int y = 23;
OutputString(COLOR_BROWN, x, y,
"Placeholder for legacy Quickfort. This screen is not required for DFHack native quickfort.",
true, left_margin);
OutputString(COLOR_WHITE, x, y, "Enter, Shift-Enter or Esc", true, left_margin);
return;
}
if (show_help)
{
OutputString(COLOR_BROWN, x, y, "Note: ");
OutputString(COLOR_WHITE, x, y, "Use Shift-Keys here", true, left_margin);
}
OutputToggleString(x, y, "Planning Mode", "P", is_planmode_enabled(type), true, left_margin);
int y = 23;
if (is_planmode_enabled(type))
{
OutputToggleString(x, y, "Quickfort Mode", "F", quickfort_mode, true, left_margin);
if (show_help)
{
OutputString(COLOR_BROWN, x, y, "Note: ");
OutputString(COLOR_WHITE, x, y, "Use Shift-Keys here", true, left_margin);
}
auto filter = planner.getDefaultItemFilterForType(type);
OutputToggleString(x, y, "Planning Mode", "P", planmode_enabled[key], true, left_margin);
OutputToggleString(x, y, "Quickfort Mode", "F", quickfort_mode, true, left_margin);
OutputHotkeyString(x, y, "Min Quality: ", "qw");
OutputString(COLOR_BROWN, x, y, filter->getMinQuality(), true, left_margin);
if (!is_planmode_enabled(key))
return;
OutputHotkeyString(x, y, "Max Quality: ", "QW");
OutputString(COLOR_BROWN, x, y, filter->getMaxQuality(), true, left_margin);
auto filter = planner.getItemFilters(key).rbegin();
y += 2;
OutputHotkeyString(x, y, "Min Quality: ", "qw");
OutputString(COLOR_BROWN, x, y, filter->getMinQuality(), true, left_margin);
OutputToggleString(x, y, "Decorated Only: ", "D", filter->decorated_only, true, left_margin);
OutputHotkeyString(x, y, "Max Quality: ", "QW");
OutputString(COLOR_BROWN, x, y, filter->getMaxQuality(), true, left_margin);
OutputHotkeyString(x, y, "Material Filter:", "M", true, left_margin);
auto filter_descriptions = filter->getMaterialFilterAsVector();
for (auto it = filter_descriptions.begin(); it != filter_descriptions.end(); ++it)
OutputString(COLOR_BROWN, x, y, " *" + *it, true, left_margin);
}
else
{
in_dummy_screen = false;
}
}
}
else if (isInPlannedBuildingQueryMode())
OutputToggleString(x, y, "Decorated Only", "D", filter->getDecoratedOnly(), true, left_margin);
OutputHotkeyString(x, y, "Material Filter:", "M", true, left_margin);
auto filter_descriptions = filter->getMaterials();
for (auto it = filter_descriptions.begin();
it != filter_descriptions.end(); ++it)
OutputString(COLOR_BROWN, x, y, " *" + *it, true, left_margin);
}
};
struct buildingplan_room_hook : public df::viewscreen_dwarfmodest
{
typedef df::viewscreen_dwarfmodest interpose_base;
std::vector<Units::NoblePosition> getNoblePositionOfSelectedBuildingOwner()
{
std::vector<Units::NoblePosition> np;
if (ui->main.mode != df::ui_sidebar_mode::QueryBuilding ||
!world->selected_building ||
!world->selected_building->owner)
{
in_dummy_screen = false;
// Hide suspend toggle option
int y = 20;
Screen::Pen pen(' ', COLOR_BLACK);
Screen::fillRect(pen, x, y, dims.menu_x2, y);
auto filter = planner.getSelectedPlannedBuilding()->getFilter();
y = 24;
OutputString(COLOR_BROWN, x, y, "Planned Building Filter:", true, left_margin);
OutputString(COLOR_BROWN, x, y, "Min Quality: ", false, left_margin);
OutputString(COLOR_BLUE, x, y, filter->getMinQuality(), true, left_margin);
OutputString(COLOR_BROWN, x, y, "Max Quality: ", false, left_margin);
OutputString(COLOR_BLUE, x, y, filter->getMaxQuality(), true, left_margin);
if (filter->decorated_only)
OutputString(COLOR_BLUE, x, y, "Decorated Only", true, left_margin);
OutputString(COLOR_BROWN, x, y, "Materials:", true, left_margin);
auto filters = filter->getMaterialFilterAsVector();
for (auto it = filters.begin(); it != filters.end(); ++it)
OutputString(COLOR_BLUE, x, y, "*" + *it, true, left_margin);
return np;
}
else if (isInNobleRoomQueryMode())
switch (world->selected_building->getType())
{
auto np = getNoblePositionOfSelectedBuildingOwner();
int y = 24;
OutputString(COLOR_BROWN, x, y, "DFHack", true, left_margin);
OutputString(COLOR_WHITE, x, y, "Auto-allocate to:", true, left_margin);
for (size_t i = 0; i < np.size() && i < 9; i++)
{
bool enabled = (roomMonitor.getReservedNobleCode(world->selected_building->id)
== np[i].position->code);
OutputToggleString(x, y, np[i].position->name[0].c_str(),
int_to_string(i+1).c_str(), enabled, true, left_margin);
}
case building_type::Bed:
case building_type::Chair:
case building_type::Table:
break;
default:
return np;
}
return getUniqueNoblePositions(world->selected_building->owner);
}
bool isInNobleRoomQueryMode()
{
if (getNoblePositionOfSelectedBuildingOwner().size() > 0)
return canReserveRoom(world->selected_building);
else
return false;
}
bool handleInput(set<df::interface_key> *input)
{
if (!isInNobleRoomQueryMode())
return false;
if (Gui::inRenameBuilding())
return false;
auto np = getNoblePositionOfSelectedBuildingOwner();
df::interface_key last_token = get_string_key(input);
if (last_token >= interface_key::STRING_A048
&& last_token <= interface_key::STRING_A058)
{
in_dummy_screen = false;
show_help = false;
size_t selection = last_token - interface_key::STRING_A048;
if (np.size() < selection)
return false;
roomMonitor.toggleRoomForPosition(world->selected_building->id, np.at(selection-1).position->code);
return true;
}
return false;
}
DEFINE_VMETHOD_INTERPOSE(void, feed, (set<df::interface_key> *input))
{
if (!handleInput(input))
INTERPOSE_NEXT(feed)(input);
}
DEFINE_VMETHOD_INTERPOSE(void, render, ())
{
INTERPOSE_NEXT(render)();
if (!isInNobleRoomQueryMode())
return;
auto np = getNoblePositionOfSelectedBuildingOwner();
auto dims = Gui::getDwarfmodeViewDims();
int left_margin = dims.menu_x1 + 1;
int x = left_margin;
int y = 24;
OutputString(COLOR_BROWN, x, y, "DFHack", true, left_margin);
OutputString(COLOR_WHITE, x, y, "Auto-allocate to:", true, left_margin);
for (size_t i = 0; i < np.size() && i < 9; i++)
{
bool enabled =
roomMonitor.getReservedNobleCode(world->selected_building->id)
== np[i].position->code;
OutputToggleString(x, y, np[i].position->name[0].c_str(),
int_to_string(i+1).c_str(), enabled, true, left_margin);
}
}
};
IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_hook, feed);
IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_hook, render);
IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_query_hook, feed);
IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_place_hook, feed);
IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_room_hook, feed);
IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_query_hook, render);
IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_place_hook, render);
IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_room_hook, render);
static command_result buildingplan_cmd(color_ostream &out, vector <string> & parameters)
{
@ -624,10 +652,14 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable)
if (enable != is_enabled)
{
planner.reset(out);
if (!INTERPOSE_HOOK(buildingplan_hook, feed).apply(enable) ||
!INTERPOSE_HOOK(buildingplan_hook, render).apply(enable))
planner.reset();
if (!INTERPOSE_HOOK(buildingplan_query_hook, feed).apply(enable) ||
!INTERPOSE_HOOK(buildingplan_place_hook, feed).apply(enable) ||
!INTERPOSE_HOOK(buildingplan_room_hook, feed).apply(enable) ||
!INTERPOSE_HOOK(buildingplan_query_hook, render).apply(enable) ||
!INTERPOSE_HOOK(buildingplan_place_hook, render).apply(enable) ||
!INTERPOSE_HOOK(buildingplan_room_hook, render).apply(enable))
return CR_FAILURE;
is_enabled = enable;
@ -640,7 +672,7 @@ DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <Plug
{
commands.push_back(
PluginCommand(
"buildingplan", "Place furniture before it's built",
"buildingplan", "Plan building construction before you have materials",
buildingplan_cmd, false, "Run 'buildingplan debug [on|off]' to toggle debugging, or 'buildingplan version' to query the plugin version."));
planner.initialize();
@ -651,7 +683,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan
{
switch (event) {
case SC_MAP_LOADED:
planner.reset(out);
planner.reset();
roomMonitor.reset(out);
break;
default:
@ -661,13 +693,17 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan
return CR_OK;
}
static bool cycle_requested = false;
#define DAY_TICKS 1200
DFhackCExport command_result plugin_onupdate(color_ostream &)
{
if (Maps::IsValid() && !World::ReadPauseState() && world->frame_counter % (DAY_TICKS/2) == 0)
if (Maps::IsValid() && !World::ReadPauseState()
&& (cycle_requested || world->frame_counter % (DAY_TICKS/2) == 0))
{
planner.doCycle();
roomMonitor.doCycle();
cycle_requested = false;
}
return CR_OK;
@ -680,8 +716,11 @@ DFhackCExport command_result plugin_shutdown(color_ostream &)
// Lua API section
static bool isPlannableBuilding(df::building_type type) {
return planner.isPlannableBuilding(type);
static bool isPlannableBuilding(df::building_type type,
int16_t subtype,
int32_t custom) {
return planner.isPlannableBuilding(
toBuildingTypeKey(type, subtype, custom));
}
static void addPlannedBuilding(df::building *bld) {
@ -692,9 +731,14 @@ static void doCycle() {
planner.doCycle();
}
static void scheduleCycle() {
cycle_requested = true;
}
DFHACK_PLUGIN_LUA_FUNCTIONS {
DFHACK_LUA_FUNCTION(isPlannableBuilding),
DFHACK_LUA_FUNCTION(addPlannedBuilding),
DFHACK_LUA_FUNCTION(doCycle),
DFHACK_LUA_FUNCTION(scheduleCycle),
DFHACK_LUA_END
};

@ -48,7 +48,7 @@ struct BuildingInfo {
}
bool allocate() {
return planner.allocatePlannedBuilding(type);
return planner.allocatePlannedBuilding(toBuildingTypeKey(type, -1, -1));
}
};
@ -122,7 +122,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable)
if (enable != is_enabled)
{
planner.reset(out);
planner.reset();
is_enabled = enable;
}

@ -4,9 +4,10 @@ local _ENV = mkmodule('plugins.buildingplan')
Native functions:
* bool isPlannableBuilding(df::building_type type)
* bool isPlannableBuilding(df::building_type type, int16_t subtype, int32_t custom)
* void addPlannedBuilding(df::building *bld)
* void doCycle()
* void scheduleCycle()
--]]

@ -79,7 +79,7 @@ static void for_each_(map<T, V> &v, Fn func)
}
template <class T, class V, typename Fn>
static void transform_(vector<T> &src, vector<V> &dst, Fn func)
static void transform_(const vector<T> &src, vector<V> &dst, Fn func)
{
transform(src.begin(), src.end(), back_inserter(dst), func);
}