diff --git a/docs/plugins/buildingplan.rst b/docs/plugins/buildingplan.rst index f77e5b590..fe3683f77 100644 --- a/docs/plugins/buildingplan.rst +++ b/docs/plugins/buildingplan.rst @@ -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 +`_). 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. diff --git a/docs/plugins/overlay.rst b/docs/plugins/overlay.rst index ab63de67e..313220fd4 100644 --- a/docs/plugins/overlay.rst +++ b/docs/plugins/overlay.rst @@ -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 ----- diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp index ed0b0699d..a1bf855d6 100644 --- a/library/LuaTools.cpp +++ b/library/LuaTools.cpp @@ -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 &pvec) +void DFHack::Lua::GetVector(lua_State *state, std::vector &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 diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index 86d3d0c7e..689d82260 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -359,7 +359,7 @@ namespace DFHack {namespace Lua { } } - DFHACK_EXPORT void GetVector(lua_State *state, std::vector &pvec); + DFHACK_EXPORT void GetVector(lua_State *state, std::vector &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); diff --git a/library/include/modules/Materials.h b/library/include/modules/Materials.h index 74acf5a8d..3cdf146c9 100644 --- a/library/include/modules/Materials.h +++ b/library/include/modules/Materials.h @@ -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); diff --git a/library/modules/Materials.cpp b/library/modules/Materials.cpp index a6141f1d8..d73b59922 100644 --- a/library/modules/Materials.cpp +++ b/library/modules/Materials.cpp @@ -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; diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 7479d348d..ec85d7952 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -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 mat_cache; +static map> mat_cache; static unordered_map, BuildingTypeKeyHash> job_item_cache; static unordered_map cur_heat_safety; static unordered_map cur_item_filters; @@ -143,20 +144,26 @@ static const vector & 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 *item_ids = NULL) { + int32_t custom, int index, vector *item_ids = NULL, + map *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()); + } + 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 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 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 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 mats; + vector 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 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 counts; + scanAvailableItems(*out, type, subtype, custom, index, NULL, &counts); + // name -> {count=int, enabled=bool, category=string} + map> ret; + for (auto & entry : mat_cache) { + auto &name = entry.first; + auto &mat = entry.second.first; + auto &cat = entry.second.second; + map 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), diff --git a/plugins/buildingplan/defaultitemfilters.cpp b/plugins/buildingplan/defaultitemfilters.cpp index 36d074363..3c3b2f3a9 100644 --- a/plugins/buildingplan/defaultitemfilters.cpp +++ b/plugins/buildingplan/defaultitemfilters.cpp @@ -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 &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); } diff --git a/plugins/buildingplan/itemfilter.cpp b/plugins/buildingplan/itemfilter.cpp index 86c9c1378..e9639f281 100644 --- a/plugins/buildingplan/itemfilter.cpp +++ b/plugins/buildingplan/itemfilter.cpp @@ -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 &materials) { +static bool deserializeMaterials(string ser, set &materials) { if (ser.empty()) return true; @@ -56,17 +57,15 @@ static bool deserializeMaterials(string ser, vector &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 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(atoi(tokens[4].c_str())); + + if (tokens.size() >= 6) + default_max_quality = static_cast(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 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(min_quality); ser << "/" << static_cast(max_quality); ser << "/" << static_cast(decorated_only); + ser << "/" << static_cast(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(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 &materials) { +void ItemFilter::setMaterials(const set &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 deserialize_item_filters(color_ostream &out, const string &serialized) { - std::vector filters; + vector filters; vector filter_strs; split_string(&filter_strs, serialized, ";"); diff --git a/plugins/buildingplan/itemfilter.h b/plugins/buildingplan/itemfilter.h index 29eb7226c..5ae59dd4a 100644 --- a/plugins/buildingplan/itemfilter.h +++ b/plugins/buildingplan/itemfilter.h @@ -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 &materials); + void setMaterials(const std::set &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 getMaterials() const { return materials; } + std::set 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 materials; + std::set materials; }; std::vector deserialize_item_filters(DFHack::color_ostream &out, const std::string &serialized); diff --git a/plugins/buildingplan/plannedbuilding.cpp b/plugins/buildingplan/plannedbuilding.cpp index 27be36a5b..a693f6fcf 100644 --- a/plugins/buildingplan/plannedbuilding.cpp +++ b/plugins/buildingplan/plannedbuilding.cpp @@ -12,8 +12,10 @@ namespace DFHack { DBG_EXTERN(buildingplan, status); } +using std::set; using std::string; using std::vector; + using namespace DFHack; static vector> get_vector_ids(color_ostream &out, int bld_id) { @@ -58,13 +60,11 @@ static vector> deserialize_vector_ids(color_ostre return ret; } -static std::vector get_item_filters(color_ostream &out, PersistentDataItem &bld_config) { - std::vector ret; - +static vector get_item_filters(color_ostream &out, PersistentDataItem &bld_config) { vector rawstrs; split_string(&rawstrs, bld_config.val(), "|"); if (rawstrs.size() < 2) - return ret; + return vector(); return deserialize_item_filters(out, rawstrs[1]); } diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index a0da8d838..a666103e5 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -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},