Merge branch 'develop' into dig-now

develop
Myk 2023-03-06 12:37:08 -08:00 committed by GitHub
commit 2095843a71
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 618 additions and 627 deletions

@ -66,6 +66,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
- `autodump`: changed behaviour to only change ``dump`` and ``forbid`` flags if an item is successfully dumped.
-@ `autochop`: generate default names for burrows with no assigned names
- ``Buildings::StockpileIterator``: fix check for stockpile items on block boundary.
- `dig-now`: fixed multi-layer channel designations only channeling every second layer
- `tailor`: block making clothing sized for toads; make replacement clothing orders use the size of the wearer, not the size of the garment
-@ `confirm`: fix fps drop when enabled
- `channel-safely`: fix an out of bounds error regarding the REPORT event listener receiving (presumably) stale id's
@ -76,6 +77,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
- `showmood`: now shows the number of items needed for cloth and bars in addition to the technically correct but always confusing "total dimension" (150 per bar or 10,000 per cloth)
-@ Stopped mouse clicks from affecting the map when a click on a DFHack screen dismisses the window
- `confirm`: configuration data is now persisted globally.
- `dig-now`: added handling of dig designations that have been converted into active jobs
- `tailor`: add support for adamantine cloth (off by default); improve logging
## API

@ -10,50 +10,48 @@ regardless of whether the required materials are available. This allows you to
focus purely on design elements when you are laying out your fort, and defers
item production concerns to a more convenient time.
Buildingplan is as an alternative to the vanilla building placement UI. It
appears after you have selected the type of building, furniture, or construction
that you want to place in the vanilla build menu. Buildingplan then takes over
for the actual placement step. If any building materials are not available yet
for the placed building, it will be created in a suspended state. Buildingplan
will periodically scan for appropriate items and attach them. Once all items are
attached, the construction job will be unsuspended and a dwarf will come and
build the building. If you have the `unsuspend` overlay enabled (it is enabled
by default), then buildingplan-suspended buildings will appear with a ``P`` marker
on the main map, as opposed to the usual ``x`` marker for "regular" suspended
buildings.
Buildingplan is an alternative to the vanilla building placement UI. It appears
after you have selected the type of building, furniture, or construction that
you want to place in the vanilla build menu. Buildingplan then takes over for
the actual placement step. If the placed building requires materials that
aren't available yet, it will be created in a suspended state. Buildingplan will
periodically scan for appropriate items and attach them to the planned
building. Once all items are attached, the construction job will be unsuspended
and a dwarf will come and build the building. If you have the `unsuspend`
overlay enabled (it is enabled by default), then buildingplan-suspended
buildings will appear with a ``P`` marker on the main map, as opposed to the
usual ``x`` marker for "regular" suspended buildings.
If you want to impose restrictions on which items are chosen for the buildings,
buildingplan has full support for quality and material filters. Before you place
a building, you can select a component item in the list and hit ``f`` or click on
the ``filter`` button next to the item description. This will let you choose your
desired item quality range, whether the item must be decorated, and even which
specific materials the item must be made out of. This lets you create layouts
with a consistent color, if that is part of your design.
buildingplan has full support for quality and material filters (see `below
<Setting quality and material filters>`_). This lets you create layouts with a
consistent color, if that is part of your design.
If you just care about the heat sensitivity of the building, you can set the
building to be fire- or magma-proof in the placement UI screen or in any item
filter screen, and the restriction will apply to all building items. This makes it
very easy to create magma-safe pump stacks, for example.
Buildingplan works very well in conjuction with other design tools like
`gui/quickfort`, which allow you to apply a building layout from a blueprint. You
can apply very large, complicated layouts, and the buildings will simply be built
when your dwarves get around to producing the needed materials. If you set filters
in the buildingplan UI before applying the blueprint, the filters will be applied
to the blueprint buildings, just as if you had planned them from the buildingplan
placement UI.
building to be fire- or magma-proof in the placement UI screen. This makes it
very easy to ensure that your pump stacks and floodgates, for example, are
magma-safe.
Buildingplan works well in conjuction with other design tools like
`gui/quickfort`, which allow you to apply a building layout from a blueprint.
You can apply very large, complicated layouts, and the buildings will simply be
built when your dwarves get around to producing the needed materials. If you
set filters in the buildingplan UI before applying the blueprint, the filters
will be applied to the blueprint buildings, just as if you had planned them
from the buildingplan placement UI.
One way to integrate buildingplan into your gameplay is to create manager
workorders to ensure you always have a few blocks/doors/beds/etc. available. You
can then place as many of each building as you like. Produced items will be used
to build the planned buildings as they are produced, with minimal space dedicated
to stockpiles. The DFHack `orders` library can help with setting up these manager
workorders for you.
to build the planned buildings as they are produced, with minimal space
dedicated to stockpiles. The DFHack `orders` library can help with setting
these manager workorders up for you.
If you do not wish to use the ``buildingplan`` interface, you can turn off the
``buildingplan.planner`` overlay in `gui/overlay`. You should not disable the
``buildingplan`` service entirely in `gui/control-panel` since then existing
planned buildings in loaded forts will stop functioning.
``buildingplan.planner`` overlay in `gui/control-panel` (on the "Overlays"
tab). You should not disable the ``buildingplan`` "System service" in
`gui/control-panel` since existing planned buildings in loaded forts will stop
functioning.
Usage
-----
@ -70,6 +68,10 @@ Examples
Print a report of current settings, which kinds of buildings are planned,
and what kinds of materials the buildings are waiting for.
``buildingplan set boulders false``
When finding items to satisfy "building materials" requirements, don't
select boulders. Use blocks or logs (if enabled) instead.
.. _buildingplan-settings:
Global settings
@ -92,3 +94,87 @@ constructions::
on-new-fortress buildingplan set boulders false
on-new-fortress buildingplan set logs false
Building placement
------------------
Once you have selected a building type to build in the vanilla build menu, the
`buildingplan` placement UI appears as an `overlay` widget, covering the
vanilla building placement panel.
For basic usage, you don't need to change any settings. Just click to place
buildings of the selected type and right click to exit building mode. Any
buildings that require materials that you don't have on hand will be suspended
and built when the items are available.
When building constructions, you'll get a few extra options, like whether the
construction area should be hollow or what types of stairs you'd like at the
top and bottom of a stairwell. Also, unlike other buildings, it is ok if some
tiles selected in the construction area are not appropriate for building. For
example, if you want to fill an area with flooring, you can select the entire
area, and any tiles with existing buildings or walls will simply be skipped.
Setting heat safety filters
+++++++++++++++++++++++++++
If you specifically need the building to be magma- or fire-safe, click on the
"Building safety" button or hit :kbd:`g` until the desired heat safety is
displayed. This filter applies to all items used to construct the building.
Setting quality and material filters
++++++++++++++++++++++++++++++++++++
If you want to set restrictions on the items chosen to complete the planned
building, you can click on the "filter" button next to the item name or select
the item with the :kbd:`*` and :kbd:`/` keys and hit :kbd:`f` to bring up the
filter dialog.
You can select whether the item must be decorated, and you can drag the ends of
the "Item quality" slider to set your desired quality range. Note that blocks,
boulders, logs, and bars don't have a quality, and the quality options are
disabled for those types. As you change the quality settings, the number of
currently available matched items of each material is adjusted in the materials
list.
You can click on specific materials to allow only items of those materials when
building the current type of building. You can also allow or disallow entire
categories of materials by clicking on the "Type" options on the left. Note
that it is perfectly fine to choose materials that currently show zero quantity.
`buildingplan` will patiently watch for items made of materials you have
selected.
Choosing specific items
+++++++++++++++++++++++
If you want to choose specific items, click on the "Choose from items" toggle
or hit :kbd:`i` before placing the building. When you click to place the
building, a dialog will come up that allows you choose which items to use. The
list is sorted by most recently used materials for that building type by
default, but you can change to sort by name or by available quantity by
clicking on the "Sort by" selector or hitting :kbd:`R`.
You can select the maximum quantity of a specified item by clicking on the item
name or selecting it with the arrow keys and hitting :kbd:`Enter`. You can
instead select items one at a time by Ctrl-clicking (:kbd:`Shift`:kbd:`Right`)
to increment or Ctrl-Shift-clicking (:kbd:`Shift`:kbd:`Left`) to decrement.
Once you are satisfied with your choices, click on the "Build" button or hit
:kbd:`B` to continue building. Note that you don't have to select all the items
that the building needs. Any remaining items will be automatically chosen from
other available items (or future items if not all items are available yet). If
there are multiple item types to choose for the current building, one dialog
will appear per item type.
Building status
---------------
When viewing a planned building, a separate `overlay` widget appears on the
building info sheet, showing you which items have been attached and which items
are still pending. For a pending item, you can see its position in the
fulfillment queue. If there is a particular building that you need built ASAP,
you can click on the "make top priority" button (or hit :kbd:`Ctrl`:kbd:`T`) to
bump the items for this building to the front of their respective queues.
Note that each item type and filter configuration has its own queue, so even if
an item is in queue position 1, there may be other queues that snag the needed
item first.

@ -7,8 +7,8 @@ overlay
The overlay framework manages the on-screen widgets that other tools (including
3rd party plugins and scripts) can register for display. For a graphical
configuration interface, please see `gui/overlay`. If you are a developer who
wants to write an overlay widget, please see the `overlay-dev-guide`.
configuration interface, please see `gui/control-panel`. If you are a developer
who wants to write an overlay widget, please see the `overlay-dev-guide`.
Usage
-----

@ -5,32 +5,20 @@ stockpiles
.. dfhack-tool::
:summary: Import and export stockpile settings.
:tags: untested fort design productivity stockpiles
:tags: fort design productivity stockpiles
:no-command:
.. dfhack-command:: copystock
:summary: Copies the configuration of the selected stockpile.
.. dfhack-command:: savestock
:summary: Exports the configuration of the selected stockpile.
.. dfhack-command:: loadstock
:summary: Imports the configuration of the selected stockpile.
:summary: Imports configuration for the selected stockpile.
When the plugin is enabled, the :kbd:`q` menu of each stockpile will have an
option for saving or loading the stockpile settings. See `gui/stockpiles` for
an in-game interface.
Select a stockpile in the UI first to use these commands.
Usage
-----
``enable stockpiles``
Add a hotkey that you can hit to easily save and load settings from
stockpiles selected in :kbd:`q` mode.
``copystock``
Copies the parameters of the currently highlighted stockpile to the custom
stockpile settings and switches to custom stockpile placement mode,
effectively allowing you to copy/paste stockpiles easily.
``savestock <filename>``
Saves the currently highlighted stockpile's settings to a file in your
Dwarf Fortress folder. This file can be used to copy settings between game
@ -45,9 +33,9 @@ etc. are not saved as they are different in every world.
Examples
--------
``savestock food_settings.dfstock``
Export the stockpile settings for the stockpile currently selected in
:kbd:`q` mode to a file named ``food_settings.dfstock``.
``loadstock food_settings.dfstock``
Set the selected stockpile settings to those saved in the
``food_settings.dfstock`` file.
``savestock food``
Export the stockpile settings for the currently selected stockpile to a
file named ``food.dfstock``.
``loadstock food``
Set the selected stockpile settings to those saved in the ``food.dfstock``
file.

@ -121,10 +121,10 @@ void DFHack::Lua::Push(lua_State *state, const df::coord2d &pos)
lua_setfield(state, -2, "y");
}
void DFHack::Lua::GetVector(lua_State *state, std::vector<std::string> &pvec)
void DFHack::Lua::GetVector(lua_State *state, std::vector<std::string> &pvec, int idx)
{
lua_pushnil(state); // first key
while (lua_next(state, 1) != 0)
while (lua_next(state, idx) != 0)
{
pvec.push_back(lua_tostring(state, -1));
lua_pop(state, 1); // remove value, leave key

@ -359,7 +359,7 @@ namespace DFHack {namespace Lua {
}
}
DFHACK_EXPORT void GetVector(lua_State *state, std::vector<std::string> &pvec);
DFHACK_EXPORT void GetVector(lua_State *state, std::vector<std::string> &pvec, int idx = 1);
DFHACK_EXPORT int PushPosXYZ(lua_State *state, const df::coord &pos);
DFHACK_EXPORT int PushPosXY(lua_State *state, const df::coord2d &pos);

@ -139,25 +139,25 @@ namespace DFHack
std::string getToken() const;
std::string toString(uint16_t temp = 10015, bool named = true) const;
bool isAnyCloth();
bool isAnyCloth() const;
void getMatchBits(df::job_item_flags1 &ok, df::job_item_flags1 &mask);
void getMatchBits(df::job_item_flags2 &ok, df::job_item_flags2 &mask);
void getMatchBits(df::job_item_flags3 &ok, df::job_item_flags3 &mask);
void getMatchBits(df::job_item_flags1 &ok, df::job_item_flags1 &mask) const;
void getMatchBits(df::job_item_flags2 &ok, df::job_item_flags2 &mask) const;
void getMatchBits(df::job_item_flags3 &ok, df::job_item_flags3 &mask) const;
df::craft_material_class getCraftClass();
bool matches(const MaterialInfo &mat)
bool matches(const MaterialInfo &mat) const
{
if (!mat.isValid()) return true;
return (type == mat.type) &&
(mat.index == -1 || index == mat.index);
}
bool matches(const df::job_material_category &cat);
bool matches(const df::dfhack_material_category &cat);
bool matches(const df::job_material_category &cat) const;
bool matches(const df::dfhack_material_category &cat) const;
bool matches(const df::job_item &item,
df::item_type itype = df::item_type::NONE);
df::item_type itype = df::item_type::NONE) const;
};
DFHACK_EXPORT bool parseJobMaterialCategory(df::job_material_category *cat, const std::string &token);
@ -169,6 +169,9 @@ namespace DFHack
inline bool operator!= (const MaterialInfo &a, const MaterialInfo &b) {
return a.type != b.type || a.index != b.index;
}
inline bool operator< (const MaterialInfo &a, const MaterialInfo &b) {
return a.type < b.type || (a.type == b.type && a.index < b.index);
}
DFHACK_EXPORT bool isSoilInorganic(int material);
DFHACK_EXPORT bool isStoneInorganic(int material);

@ -376,7 +376,7 @@ df::craft_material_class MaterialInfo::getCraftClass()
return craft_material_class::None;
}
bool MaterialInfo::isAnyCloth()
bool MaterialInfo::isAnyCloth() const
{
using namespace df::enums::material_flags;
@ -387,7 +387,7 @@ bool MaterialInfo::isAnyCloth()
);
}
bool MaterialInfo::matches(const df::job_material_category &cat)
bool MaterialInfo::matches(const df::job_material_category &cat) const
{
if (!material)
return false;
@ -416,7 +416,7 @@ bool MaterialInfo::matches(const df::job_material_category &cat)
return false;
}
bool MaterialInfo::matches(const df::dfhack_material_category &cat)
bool MaterialInfo::matches(const df::dfhack_material_category &cat) const
{
if (!material)
return false;
@ -444,7 +444,7 @@ bool MaterialInfo::matches(const df::dfhack_material_category &cat)
#undef TEST
bool MaterialInfo::matches(const df::job_item &item, df::item_type itype)
bool MaterialInfo::matches(const df::job_item &item, df::item_type itype) const
{
if (!isValid()) return false;
@ -465,7 +465,7 @@ bool MaterialInfo::matches(const df::job_item &item, df::item_type itype)
bits_match(item.flags3.whole, ok3.whole, mask3.whole);
}
void MaterialInfo::getMatchBits(df::job_item_flags1 &ok, df::job_item_flags1 &mask)
void MaterialInfo::getMatchBits(df::job_item_flags1 &ok, df::job_item_flags1 &mask) const
{
ok.whole = mask.whole = 0;
if (!isValid()) return;
@ -500,7 +500,7 @@ void MaterialInfo::getMatchBits(df::job_item_flags1 &ok, df::job_item_flags1 &ma
//04000000 - "milkable" - vtable[107],1,1
}
void MaterialInfo::getMatchBits(df::job_item_flags2 &ok, df::job_item_flags2 &mask)
void MaterialInfo::getMatchBits(df::job_item_flags2 &ok, df::job_item_flags2 &mask) const
{
ok.whole = mask.whole = 0;
if (!isValid()) return;
@ -538,7 +538,7 @@ void MaterialInfo::getMatchBits(df::job_item_flags2 &ok, df::job_item_flags2 &ma
TEST(yarn, MAT_FLAG(YARN));
}
void MaterialInfo::getMatchBits(df::job_item_flags3 &ok, df::job_item_flags3 &mask)
void MaterialInfo::getMatchBits(df::job_item_flags3 &ok, df::job_item_flags3 &mask) const
{
ok.whole = mask.whole = 0;
if (!isValid()) return;

@ -156,7 +156,7 @@ dfhack_plugin(showmood showmood.cpp)
#dfhack_plugin(steam-engine steam-engine.cpp)
#add_subdirectory(spectate)
#dfhack_plugin(stockflow stockflow.cpp LINK_LIBRARIES lua)
#add_subdirectory(stockpiles)
add_subdirectory(stockpiles)
#dfhack_plugin(stocks stocks.cpp)
dfhack_plugin(strangemood strangemood.cpp)
dfhack_plugin(tailor tailor.cpp LINK_LIBRARIES lua)

@ -14,6 +14,7 @@
#include "df/world.h"
using std::map;
using std::set;
using std::string;
using std::unordered_map;
using std::vector;
@ -52,7 +53,7 @@ void set_config_bool(PersistentDataItem &c, int index, bool value) {
static PersistentDataItem config;
// for use in counting available materials for the UI
static vector<MaterialInfo> mat_cache;
static map<string, std::pair<MaterialInfo, string>> mat_cache;
static unordered_map<BuildingTypeKey, vector<const df::job_item *>, BuildingTypeKeyHash> job_item_cache;
static unordered_map<BuildingTypeKey, HeatSafety, BuildingTypeKeyHash> cur_heat_safety;
static unordered_map<BuildingTypeKey, DefaultItemFilters, BuildingTypeKeyHash> cur_item_filters;
@ -143,20 +144,26 @@ static const vector<const df::job_item *> & get_job_items(color_ostream &out, Bu
return jitems;
}
static void cache_matched(int16_t type, int32_t index) {
static const df::dfhack_material_category building_material_categories(
df::dfhack_material_category::mask_glass |
df::dfhack_material_category::mask_metal |
df::dfhack_material_category::mask_soap |
df::dfhack_material_category::mask_stone |
df::dfhack_material_category::mask_wood
);
static const df::dfhack_material_category stone_cat(df::dfhack_material_category::mask_stone);
static const df::dfhack_material_category wood_cat(df::dfhack_material_category::mask_wood);
static const df::dfhack_material_category metal_cat(df::dfhack_material_category::mask_metal);
static const df::dfhack_material_category glass_cat(df::dfhack_material_category::mask_glass);
static void cache_matched(int16_t type, int32_t index) {
MaterialInfo mi;
mi.decode(type, index);
if (mi.matches(building_material_categories)) {
DEBUG(status).print("cached material: %s\n", mi.toString().c_str());
mat_cache.emplace_back(mi);
if (mi.matches(stone_cat)) {
DEBUG(status).print("cached stone material: %s\n", mi.toString().c_str());
mat_cache.emplace(mi.toString(), std::make_pair(mi, "stone"));
} else if (mi.matches(wood_cat)) {
DEBUG(status).print("cached wood material: %s\n", mi.toString().c_str());
mat_cache.emplace(mi.toString(), std::make_pair(mi, "wood"));
} else if (mi.matches(metal_cat)) {
DEBUG(status).print("cached metal material: %s\n", mi.toString().c_str());
mat_cache.emplace(mi.toString(), std::make_pair(mi, "metal"));
} else if (mi.matches(glass_cat)) {
DEBUG(status).print("cached glass material: %s\n", mi.toString().c_str());
mat_cache.emplace(mi.toString(), std::make_pair(mi, "glass"));
}
else
TRACE(status).print("not matched: %s\n", mi.toString().c_str());
@ -601,7 +608,8 @@ static void scheduleCycle(color_ostream &out) {
}
static int scanAvailableItems(color_ostream &out, df::building_type type, int16_t subtype,
int32_t custom, int index, vector<int> *item_ids = NULL) {
int32_t custom, int index, vector<int> *item_ids = NULL,
map<MaterialInfo, int32_t> *counts = NULL) {
DEBUG(status,out).print(
"entering countAvailableItems building_type=%d subtype=%d custom=%d index=%d\n",
type, subtype, custom, index);
@ -619,9 +627,20 @@ static int scanAvailableItems(color_ostream &out, df::building_type type, int16_
for (auto vector_id : vector_ids) {
auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id);
for (auto &item : df::global::world->items.other[other_id]) {
if (itemPassesScreen(item) && matchesFilters(item, jitem, heat, item_filters[index])) {
ItemFilter filter = item_filters[index];
if (counts) {
// don't filter by material; we want counts for all materials
filter.setMaterialMask(0);
filter.setMaterials(set<MaterialInfo>());
}
if (itemPassesScreen(item) && matchesFilters(item, jitem, heat, filter)) {
if (item_ids)
item_ids->emplace_back(item->id);
if (counts) {
MaterialInfo mi;
mi.decode(item);
(*counts)[mi]++;
}
++count;
}
}
@ -686,13 +705,132 @@ static void clearFilter(color_ostream &out, df::building_type type, int16_t subt
auto &filters = get_item_filters(out, key);
if (index < 0 || filters.getItemFilters().size() <= (size_t)index)
return;
filters.setItemFilter(out, ItemFilter(), index);
ItemFilter filter = filters.getItemFilters()[index];
filter.clear();
filters.setItemFilter(out, filter, index);
call_buildingplan_lua(&out, "signal_reset");
}
static void setMaterialFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index, string filter) {
DEBUG(status,out).print("entering setMaterialFilter\n");
call_buildingplan_lua(&out, "signal_reset");
static int setMaterialMaskFilter(lua_State *L) {
color_ostream *out = Lua::GetOutput(L);
if (!out)
out = &Core::getInstance().getConsole();
df::building_type type = (df::building_type)luaL_checkint(L, 1);
int16_t subtype = luaL_checkint(L, 2);
int32_t custom = luaL_checkint(L, 3);
int index = luaL_checkint(L, 4);
DEBUG(status,*out).print(
"entering setMaterialMaskFilter building_type=%d subtype=%d custom=%d index=%d\n",
type, subtype, custom, index);
BuildingTypeKey key(type, subtype, custom);
auto &filters = get_item_filters(*out, key).getItemFilters();
if (index < 0 || filters.size() <= (size_t)index)
return 0;
uint32_t mask = 0;
vector<string> cats;
Lua::GetVector(L, cats, 5);
for (auto &cat : cats) {
if (cat == "stone")
mask |= stone_cat.whole;
else if (cat == "wood")
mask |= wood_cat.whole;
else if (cat == "metal")
mask |= metal_cat.whole;
else if (cat == "glass")
mask |= glass_cat.whole;
}
DEBUG(status,*out).print(
"setting material mask filter for building_type=%d subtype=%d custom=%d index=%d to %x\n",
type, subtype, custom, index, mask);
ItemFilter filter = filters[index];
filter.setMaterialMask(mask);
set<MaterialInfo> new_mats;
if (mask) {
// remove materials from the list that don't match the mask
const auto &mats = filter.getMaterials();
const df::dfhack_material_category mat_mask(mask);
for (auto & mat : mats) {
if (mat.matches(mat_mask))
new_mats.emplace(mat);
}
}
filter.setMaterials(new_mats);
get_item_filters(*out, key).setItemFilter(*out, filter, index);
call_buildingplan_lua(out, "signal_reset");
return 0;
}
static int getMaterialMaskFilter(lua_State *L) {
color_ostream *out = Lua::GetOutput(L);
if (!out)
out = &Core::getInstance().getConsole();
df::building_type type = (df::building_type)luaL_checkint(L, 1);
int16_t subtype = luaL_checkint(L, 2);
int32_t custom = luaL_checkint(L, 3);
int index = luaL_checkint(L, 4);
DEBUG(status,*out).print(
"entering getMaterialFilter building_type=%d subtype=%d custom=%d index=%d\n",
type, subtype, custom, index);
BuildingTypeKey key(type, subtype, custom);
auto &filters = get_item_filters(*out, key);
if (index < 0 || filters.getItemFilters().size() <= (size_t)index)
return 0;
map<string, bool> ret;
uint32_t bits = filters.getItemFilters()[index].getMaterialMask().whole;
ret.emplace("unset", !bits);
ret.emplace("stone", !bits || bits & stone_cat.whole);
ret.emplace("wood", !bits || bits & wood_cat.whole);
ret.emplace("metal", !bits || bits & metal_cat.whole);
ret.emplace("glass", !bits || bits & glass_cat.whole);
Lua::Push(L, ret);
return 1;
}
static int setMaterialFilter(lua_State *L) {
color_ostream *out = Lua::GetOutput(L);
if (!out)
out = &Core::getInstance().getConsole();
df::building_type type = (df::building_type)luaL_checkint(L, 1);
int16_t subtype = luaL_checkint(L, 2);
int32_t custom = luaL_checkint(L, 3);
int index = luaL_checkint(L, 4);
DEBUG(status,*out).print(
"entering setMaterialFilter building_type=%d subtype=%d custom=%d index=%d\n",
type, subtype, custom, index);
BuildingTypeKey key(type, subtype, custom);
auto &filters = get_item_filters(*out, key).getItemFilters();
if (index < 0 || filters.size() <= (size_t)index)
return 0;
set<MaterialInfo> mats;
vector<string> matstrs;
Lua::GetVector(L, matstrs, 5);
for (auto &mat : matstrs) {
if (mat_cache.count(mat))
mats.emplace(mat_cache.at(mat).first);
}
DEBUG(status,*out).print(
"setting material filter for building_type=%d subtype=%d custom=%d index=%d to %zd materials\n",
type, subtype, custom, index, mats.size());
ItemFilter filter = filters[index];
filter.setMaterials(mats);
// ensure relevant masks are explicitly enabled
df::dfhack_material_category mask = filter.getMaterialMask();
if (!mats.size())
mask.whole = 0; // if all materials are disabled, reset the mask
for (auto & mat : mats) {
if (mat.matches(stone_cat))
mask.whole |= stone_cat.whole;
else if (mat.matches(wood_cat))
mask.whole |= wood_cat.whole;
else if (mat.matches(metal_cat))
mask.whole |= metal_cat.whole;
else if (mat.matches(glass_cat))
mask.whole |= glass_cat.whole;
}
filter.setMaterialMask(mask.whole);
get_item_filters(*out, key).setItemFilter(*out, filter, index);
call_buildingplan_lua(out, "signal_reset");
return 0;
}
static int getMaterialFilter(lua_State *L) {
@ -706,8 +844,29 @@ static int getMaterialFilter(lua_State *L) {
DEBUG(status,*out).print(
"entering getMaterialFilter building_type=%d subtype=%d custom=%d index=%d\n",
type, subtype, custom, index);
map<MaterialInfo, int> counts_per_material;
Lua::Push(L, counts_per_material);
BuildingTypeKey key(type, subtype, custom);
auto &filters = get_item_filters(*out, key).getItemFilters();
if (index < 0 || filters.size() <= (size_t)index)
return 0;
const auto &mat_filter = filters[index].getMaterials();
map<MaterialInfo, int32_t> counts;
scanAvailableItems(*out, type, subtype, custom, index, NULL, &counts);
// name -> {count=int, enabled=bool, category=string}
map<string, map<string, string>> ret;
for (auto & entry : mat_cache) {
auto &name = entry.first;
auto &mat = entry.second.first;
auto &cat = entry.second.second;
map<string, string> props;
string count = "0";
if (counts.count(mat))
count = int_to_string(counts.at(mat));
props.emplace("count", count);
props.emplace("enabled", (!mat_filter.size() || mat_filter.count(mat)) ? "true" : "false");
props.emplace("category", cat);
ret.emplace(name, props);
}
Lua::Push(L, ret);
return 1;
}
@ -874,7 +1033,6 @@ DFHACK_PLUGIN_LUA_FUNCTIONS {
DFHACK_LUA_FUNCTION(countAvailableItems),
DFHACK_LUA_FUNCTION(hasFilter),
DFHACK_LUA_FUNCTION(clearFilter),
DFHACK_LUA_FUNCTION(setMaterialFilter),
DFHACK_LUA_FUNCTION(setHeatSafetyFilter),
DFHACK_LUA_FUNCTION(setQualityFilter),
DFHACK_LUA_FUNCTION(getDescString),
@ -886,6 +1044,9 @@ DFHACK_PLUGIN_LUA_FUNCTIONS {
DFHACK_PLUGIN_LUA_COMMANDS {
DFHACK_LUA_COMMAND(getGlobalSettings),
DFHACK_LUA_COMMAND(getAvailableItems),
DFHACK_LUA_COMMAND(setMaterialMaskFilter),
DFHACK_LUA_COMMAND(getMaterialMaskFilter),
DFHACK_LUA_COMMAND(setMaterialFilter),
DFHACK_LUA_COMMAND(getMaterialFilter),
DFHACK_LUA_COMMAND(getHeatSafetyFilter),
DFHACK_LUA_COMMAND(getQualityFilter),

@ -20,6 +20,17 @@ BuildingTypeKey DefaultItemFilters::getKey(PersistentDataItem &filter_config) {
get_config_val(filter_config, FILTER_CONFIG_CUSTOM));
}
static int get_max_quality(const df::job_item *jitem) {
if (jitem->flags2.bits.building_material ||
jitem->item_type == df::item_type::WOOD ||
jitem->item_type == df::item_type::BLOCKS ||
jitem->item_type == df::item_type::BAR ||
jitem->item_type == df::item_type::BOULDER)
return df::item_quality::Ordinary;
return df::item_quality::Masterful;
}
DefaultItemFilters::DefaultItemFilters(color_ostream &out, BuildingTypeKey key, const std::vector<const df::job_item *> &jitems)
: key(key) {
DEBUG(status,out).print("creating persistent data for filter key %d,%d,%d\n",
@ -29,6 +40,9 @@ DefaultItemFilters::DefaultItemFilters(color_ostream &out, BuildingTypeKey key,
set_config_val(filter_config, FILTER_CONFIG_SUBTYPE, std::get<1>(key));
set_config_val(filter_config, FILTER_CONFIG_CUSTOM, std::get<2>(key));
item_filters.resize(jitems.size());
for (size_t idx = 0; idx < jitems.size(); ++idx) {
item_filters[idx].setMaxQuality(get_max_quality(jitems[idx]), true);
}
filter_config.val() = serialize_item_filters(item_filters);
}

@ -8,18 +8,19 @@ namespace DFHack {
DBG_EXTERN(buildingplan, status);
}
using std::set;
using std::string;
using std::vector;
using namespace DFHack;
ItemFilter::ItemFilter() {
ItemFilter::ItemFilter() : default_max_quality(df::item_quality::Masterful) {
clear();
}
void ItemFilter::clear() {
min_quality = df::item_quality::Ordinary;
max_quality = df::item_quality::Masterful;
max_quality = default_max_quality;
decorated_only = false;
mat_mask.whole = 0;
materials.clear();
@ -27,7 +28,7 @@ void ItemFilter::clear() {
bool ItemFilter::isEmpty() const {
return min_quality == df::item_quality::Ordinary
&& max_quality == df::item_quality::Masterful
&& max_quality == default_max_quality
&& !decorated_only
&& !mat_mask.whole
&& materials.empty();
@ -44,7 +45,7 @@ static bool deserializeMaterialMask(string ser, df::dfhack_material_category mat
return true;
}
static bool deserializeMaterials(string ser, vector<DFHack::MaterialInfo> &materials) {
static bool deserializeMaterials(string ser, set<DFHack::MaterialInfo> &materials) {
if (ser.empty())
return true;
@ -56,17 +57,15 @@ static bool deserializeMaterials(string ser, vector<DFHack::MaterialInfo> &mater
DEBUG(status).print("invalid material name serialization: '%s'", ser.c_str());
return false;
}
materials.push_back(material);
materials.emplace(material);
}
return true;
}
ItemFilter::ItemFilter(color_ostream &out, string serialized) {
clear();
ItemFilter::ItemFilter(color_ostream &out, string serialized) : ItemFilter() {
vector<string> tokens;
split_string(&tokens, serialized, "/");
if (tokens.size() != 5) {
if (tokens.size() < 5) {
DEBUG(status,out).print("invalid ItemFilter serialization: '%s'", serialized.c_str());
return;
}
@ -77,20 +76,25 @@ ItemFilter::ItemFilter(color_ostream &out, string serialized) {
setMinQuality(atoi(tokens[2].c_str()));
setMaxQuality(atoi(tokens[3].c_str()));
decorated_only = static_cast<bool>(atoi(tokens[4].c_str()));
if (tokens.size() >= 6)
default_max_quality = static_cast<df::item_quality>(atoi(tokens[5].c_str()));
}
// format: mat,mask,elements/materials,list/minq/maxq/decorated
string ItemFilter::serialize() const {
std::ostringstream ser;
ser << bitfield_to_string(mat_mask, ",") << "/";
vector<string> matstrs;
if (!materials.empty()) {
ser << materials[0].getToken();
for (size_t i = 1; i < materials.size(); ++i)
ser << "," << materials[i].getToken();
for (auto &mat : materials)
matstrs.emplace_back(mat.getToken());
ser << join_strings(",", matstrs);
}
ser << "/" << static_cast<int>(min_quality);
ser << "/" << static_cast<int>(max_quality);
ser << "/" << static_cast<int>(decorated_only);
ser << "/" << static_cast<int>(default_max_quality);
return ser.str();
}
@ -112,11 +116,13 @@ void ItemFilter::setMinQuality(int quality) {
max_quality = min_quality;
}
void ItemFilter::setMaxQuality(int quality) {
void ItemFilter::setMaxQuality(int quality, bool is_default) {
max_quality = static_cast<df::item_quality>(quality);
clampItemQuality(&max_quality);
if (max_quality < min_quality)
min_quality = max_quality;
if (is_default)
default_max_quality = max_quality;
}
void ItemFilter::setDecoratedOnly(bool decorated) {
@ -127,7 +133,7 @@ void ItemFilter::setMaterialMask(uint32_t mask) {
mat_mask.whole = mask;
}
void ItemFilter::setMaterials(const vector<DFHack::MaterialInfo> &materials) {
void ItemFilter::setMaterials(const set<DFHack::MaterialInfo> &materials) {
this->materials = materials;
}
@ -140,8 +146,8 @@ bool ItemFilter::matches(df::dfhack_material_category mask) const {
}
bool ItemFilter::matches(DFHack::MaterialInfo &material) const {
for (auto it = materials.begin(); it != materials.end(); ++it)
if (material.matches(*it))
for (auto &mat : materials)
if (material.matches(mat))
return true;
return false;
}
@ -161,7 +167,7 @@ bool ItemFilter::matches(df::item *item) const {
}
vector<ItemFilter> deserialize_item_filters(color_ostream &out, const string &serialized) {
std::vector<ItemFilter> filters;
vector<ItemFilter> filters;
vector<string> filter_strs;
split_string(&filter_strs, serialized, ";");

@ -15,16 +15,16 @@ public:
std::string serialize() const;
void setMinQuality(int quality);
void setMaxQuality(int quality);
void setMaxQuality(int quality, bool is_default = false);
void setDecoratedOnly(bool decorated);
void setMaterialMask(uint32_t mask);
void setMaterials(const std::vector<DFHack::MaterialInfo> &materials);
void setMaterials(const std::set<DFHack::MaterialInfo> &materials);
df::item_quality getMinQuality() const { return min_quality; }
df::item_quality getMaxQuality() const {return max_quality; }
bool getDecoratedOnly() const { return decorated_only; }
df::dfhack_material_category getMaterialMask() const { return mat_mask; }
std::vector<DFHack::MaterialInfo> getMaterials() const { return materials; }
std::set<DFHack::MaterialInfo> getMaterials() const { return materials; }
bool matches(df::dfhack_material_category mask) const;
bool matches(DFHack::MaterialInfo &material) const;
@ -33,9 +33,10 @@ public:
private:
df::item_quality min_quality;
df::item_quality max_quality;
df::item_quality default_max_quality;
bool decorated_only;
df::dfhack_material_category mat_mask;
std::vector<DFHack::MaterialInfo> materials;
std::set<DFHack::MaterialInfo> materials;
};
std::vector<ItemFilter> deserialize_item_filters(DFHack::color_ostream &out, const std::string &serialized);

@ -12,8 +12,10 @@ namespace DFHack {
DBG_EXTERN(buildingplan, status);
}
using std::set;
using std::string;
using std::vector;
using namespace DFHack;
static vector<vector<df::job_item_vector_id>> get_vector_ids(color_ostream &out, int bld_id) {
@ -58,13 +60,11 @@ static vector<vector<df::job_item_vector_id>> deserialize_vector_ids(color_ostre
return ret;
}
static std::vector<ItemFilter> get_item_filters(color_ostream &out, PersistentDataItem &bld_config) {
std::vector<ItemFilter> ret;
static vector<ItemFilter> get_item_filters(color_ostream &out, PersistentDataItem &bld_config) {
vector<string> rawstrs;
split_string(&rawstrs, bld_config.val(), "|");
if (rawstrs.size() < 2)
return ret;
return vector<ItemFilter>();
return deserialize_item_filters(out, rawstrs[1]);
}

@ -96,6 +96,44 @@ local function get_quantity(filter, hollow, placement_data)
return quantity * dimx * dimy * dimz
end
local function to_title_case(str)
str = str:gsub('(%a)([%w_]*)',
function (first, rest) return first:upper()..rest:lower() end)
str = str:gsub('_', ' ')
return str
end
function get_desc(filter)
local desc = 'Unknown'
if filter.has_tool_use and filter.has_tool_use > -1 then
desc = to_title_case(df.tool_uses[filter.has_tool_use])
elseif filter.flags2 and filter.flags2.screw then
desc = 'Screw'
elseif filter.item_type and filter.item_type > -1 then
desc = to_title_case(df.item_type[filter.item_type])
elseif filter.vector_id and filter.vector_id > -1 then
desc = to_title_case(df.job_item_vector_id[filter.vector_id])
elseif filter.flags2 and filter.flags2.building_material then
desc = 'Building material';
if filter.flags2.fire_safe then
desc = 'Fire-safe material';
end
if filter.flags2.magma_safe then
desc = 'Magma-safe material';
end
end
if desc:endswith('s') then
desc = desc:sub(1,-2)
end
if desc == 'Trappart' then
desc = 'Mechanism'
elseif desc == 'Wood' then
desc = 'Log'
end
return desc
end
local BUTTON_START_PEN, BUTTON_END_PEN, SELECTED_ITEM_PEN = nil, nil, nil
local reset_counts_flag = false
local reset_inspector_flag = false
@ -229,10 +267,11 @@ function ItemSelection:init()
choices=self:get_choices(sort_by_recency),
icon_width=2,
on_submit=self:callback('toggle_group'),
edit_on_char=function(ch) return ch:match('[%l -]') end,
},
widgets.CycleHotkeyLabel{
frame={l=0, b=2},
key='CUSTOM_CTRL_X',
key='CUSTOM_SHIFT_R',
label='Sort by:',
options={
{label='Recently used', value=sort_by_recency},
@ -250,7 +289,7 @@ function ItemSelection:init()
},
widgets.HotkeyLabel{
frame={l=22, b=1},
key='CUSTOM_CTRL_D',
key='CUSTOM_SHIFT_B',
label='Build',
auto_width=true,
on_activate=self:callback('submit'),
@ -622,7 +661,7 @@ QualityAndMaterialsPage.ATTRS{
}
local TYPE_COL_WIDTH = 20
local HEADER_HEIGHT = 8
local HEADER_HEIGHT = 7
local QUALITY_HEIGHT = 9
local FOOTER_HEIGHT = 4
@ -639,11 +678,20 @@ local function can_be_improved(idx)
filter.item_type ~= df.item_type.BOULDER
end
local function mat_sort_by_name(a, b)
return a.name < b.name
end
local function mat_sort_by_quantity(a, b)
return a.quantity > b.quantity or
(a.quantity == b.quantity and mat_sort_by_name(a, b))
end
function QualityAndMaterialsPage:init()
self.lowest_other_item_heat_safety = 2
self.dirty = true
self.summary = ''
local enable_item_quality = can_be_improved(self.index)
local enable_item_quality = can_be_improved(self.index)
self:addviews{
widgets.Panel{
@ -652,52 +700,39 @@ function QualityAndMaterialsPage:init()
frame_inset={l=1},
subviews={
widgets.Label{
frame={l=0, t=0, h=1, r=0},
text={
'Current filter:',
{gap=1, pen=COLOR_LIGHTCYAN, text=self:callback('get_summary')}
},
},
widgets.CycleHotkeyLabel{
view_id='safety',
frame={t=2, l=0, w=35},
key='CUSTOM_SHIFT_G',
label='Building heat safety:',
options={
{label='Fire Magma', value=0, pen=COLOR_GREY},
{label='Fire Magma', value=2, pen=COLOR_RED},
{label='Fire', value=1, pen=COLOR_LIGHTRED},
},
on_change=self:callback('set_heat_safety'),
},
widgets.Label{
frame={t=2, l=30},
text='Magma',
auto_width=true,
text_pen=COLOR_GREY,
visible=function() return self.subviews.safety:getOptionValue() == 1 end,
},
widgets.Label{
frame={t=3, l=3},
text='Other items for this building may not be able to use all of their selected materials.',
visible=function() return self.subviews.safety:getOptionValue() > self.lowest_other_item_heat_safety end,
frame={l=0, t=0},
text='Current filter:',
},
widgets.EditField{
frame={l=0, t=4, w=23},
label_text='Search: ',
on_char=function(ch) return ch:match('%l') end,
widgets.WrappedLabel{
frame={l=16, t=0, h=2, r=0},
text_pen=COLOR_LIGHTCYAN,
text_to_wrap=function() return self.summary end,
auto_height=false,
},
widgets.CycleHotkeyLabel{
frame={l=24, t=4, w=21},
view_id='mat_sort',
frame={l=0, t=3, w=21},
label='Sort by:',
key='CUSTOM_SHIFT_R',
options={'name', 'available'},
options={
{label='name', value=mat_sort_by_name},
{label='available', value=mat_sort_by_quantity}
},
on_change=function() self.dirty = true end,
},
widgets.ToggleHotkeyLabel{
frame={l=24, t=5, w=24},
view_id='hide_zero',
frame={l=0, t=4, w=24},
label='Hide unavailable:',
key='CUSTOM_SHIFT_H',
initial_option=false,
on_change=function() self.dirty = true end,
},
widgets.EditField{
view_id='search',
frame={l=26, t=3},
label_text='Search: ',
on_char=function(ch) return ch:match('[%l -]') end,
},
widgets.Label{
frame={l=1, b=0},
@ -720,20 +755,15 @@ function QualityAndMaterialsPage:init()
view_id='materials_categories',
frame={l=1, t=0, b=0, w=TYPE_COL_WIDTH-3},
scroll_keys={},
choices={
{text='Stone', key='CUSTOM_SHIFT_S'},
{text='Wood', key='CUSTOM_SHIFT_O'},
{text='Metal', key='CUSTOM_SHIFT_M'},
{text='Other', key='CUSTOM_SHIFT_T'},
},
icon_width=2,
cursor_pen=COLOR_CYAN,
on_submit=self:callback('toggle_category'),
},
widgets.List{
widgets.FilteredList{
view_id='materials_mats',
frame={l=TYPE_COL_WIDTH, t=0, r=0, b=0},
choices={
{text='9 - granite'},
{text='0 - graphite'},
},
icon_width=2,
on_submit=self:callback('toggle_material'),
},
},
},
@ -832,35 +862,72 @@ function QualityAndMaterialsPage:init()
},
widgets.HotkeyLabel{
frame={l=30, t=0},
label='Select all',
auto_width=true,
key='CUSTOM_SHIFT_A',
},
widgets.HotkeyLabel{
frame={l=30, t=1},
label='Invert selection',
auto_width=true,
key='CUSTOM_SHIFT_I',
on_activate=self:callback('invert_materials'),
},
widgets.HotkeyLabel{
frame={l=30, t=2},
label='Clear selection',
label='Reset filter',
auto_width=true,
key='CUSTOM_SHIFT_C',
key='CUSTOM_SHIFT_X',
on_activate=self:callback('clear_filter'),
},
},
}
}
-- replace the FilteredList's built-in EditField with our own
self.subviews.materials_mats.list.frame.t = 0
self.subviews.materials_mats.edit.visible = false
self.subviews.materials_mats.edit = self.subviews.search
self.subviews.search.on_change = self.subviews.materials_mats:callback('onFilterChange')
end
local MAT_ENABLED_PEN = to_pen{ch=string.char(251), fg=COLOR_LIGHTGREEN}
local MAT_DISABLED_PEN = to_pen{ch='x', fg=COLOR_RED}
local function make_cat_choice(label, cat, key, cats)
local enabled = cats[cat]
local icon = nil
if not cats.unset then
icon = enabled and MAT_ENABLED_PEN or MAT_DISABLED_PEN
end
return {
text=label,
key=key,
enabled=enabled,
cat=cat,
icon=icon,
}
end
local function make_mat_choice(name, props, enabled, cats)
local quantity = tonumber(props.count)
local text = ('%5d - %s'):format(quantity, name)
local icon = nil
if not cats.unset then
icon = enabled and MAT_ENABLED_PEN or MAT_DISABLED_PEN
end
return {
text=text,
enabled=enabled,
icon=icon,
name=name,
cat=props.category,
quantity=quantity,
}
end
function QualityAndMaterialsPage:refresh()
local summary = ''
local summary = get_desc(get_cur_filters()[self.index])
local subviews = self.subviews
local heat = getHeatSafetyFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type)
subviews.safety:setOption(heat)
if heat >= 2 then summary = summary .. 'Magma safe '
elseif heat == 1 then summary = summary .. 'Fire safe '
if heat >= 2 then summary = 'Magma safe ' .. summary
elseif heat == 1 then summary = 'Fire safe ' .. summary
else summary = 'Any ' .. summary
end
local quality = getQualityFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1)
@ -868,17 +935,95 @@ function QualityAndMaterialsPage:refresh()
subviews.min_quality:setOption(quality.min_quality)
subviews.max_quality:setOption(quality.max_quality)
local cats = getMaterialMaskFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1)
local category_choices={
make_cat_choice('Stone', 'stone', 'CUSTOM_SHIFT_S', cats),
make_cat_choice('Wood', 'wood', 'CUSTOM_SHIFT_O', cats),
make_cat_choice('Metal', 'metal', 'CUSTOM_SHIFT_M', cats),
make_cat_choice('Glass', 'glass', 'CUSTOM_SHIFT_G', cats),
}
self.subviews.materials_categories:setChoices(category_choices)
local mats = getMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1)
local mat_choices = {}
local hide_zero = self.subviews.hide_zero:getOptionValue()
local enabled_mat_names = {}
for name,props in pairs(mats) do
local enabled = props.enabled == 'true' and cats[props.category]
if not cats.unset and enabled then
table.insert(enabled_mat_names, name)
end
if not hide_zero or tonumber(props.count) > 0 then
table.insert(mat_choices, make_mat_choice(name, props, enabled, cats))
end
end
table.sort(mat_choices, self.subviews.mat_sort:getOptionValue())
local prev_filter = self.subviews.search.text
self.subviews.materials_mats:setChoices(mat_choices)
self.subviews.materials_mats:setFilter(prev_filter)
if #enabled_mat_names > 0 then
table.sort(enabled_mat_names)
summary = summary .. (' of %s'):format(table.concat(enabled_mat_names, ', '))
end
self.summary = summary
self.dirty = false
self:updateLayout()
end
function QualityAndMaterialsPage:toggle_category(_, choice)
local cats = {}
if not choice.icon then
-- toggling from unset to something is set
table.insert(cats, choice.cat)
else
choice.enabled = not choice.enabled
for _,c in ipairs(self.subviews.materials_categories:getChoices()) do
if c.enabled then
table.insert(cats, c.cat)
end
end
end
setMaterialMaskFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1, cats)
self.dirty = true
end
function QualityAndMaterialsPage:toggle_material(_, choice)
local mats = {}
if not choice.icon then
-- toggling from unset to something is set
table.insert(mats, choice.name)
else
for _,c in ipairs(self.subviews.materials_mats:getChoices()) do
local enabled = c.enabled
if choice.name == c.name then
enabled = not c.enabled
end
if enabled then
table.insert(mats, c.name)
end
end
end
setMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1, mats)
self.dirty = true
end
function QualityAndMaterialsPage:get_summary()
-- TODO: summarize materials
return self.summary
function QualityAndMaterialsPage:invert_materials()
local mats = {}
for _,c in ipairs(self.subviews.materials_mats:getChoices()) do
if not c.icon then return end
if not c.enabled then
table.insert(mats, c.name)
end
end
setMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1, mats)
self.dirty = true
end
function QualityAndMaterialsPage:set_heat_safety(heat)
setHeatSafetyFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, heat)
function QualityAndMaterialsPage:clear_filter()
clearFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1)
self.dirty = true
end
@ -952,7 +1097,7 @@ GlobalSettingsPage = defclass(GlobalSettingsPage, widgets.ResizingPanel)
GlobalSettingsPage.ATTRS{
autoarrange_subviews=true,
frame={t=0, l=0},
frame_inset={l=1, r=1},
frame_style=gui.INTERIOR_FRAME,
}
function GlobalSettingsPage:init()
@ -1021,8 +1166,8 @@ end
FilterSelection = defclass(FilterSelection, widgets.Window)
FilterSelection.ATTRS{
frame_title='Choose filters [MOCK -- NOT FUNCTIONAL]',
frame={w=53, h=53, l=30, t=8},
frame_title='Choose filters',
frame={w=55, h=53, l=30, t=8},
frame_inset={t=1},
resizable=true,
index=DEFAULT_NIL,
@ -1146,13 +1291,6 @@ local function is_over_options_panel()
return v:getMousePos()
end
local function to_title_case(str)
str = str:gsub('(%a)([%w_]*)',
function (first, rest) return first:upper()..rest:lower() end)
str = str:gsub('_', ' ')
return str
end
ItemLine = defclass(ItemLine, widgets.Panel)
ItemLine.ATTRS{
idx=DEFAULT_NIL,
@ -1221,37 +1359,6 @@ function ItemLine:get_x_pen()
COLOR_GREEN or COLOR_GREY
end
function get_desc(filter)
local desc = 'Unknown'
if filter.has_tool_use and filter.has_tool_use > -1 then
desc = to_title_case(df.tool_uses[filter.has_tool_use])
elseif filter.flags2 and filter.flags2.screw then
desc = 'Screw'
elseif filter.item_type and filter.item_type > -1 then
desc = to_title_case(df.item_type[filter.item_type])
elseif filter.vector_id and filter.vector_id > -1 then
desc = to_title_case(df.job_item_vector_id[filter.vector_id])
elseif filter.flags2 and filter.flags2.building_material then
desc = 'Building material';
if filter.flags2.fire_safe then
desc = 'Fire-safe material';
end
if filter.flags2.magma_safe then
desc = 'Magma-safe material';
end
end
if desc:endswith('s') then
desc = desc:sub(1,-2)
end
if desc == 'Trappart' then
desc = 'Mechanism'
elseif desc == 'Wood' then
desc = 'Log'
end
return desc
end
function ItemLine:get_item_line_text()
local idx = self.idx
local filter = get_cur_filters()[idx]
@ -1654,7 +1761,7 @@ function PlannerOverlay:onInput(keys)
end
end
end
return keys._MOUSE_L
return keys._MOUSE_L or keys.SELECT
end
function PlannerOverlay:render(dc)
@ -1888,6 +1995,7 @@ function InspectorOverlay:init()
frame={t=11, l=0},
label='adjust filters',
key='CUSTOM_CTRL_F',
visible=false, -- until implemented
},
widgets.HotkeyLabel{
frame={t=12, l=0},

@ -474,7 +474,6 @@ void StockpileSerializer::write_general()
mBuffer.set_max_wheelbarrows ( mPile->max_wheelbarrows );
mBuffer.set_max_barrels ( mPile->max_barrels );
mBuffer.set_use_links_only ( mPile->use_links_only );
mBuffer.set_unknown1 ( mPile->settings.unk1 );
mBuffer.set_allow_inorganic ( mPile->settings.allow_inorganic );
mBuffer.set_allow_organic ( mPile->settings.allow_organic );
mBuffer.set_corpses ( mPile->settings.flags.bits.corpses );
@ -490,8 +489,6 @@ void StockpileSerializer::read_general()
mPile->max_barrels = mBuffer.max_barrels();
if ( mBuffer.has_use_links_only() )
mPile->use_links_only = mBuffer.use_links_only();
if ( mBuffer.has_unknown1() )
mPile->settings.unk1 = mBuffer.unknown1();
if ( mBuffer.has_allow_inorganic() )
mPile->settings.allow_inorganic = mBuffer.allow_inorganic();
if ( mBuffer.has_allow_organic() )

@ -136,7 +136,7 @@ message StockpileSettings {
optional AnimalsSet animals = 1;
optional FoodSet food = 2;
optional FurnitureSet furniture = 3;
optional int32 unknown1 = 4;
optional int32 unknown1 = 4 [deprecated=true];
optional RefuseSet refuse = 5;
optional StoneSet stone = 6;
optional OreSet ore = 7;

@ -1,82 +1,32 @@
#include "Core.h"
#include "Console.h"
#include "Export.h"
#include "PluginManager.h"
#include "DataFuncs.h"
#include "LuaTools.h"
#include "modules/Filesystem.h"
#include "../uicommon.h"
#include "StockpileUtils.h"
#include "StockpileSerializer.h"
#include "modules/Filesystem.h"
#include "modules/Gui.h"
#include "modules/Filesystem.h"
#include "df/world.h"
#include "df/world_data.h"
#include "DataDefs.h"
#include "df/plotinfost.h"
#include "df/building_stockpilest.h"
#include "df/stockpile_settings.h"
#include "df/global_objects.h"
#include "df/viewscreen_dwarfmodest.h"
// stl
#include <functional>
#include <vector>
using std::vector;
using std::string;
using std::endl;
using namespace DFHack;
using namespace df::enums;
using namespace google::protobuf;
using namespace dfstockpiles;
DFHACK_PLUGIN ( "stockpiles" );
REQUIRE_GLOBAL(gps);
REQUIRE_GLOBAL(world);
REQUIRE_GLOBAL(plotinfo);
REQUIRE_GLOBAL(selection_rect);
using df::building_stockpilest;
using std::placeholders::_1;
using namespace DFHack;
static command_result copystock ( color_ostream &out, vector <string> & parameters );
static bool copystock_guard ( df::viewscreen *top );
DFHACK_PLUGIN("stockpiles");
static command_result savestock ( color_ostream &out, vector <string> & parameters );
static bool savestock_guard ( df::viewscreen *top );
static command_result loadstock ( color_ostream &out, vector <string> & parameters );
static bool loadstock_guard ( df::viewscreen *top );
DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands )
{
if ( world && plotinfo )
{
commands.push_back(PluginCommand(
"copystock",
"Copy stockpile under cursor.",
copystock,
copystock_guard));
commands.push_back(PluginCommand(
"savestock",
"Save the active stockpile's settings to a file.",
savestock,
savestock_guard));
commands.push_back(PluginCommand(
"loadstock",
"Load and apply stockpile settings from a file.",
loadstock,
loadstock_guard));
}
commands.push_back(PluginCommand(
"savestock",
"Save the active stockpile's settings to a file.",
savestock,
Gui::any_stockpile_hotkey));
commands.push_back(PluginCommand(
"loadstock",
"Load and apply stockpile settings from a file.",
loadstock,
Gui::any_stockpile_hotkey));
return CR_OK;
}
@ -86,111 +36,10 @@ DFhackCExport command_result plugin_shutdown ( color_ostream &out )
return CR_OK;
}
DFhackCExport command_result plugin_onstatechange ( color_ostream &out, state_change_event event )
{
switch ( event )
{
case SC_MAP_LOADED:
break;
default:
break;
}
return CR_OK;
}
static bool copystock_guard ( df::viewscreen *top )
{
using namespace ui_sidebar_mode;
if ( !Gui::dwarfmode_hotkey ( top ) )
return false;
switch ( plotinfo->main.mode )
{
case Stockpiles:
return true;
case BuildingItems:
case QueryBuilding:
return !!virtual_cast<building_stockpilest> ( world->selected_building );
default:
return false;
}
}
static command_result copystock ( color_ostream &out, vector <string> & parameters )
{
// HOTKEY COMMAND: CORE ALREADY SUSPENDED
// For convenience: when used in the stockpiles mode, switch to 'q'
if ( plotinfo->main.mode == ui_sidebar_mode::Stockpiles )
{
world->selected_building = NULL; // just in case it contains some kind of garbage
plotinfo->main.mode = ui_sidebar_mode::QueryBuilding;
selection_rect->start_x = -30000;
out << "Switched back to query building." << endl;
return CR_OK;
}
building_stockpilest *sp = virtual_cast<building_stockpilest> ( world->selected_building );
if ( !sp )
{
out.printerr ( "Selected building isn't a stockpile.\n" );
return CR_WRONG_USAGE;
}
plotinfo->stockpile.custom_settings = sp->settings;
plotinfo->main.mode = ui_sidebar_mode::Stockpiles;
world->selected_stockpile_type = stockpile_category::Custom;
out << "Stockpile options copied." << endl;
return CR_OK;
}
static bool savestock_guard ( df::viewscreen *top )
{
using namespace ui_sidebar_mode;
if ( !Gui::dwarfmode_hotkey ( top ) )
return false;
switch ( plotinfo->main.mode )
{
case Stockpiles:
return true;
case BuildingItems:
case QueryBuilding:
return !!virtual_cast<building_stockpilest> ( world->selected_building );
default:
return false;
}
}
static bool loadstock_guard ( df::viewscreen *top )
{
using namespace ui_sidebar_mode;
if ( !Gui::dwarfmode_hotkey ( top ) )
return false;
switch ( plotinfo->main.mode )
{
case Stockpiles:
return true;
case BuildingItems:
case QueryBuilding:
return !!virtual_cast<building_stockpilest> ( world->selected_building );
default:
return false;
}
}
// exporting
static command_result savestock ( color_ostream &out, vector <string> & parameters )
{
building_stockpilest *sp = virtual_cast<building_stockpilest> ( world->selected_building );
df::building_stockpilest *sp = Gui::getSelectedStockpile(out, true);
if ( !sp )
{
out.printerr ( "Selected building isn't a stockpile.\n" );
@ -247,7 +96,7 @@ static command_result savestock ( color_ostream &out, vector <string> & paramete
// importing
static command_result loadstock ( color_ostream &out, vector <string> & parameters )
{
building_stockpilest *sp = virtual_cast<building_stockpilest> ( world->selected_building );
df::building_stockpilest *sp = Gui::getSelectedStockpile(out, true);
if ( !sp )
{
out.printerr ( "Selected building isn't a stockpile.\n" );
@ -302,227 +151,3 @@ static command_result loadstock ( color_ostream &out, vector <string> & paramete
}
return CR_OK;
}
/**
* calls the lua function manage_settings() to kickoff the GUI
*/
bool manage_settings ( building_stockpilest *sp )
{
auto L = Lua::Core::State;
color_ostream_proxy out ( Core::getInstance().getConsole() );
CoreSuspendClaimer suspend;
Lua::StackUnwinder top ( L );
if ( !lua_checkstack ( L, 2 ) )
return false;
if ( !Lua::PushModulePublic ( out, L, "plugins.stockpiles", "manage_settings" ) )
return false;
Lua::Push ( L, sp );
if ( !Lua::SafeCall ( out, L, 1, 2 ) )
return false;
return true;
}
bool show_message_box ( const std::string & title, const std::string & msg, bool is_error = false )
{
auto L = Lua::Core::State;
color_ostream_proxy out ( Core::getInstance().getConsole() );
CoreSuspendClaimer suspend;
Lua::StackUnwinder top ( L );
if ( !lua_checkstack ( L, 4 ) )
return false;
if ( !Lua::PushModulePublic ( out, L, "plugins.stockpiles", "show_message_box" ) )
return false;
Lua::Push ( L, title );
Lua::Push ( L, msg );
Lua::Push ( L, is_error );
if ( !Lua::SafeCall ( out, L, 3, 0 ) )
return false;
return true;
}
struct stockpiles_import_hook : public df::viewscreen_dwarfmodest
{
typedef df::viewscreen_dwarfmodest interpose_base;
bool handleInput ( set<df::interface_key> *input )
{
if ( Gui::inRenameBuilding() )
return false;
df::building_stockpilest *sp = get_selected_stockpile();
if ( !sp )
return false;
if ( input->count ( interface_key::CUSTOM_L ) )
{
manage_settings ( sp );
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 ) ();
df::building_stockpilest *sp = get_selected_stockpile();
if ( !sp )
return;
auto dims = Gui::getDwarfmodeViewDims();
int left_margin = dims.menu_x1 + 1;
int x = left_margin;
int y = dims.y2 - 3; // below autodump, automelt, autotrade, stocks; above stockflow
OutputHotkeyString ( x, y, "Load/Save Settings", "l", true, left_margin, COLOR_WHITE, COLOR_LIGHTRED );
}
};
IMPLEMENT_VMETHOD_INTERPOSE ( stockpiles_import_hook, feed );
IMPLEMENT_VMETHOD_INTERPOSE ( stockpiles_import_hook, render );
DFHACK_PLUGIN_IS_ENABLED ( is_enabled );
DFhackCExport command_result plugin_enable ( color_ostream &out, bool enable )
{
if ( !gps )
return CR_FAILURE;
if ( enable != is_enabled )
{
if (
!INTERPOSE_HOOK ( stockpiles_import_hook, feed ).apply ( enable ) ||
!INTERPOSE_HOOK ( stockpiles_import_hook, render ).apply ( enable )
)
return CR_FAILURE;
is_enabled = enable;
}
return CR_OK;
}
static std::vector<std::string> list_dir ( const std::string &path, bool recursive = false )
{
// color_ostream_proxy out ( Core::getInstance().getConsole() );
std::vector<std::string> files;
std::stack<std::string> dirs;
dirs.push(path);
// out << "list_dir start" << endl;
while (!dirs.empty() ) {
const std::string current = dirs.top();
// out << "\t walking " << current << endl;
dirs.pop();
std::vector<std::string> entries;
const int res = DFHack::getdir(current, entries);
if ( res != 0 )
continue;
for ( std::vector<std::string>::iterator it = entries.begin() ; it != entries.end(); ++it )
{
if ( (*it).empty() || (*it)[0] == '.' ) continue;
// shitty cross platform c++ we've got to construct the actual path manually
std::ostringstream child_path_s;
child_path_s << current << "/" << *it;
const std::string child = child_path_s.str();
if ( recursive && Filesystem::isdir ( child ) )
{
// out << "\t\tgot child dir: " << child << endl;
dirs.push ( child );
}
else if ( Filesystem::isfile ( child ) )
{
const std::string rel_path ( child.substr ( std::string ( "./"+path).length()-1 ) );
// out << "\t\t adding file: " << child << " as " << rel_path << endl;
files.push_back ( rel_path );
}
}
}
// out << "listdir_stop" << endl;
return files;
}
static std::vector<std::string> clean_dfstock_list ( const std::string &path )
{
if ( !Filesystem::exists ( path ) )
{
return std::vector<std::string>();
}
std::vector<std::string> files ( list_dir ( path, true) );
files.erase ( std::remove_if ( files.begin(), files.end(), [] ( const std::string &f )
{
return !is_dfstockfile ( f );
} ), files.end() );
std::transform ( files.begin(), files.end(), files.begin(), [] ( const std::string &f )
{
return f.substr ( 0, f.find_last_of ( "." ) );
} );
std::sort ( files.begin(),files.end(), CompareNoCase );
return files;
}
static int stockpiles_list_settings ( lua_State *L )
{
auto path = luaL_checkstring ( L, 1 );
if ( Filesystem::exists ( path ) && !Filesystem::isdir ( path ) )
{
lua_pushfstring ( L, "stocksettings path invalid: %s", path );
lua_error ( L );
return 0;
}
std::vector<std::string> files = clean_dfstock_list ( path );
Lua::PushVector ( L, files, true );
return 1;
}
const std::string err_title = "Stockpile Settings Error";
const std::string err_help = "Does the folder exist?\nCheck the console for more information.";
static void stockpiles_load ( color_ostream &out, std::string filename )
{
std::vector<std::string> params;
params.push_back ( filename );
command_result r = loadstock ( out, params );
if ( r != CR_OK )
show_message_box ( err_title, "Couldn't load. " + err_help, true );
}
static void stockpiles_save ( color_ostream &out, std::string filename )
{
std::vector<std::string> params;
params.push_back ( filename );
command_result r = savestock ( out, params );
if ( r != CR_OK )
show_message_box ( err_title, "Couldn't save. " + err_help, true );
}
DFHACK_PLUGIN_LUA_FUNCTIONS
{
DFHACK_LUA_FUNCTION ( stockpiles_load ),
DFHACK_LUA_FUNCTION ( stockpiles_save ),
DFHACK_LUA_END
};
DFHACK_PLUGIN_LUA_COMMANDS
{
DFHACK_LUA_COMMAND ( stockpiles_list_settings ),
DFHACK_LUA_END
};

@ -1 +1 @@
Subproject commit 81183a380b11f4c3045a7888c35afe215d2185ad
Subproject commit 288b38c9e9d8fabf1a0934ffbe23104274c39883