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: 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 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 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 burrows
======= =======

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

@ -305,7 +305,7 @@ bool MaterialInfo::findProduct(df::material *material, const std::string &name)
return decode(-1); return decode(-1);
} }
std::string MaterialInfo::getToken() std::string MaterialInfo::getToken() const
{ {
if (isNone()) if (isNone())
return "NONE"; 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()) if (isNone())
return "any"; return "any";

@ -1,16 +1,22 @@
#include "buildingplan-lib.h" #include "buildingplan-lib.h"
#include <cstdarg>
#include "Core.h" #include "Core.h"
using namespace DFHack; using namespace DFHack;
bool show_debugging = false; bool show_debugging = false;
void debug(const std::string &msg) void debug(const char *fmt, ...)
{ {
if (!show_debugging) if (!show_debugging)
return; return;
color_ostream_proxy out(Core::getInstance().getConsole()); 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-planner.h"
#include "buildingplan-rooms.h" #include "buildingplan-rooms.h"
void debug(const std::string &msg); void debug(const char *fmt, ...) Wformat(printf,1,2);
extern bool show_debugging; extern bool show_debugging;

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

@ -1,99 +1,131 @@
#pragma once #pragma once
#include "df/item_quality.h" #include <unordered_map>
#include "df/building.h"
#include "df/dfhack_material_category.h" #include "df/dfhack_material_category.h"
#include "df/item_quality.h"
#include "df/job_item.h"
#include "modules/Materials.h" #include "modules/Materials.h"
#include "modules/Persistence.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; df::dfhack_material_category mat_mask;
std::vector<DFHack::MaterialInfo> materials; std::vector<DFHack::MaterialInfo> materials;
df::item_quality min_quality; df::item_quality min_quality;
df::item_quality max_quality; df::item_quality max_quality;
bool decorated_only; bool decorated_only;
ItemFilter(); bool deserializeMaterialMask(std::string ser);
bool deserializeMaterials(std::string ser);
bool matchesMask(DFHack::MaterialInfo &mat); void setMinQuality(int quality);
bool matches(const df::dfhack_material_category mask) const; void setMaxQuality(int quality);
bool matches(DFHack::MaterialInfo &material) const; bool matchesMask(DFHack::MaterialInfo &mat) 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;
}; };
class PlannedBuilding class PlannedBuilding
{ {
public: public:
PlannedBuilding(df::building *building, ItemFilter *filter); PlannedBuilding(df::building *building, const std::vector<ItemFilter> &filters);
PlannedBuilding(DFHack::PersistentDataItem &config, DFHack::color_ostream &out); PlannedBuilding(DFHack::PersistentDataItem &config);
bool assignClosestItem(std::vector<df::item *> *items_vector); bool assignClosestItem(std::vector<df::item *> *items_vector);
bool assignItem(df::item *item); bool assignItem(df::item *item);
bool isValid(); bool isValid() const;
void remove(); void remove();
df::building_type getType(); df::building * getBuilding();
bool isCurrentlySelectedBuilding(); const std::vector<ItemFilter> & getFilters() const;
ItemFilter *getFilter();
private: private:
df::building *building;
DFHack::PersistentDataItem config; DFHack::PersistentDataItem config;
df::coord pos; df::building *building;
ItemFilter filter; 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 class Planner
{ {
public: public:
Planner(); class ItemFiltersWrapper
{
bool isPlannableBuilding(const df::building_type type) const; public:
ItemFiltersWrapper(std::vector<ItemFilter> & item_filters)
void reset(DFHack::color_ostream &out); : 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 initialize();
void reset();
bool allocatePlannedBuilding(BuildingTypeKey key);
void addPlannedBuilding(df::building *bld); void addPlannedBuilding(df::building *bld);
PlannedBuilding *getPlannedBuilding(df::building *bld);
void doCycle(); bool isPlannableBuilding(BuildingTypeKey key);
bool allocatePlannedBuilding(df::building_type type);
PlannedBuilding *getSelectedPlannedBuilding();
void removeSelectedPlannedBuilding(); // returns an empty vector if the type is not supported
ItemFiltersWrapper getItemFilters(BuildingTypeKey key);
ItemFilter *getDefaultItemFilterForType(df::building_type type); void doCycle();
void adjustMinQuality(df::building_type type, int amount);
void adjustMaxQuality(df::building_type type, int amount);
private: private:
std::map<df::building_type, df::item_type> item_for_building_type; 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, 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::map<df::item_type, bool> is_relevant_item_type; //Needed for fast check when looping over all items
std::vector<PlannedBuilding> planned_buildings; std::vector<PlannedBuilding> planned_buildings;
void boundsCheckItemQuality(df::enums::item_quality::item_quality *quality);
void gather_available_items(); void gather_available_items();
}; };

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

@ -48,7 +48,7 @@ struct BuildingInfo {
} }
bool allocate() { 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) if (enable != is_enabled)
{ {
planner.reset(out); planner.reset();
is_enabled = enable; is_enabled = enable;
} }

@ -4,9 +4,10 @@ local _ENV = mkmodule('plugins.buildingplan')
Native functions: 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 addPlannedBuilding(df::building *bld)
* void doCycle() * 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> 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); transform(src.begin(), src.end(), back_inserter(dst), func);
} }