prep buildingplan for core algorithm changes

player-visible changes
- removed text that showed up if you used the wrong hotkeys. no other
  dfhack screen does this, and it seems unneeded. can add back if others
  think otherwise, though

internal changes
- 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-04 20:05:08 -07:00
parent d9d949096e
commit 4d7f4d80ad
11 changed files with 704 additions and 530 deletions

@ -3691,9 +3691,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,185 +1,265 @@
#include "df/building_design.h"
#include "df/building_doorst.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() : /*
min_quality(df::item_quality::Ordinary), * ItemFilter
max_quality(df::item_quality::Artifact), */
decorated_only(false),
valid(true)
{
clear(); // mat_mask is not cleared by default (see issue #1047)
}
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) clear();
if (material.matches(*it))
return true;
return false;
}
bool ItemFilter::matches(df::item *item) std::vector<std::string> tokens;
{ split_string(&tokens, config.val(), "/");
if (item->getQuality() < min_quality || item->getQuality() > max_quality) if (tokens.size() != 2)
{
debug("invalid ItemFilter serialization: '%s'", config.val().c_str());
return false; return false;
}
if (decorated_only && !item->hasImprovements()) if (!deserializeMaterialMask(tokens[0]) || !deserializeMaterials(tokens[1]))
return false; return false;
auto imattype = item->getActualMaterial(); setMinQuality(config.ival(2) - 1);
auto imatindex = item->getActualMaterialIndex(); setMaxQuality(config.ival(4) - 1);
auto item_mat = DFHack::MaterialInfo(imattype, imatindex); decorated_only = config.ival(3) - 1;
return true;
return (materials.size() == 0) ? matchesMask(item_mat) : matches(item_mat);
} }
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) void ItemFilter::serialize(PersistentDataItem &config) const
bitfield_to_string(&descriptions, mat_mask); {
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) void ItemFilter::clearMaterialMask()
descriptions.push_back("any"); {
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, ",")); static void clampItemQuality(df::item_quality *quality)
str.append("/"); {
if (materials.size() > 0) if (*quality > item_quality::Artifact)
{ {
for (size_t i = 0; i < materials.size(); i++) debug("clamping quality to Artifact");
str.append(materials[i].getToken() + ","); *quality = item_quality::Artifact;
if (str[str.size()-1] == ',')
str.resize(str.size () - 1);
} }
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; max_quality = static_cast<df::item_quality>(quality);
std::vector<std::string> tokens; clampItemQuality(&max_quality);
split_string(&tokens, str, "/"); if (max_quality < min_quality)
min_quality = max_quality;
}
if (tokens.size() > 0 && !tokens[0].empty()) void ItemFilter::incMinQuality() { setMinQuality(min_quality + 1); }
{ void ItemFilter::decMinQuality() { setMinQuality(min_quality - 1); }
if (!parseJobMaterialCategory(&mat_mask, tokens[0])) void ItemFilter::incMaxQuality() { setMaxQuality(max_quality + 1); }
return false; void ItemFilter::decMaxQuality() { setMaxQuality(max_quality - 1); }
}
if (tokens.size() > 1 && !tokens[1].empty()) void ItemFilter::toggleDecoratedOnly() { decorated_only = !decorated_only; }
{
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); static std::string material_to_string_fn(const MaterialInfo &m) { return m.toString(); }
}
}
valid = true; uint32_t ItemFilter::getMaterialMask() const { return mat_mask.whole; }
return true;
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); 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); 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; return mat_mask.whole ? mat.matches(mat_mask) : true;
materials.clear();
} }
/* bool ItemFilter::matches(df::dfhack_material_category mask) const
* PlannedBuilding {
*/ return mask.whole & mat_mask.whole;
}
PlannedBuilding::PlannedBuilding(df::building *building, ItemFilter *filter) bool ItemFilter::matches(DFHack::MaterialInfo &material) const
{ {
this->building = building; for (auto it = materials.begin(); it != materials.end(); ++it)
this->filter = *filter; if (material.matches(*it))
pos = df::coord(building->centerx, building->centery, building->z); return true;
config = DFHack::World::AddPersistentData("buildingplan/constraints"); return false;
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;
} }
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())) if (decorated_only && !item->hasImprovements())
{ return false;
out.printerr("Buildingplan: Cannot parse filter: %s\nDiscarding.", config.val().c_str());
return;
}
building = df::building::find(config.ival(1)); auto imattype = item->getActualMaterial();
if (!building) auto imatindex = item->getActualMaterialIndex();
return; auto item_mat = DFHack::MaterialInfo(imattype, imatindex);
pos = df::coord(building->centerx, building->centery, building->z); return (materials.size() == 0) ? matchesMask(item_mat) : matches(item_mat);
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;
} }
/*
* PlannedBuilding
*/
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 int 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_id(config.ival(1)),
building(df::building::find(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 +267,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 +335,70 @@ 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()
{
DFHack::World::DeletePersistentData(config);
}
/* /*
* Planner * BuildingTypeKey
*/ */
Planner::Planner() { } BuildingTypeKey toBuildingTypeKey(
df::building_type btype, int16_t subtype, int32_t custom)
{
return std::make_tuple(btype, subtype, custom);
}
bool Planner::isPlannableBuilding(const df::building_type type) const BuildingTypeKey toBuildingTypeKey(df::building *bld)
{ {
return item_for_building_type.find(type) != item_for_building_type.end(); return std::make_tuple(
bld->getType(), bld->getSubtype(), bld->getCustomType());
} }
void Planner::reset(color_ostream &out) BuildingTypeKey toBuildingTypeKey(df::ui_build_selector *uibs)
{ {
planned_buildings.clear(); return std::make_tuple(
std::vector<PersistentDataItem> items; uibs->building_type, uibs->building_subtype, uibs->custom_type);
DFHack::World::GetPersistentData(&items, "buildingplan/constraints"); }
for (auto i = items.begin(); i != items.end(); i++) std::size_t BuildingTypeKeyHash::operator() (const BuildingTypeKey & key) const
{ {
PlannedBuilding pb(*i, out); return std::hash<df::building_type>()(std::get<0>(key))
if (pb.isValid()) ^ std::hash<int16_t>()(std::get<1>(key))
planned_buildings.push_back(pb); ^ std::hash<int32_t>()(std::get<2>(key));
}
} }
/*
* 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 +436,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");
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;
}
// protect against multiple registrations
if (getPlannedBuilding(bld))
{
debug("building already registered");
return; return;
}
debug("Planned count: " + int_to_string(planned_buildings.size())); PlannedBuilding pb(bld, item_filters);
if (pb.isValid())
{
for (auto job : bld->jobs)
job->flags.bits.suspend = true;
gather_available_items(); planned_buildings.push_back(pb);
for (auto building_iter = planned_buildings.begin(); building_iter != planned_buildings.end();) }
else
{ {
if (building_iter->isValid()) pb.remove();
{ }
if (show_debugging) }
debug(std::string("Trying to allocate ") + enum_item_key_str(building_iter->getType()));
auto required_item_type = item_for_building_type[building_iter->getType()]; PlannedBuilding * Planner::getPlannedBuilding(df::building *bld)
auto items_vector = &available_item_vectors[required_item_type]; {
if (items_vector->size() == 0 || !building_iter->assignClosestItem(items_vector)) for (auto & pb : planned_buildings)
{ {
debug("Unable to allocate an item"); if (pb.getBuilding() == bld)
++building_iter; return &pb;
continue;
}
}
debug("Removing building plan");
building_iter = planned_buildings.erase(building_iter);
} }
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; 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 +541,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; int nfilters = getNumFilters(key);
if (nfilters < 1)
return empty_ret;
std::vector<ItemFilter> ret;
ret.push_back(item_filters[std::get<0>(key)]);
return ItemFiltersWrapper(ret);
} }
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,129 @@
#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::map<df::building_type, ItemFilter> 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,5 +1,5 @@
#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"
#include "df/ui_build_selector.h" #include "df/ui_build_selector.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
@ -27,15 +28,14 @@ REQUIRE_GLOBAL(world);
using namespace DFHack; using namespace DFHack;
using namespace df::enums; using namespace df::enums;
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 +47,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 +129,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 +145,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 +165,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 +178,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 +215,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 +223,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,179 +284,157 @@ 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
planner.getPlannedBuilding(world->selected_building)->remove();
// still allow the building to be removed
return false;
} }
switch (world->selected_building->getType()) return true;
{ }
case building_type::Bed:
case building_type::Chair:
case building_type::Table:
break;
default:
return np;
}
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) INTERPOSE_NEXT(render)();
return canReserveRoom(world->selected_building);
else if (!isInPlannedBuildingQueryMode())
return false; 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) bool handleInput(set<df::interface_key> *input)
{ {
if (isInPlannedBuildingPlacementMode()) if (!isInPlannedBuildingPlacementMode())
return false;
if (in_dummy_screen)
{ {
auto type = ui_build_selector->building_type; if (input->count(interface_key::SELECT) || input->count(interface_key::SEC_SELECT)
if (input->count(interface_key::CUSTOM_SHIFT_P)) || input->count(interface_key::LEAVESCREEN))
{
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; in_dummy_screen = false;
// pass LEAVESCREEN up to parent view
input->clear();
input->insert(interface_key::LEAVESCREEN);
return false;
} }
return 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) {
{ planmode_enabled[key] = !planmode_enabled[key];
if (input->count(interface_key::SELECT) || input->count(interface_key::SEC_SELECT) if (!is_planmode_enabled(key))
|| input->count(interface_key::LEAVESCREEN)) Gui::refreshSidebar();
{ return true;
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;
}
}
} }
else if (isInPlannedBuildingQueryMode()) if (input->count(interface_key::CUSTOM_SHIFT_F))
{ {
if (input->count(interface_key::SUSPENDBUILDING)) quickfort_mode = !quickfort_mode;
{ return true;
return true; // Don't unsuspend planned buildings
}
else if (input->count(interface_key::DESTROYBUILDING))
{
planner.removeSelectedPlannedBuilding(); // Remove persistent data
}
} }
else if (isInNobleRoomQueryMode())
if (!is_planmode_enabled(key))
return false;
if (input->count(interface_key::SELECT))
{ {
if (Gui::inRenameBuilding()) if (ui_build_selector->errors.size() == 0 && planner.allocatePlannedBuilding(key))
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; Gui::refreshSidebar();
if (np.size() < selection) if (quickfort_mode)
return false; in_dummy_screen = true;
roomMonitor.toggleRoomForPosition(world->selected_building->id, np.at(selection-1).position->code);
return 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)) DEFINE_VMETHOD_INTERPOSE(void, feed, (set<df::interface_key> *input))
@ -469,133 +446,170 @@ 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 // FIXME Hide bags
if (((*iter)->find("Needs") != string::npos && **iter != "Needs adjacent wall") || if (((*iter)->find("Needs") != string::npos
(*iter)->find("No access") != 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);
{ int y = dims.y1 + 1;
Screen::Pen pen(' ',COLOR_BLACK); Screen::fillRect(pen, x, y, dims.menu_x2, y + 20);
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_BROWN, x, y,
OutputString(COLOR_WHITE, x, y, "Enter, Shift-Enter or Esc", true, left_margin); "Placeholder for legacy Quickfort. This screen is not required for DFHack native quickfort.",
} true, left_margin);
else OutputString(COLOR_WHITE, x, y, "Enter, Shift-Enter or Esc", true, left_margin);
{ return;
int y = 23; }
if (show_help) int y = 23;
{
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);
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;
OutputHotkeyString(x, y, "Min Quality: ", "qw"); auto filter = planner.getItemFilters(key).rbegin();
OutputString(COLOR_BROWN, x, y, filter->getMinQuality(), true, left_margin); y += 2;
OutputHotkeyString(x, y, "Min Quality: ", "qw");
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();
OutputString(COLOR_BROWN, x, y, " *" + *it, true, left_margin); it != filter_descriptions.end(); ++it)
} OutputString(COLOR_BROWN, x, y, " *" + *it, true, left_margin);
else }
{ };
in_dummy_screen = false;
} struct buildingplan_room_hook : public df::viewscreen_dwarfmodest
} {
} typedef df::viewscreen_dwarfmodest interpose_base;
else if (isInPlannedBuildingQueryMode())
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; return np;
// 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);
} }
else if (isInNobleRoomQueryMode())
switch (world->selected_building->getType())
{ {
auto np = getNoblePositionOfSelectedBuildingOwner(); case building_type::Bed:
int y = 24; case building_type::Chair:
OutputString(COLOR_BROWN, x, y, "DFHack", true, left_margin); case building_type::Table:
OutputString(COLOR_WHITE, x, y, "Auto-allocate to:", true, left_margin); break;
for (size_t i = 0; i < np.size() && i < 9; i++) default:
{ return np;
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);
}
} }
return getUniqueNoblePositions(world->selected_building->owner);
}
bool isInNobleRoomQueryMode()
{
if (getNoblePositionOfSelectedBuildingOwner().size() > 0)
return canReserveRoom(world->selected_building);
else 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)
{
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++)
{ {
in_dummy_screen = false; bool enabled =
show_help = false; 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_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 +638,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 +658,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 +669,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 +679,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 +702,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 +717,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);
} }