diff --git a/CMakeLists.txt b/CMakeLists.txt index b2cfe724e..6854b3300 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -189,8 +189,8 @@ if(NOT EXISTS ${dfhack_SOURCE_DIR}/library/xml/codegen.pl endif() # set up versioning. -set(DF_VERSION "50.05") -set(DFHACK_RELEASE "alpha3.1") +set(DF_VERSION "50.07") +set(DFHACK_RELEASE "alpha0") set(DFHACK_PRERELEASE TRUE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/data/art/dfhack.png b/data/art/dfhack.png index 17719f084..133dc5aaf 100644 Binary files a/data/art/dfhack.png and b/data/art/dfhack.png differ diff --git a/data/orders/military.json b/data/orders/military.json index 5b7604dac..0e53747b6 100644 --- a/data/orders/military.json +++ b/data/orders/military.json @@ -1138,42 +1138,6 @@ "job" : "MakeWeapon", "material" : "INORGANIC:SILVER" }, - { - "amount_left" : 1, - "amount_total" : 1, - "frequency" : "Daily", - "id" : 64, - "is_active" : false, - "is_validated" : false, - "item_conditions" : - [ - { - "condition" : "AtLeast", - "item_type" : "BAR", - "material" : "INORGANIC:SILVER", - "value" : 20 - }, - { - "condition" : "AtLeast", - "item_type" : "BAR", - "material" : "COAL", - "value" : 100 - }, - { - "condition" : "AtMost", - "flags" : - [ - "metal" - ], - "item_subtype" : "ITEM_WEAPON_CROSSBOW", - "item_type" : "WEAPON", - "value" : 10 - } - ], - "item_subtype" : "ITEM_WEAPON_CROSSBOW", - "job" : "MakeWeapon", - "material" : "INORGANIC:SILVER" - }, { "amount_left" : 1, "amount_total" : 1, @@ -1656,12 +1620,6 @@ "item_type" : "WEAPON", "material" : "INORGANIC:STEEL", "value" : 10 - }, - { - "condition" : "LessThan", - "item_type" : "BAR", - "material" : "INORGANIC:SILVER", - "value" : 5 } ], "item_subtype" : "ITEM_WEAPON_CROSSBOW", @@ -2357,12 +2315,6 @@ "material" : "INORGANIC:STEEL", "value" : 30 }, - { - "condition" : "LessThan", - "item_type" : "BAR", - "material" : "INORGANIC:SILVER", - "value" : 5 - }, { "condition" : "AtMost", "flags" : @@ -3068,12 +3020,6 @@ "material" : "INORGANIC:STEEL", "value" : 30 }, - { - "condition" : "LessThan", - "item_type" : "BAR", - "material" : "INORGANIC:SILVER", - "value" : 5 - }, { "condition" : "AtMost", "flags" : @@ -3856,12 +3802,6 @@ "material" : "INORGANIC:STEEL", "value" : 30 }, - { - "condition" : "LessThan", - "item_type" : "BAR", - "material" : "INORGANIC:SILVER", - "value" : 5 - }, { "condition" : "AtMost", "flags" : @@ -4734,12 +4674,6 @@ "material" : "INORGANIC:STEEL", "value" : 30 }, - { - "condition" : "LessThan", - "item_type" : "BAR", - "material" : "INORGANIC:SILVER", - "value" : 5 - }, { "condition" : "AtMost", "flags" : diff --git a/docs/changelog.txt b/docs/changelog.txt index ece6f04e1..c151e2ebc 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -39,16 +39,22 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - ``Units::isFortControlled``: Account for agitated wildlife - Fix right click sometimes closing both a DFHack window and a vanilla panel - Fixed issue with scrollable lists having some data off-screen if they were scrolled before being made visible +- `channel-safely`: fixed bug resulting in marker mode never being set for any designation +-@ `automelt`: fixed bug related to lua stack smashing behavior in returned stockpile configs +-@ `autochop`: fixed bug related to lua stack smashing behavior in returned stockpile configs ## Misc Improvements - `automelt`: is now more resistent to savegame corruption -- `hotkeys`: DFHack logo is now hidden on screens where it covers important information when in the default position (e.g. when choosing an embark site) +-@ `hotkeys`: DFHack logo is now hidden on screens where it covers important information when in the default position (e.g. when choosing an embark site) +- `misery`: now persists state with the fort - `autodump`: reinstate ``autodump-destroy-item``, hotkey: Ctrl-K - `autodump`: new hotkey for ``autodump-destroy-here``: Ctrl-H - `dig`: new hotkeys for vein designation on z-level (Ctrl-V) and vein designation across z-levels (Ctrl-Shift-V) - `clean`: new hotkey for `spotclean`: Ctrl-C - `autobutcher`: changed defaults from 5 females / 1 male to 4 females / 2 males so a single unfortunate accident doesn't leave players without a mating pair - `autobutcher`: now immediately loads races available at game start into the watchlist +-@ replaced DFHack logo used for the hover hotspot with a crisper image +- `orders`: recipe for silver crossbows removed from ``library/military`` as it is not a vanilla recipe, but is available in ``library/military_include_artifact_materials`` ## Documentation @@ -57,13 +63,16 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## Lua - `overlay`: overlay widgets can now specify focus paths for the viewscreens they attach to so they only appear in specific contexts. see `overlay-dev-guide` for details. - ``widgets.CycleHotkeyLabel``: Added ``key_back`` optional parameter to cycle backwards. +- ``widgets.HotkeyLabel``: Added ``setLabel`` method to allow easily updating the label text without mangling the keyboard shortcut. +- ``widgets.HotkeyLabel``: Added ``setOnActivate`` method to allow easily updating the ``on_activate`` callback. +- ``widgets.FilteredList``: Added ``case_sensitive`` optional paramter to determine if filtering is case sensitive. ## Removed # 50.05-alpha3.1 ## Fixes -- `seedwatch`: fix parameter parsing when setting targets +-@ `seedwatch`: fix parameter parsing when setting targets # 50.05-alpha3 diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 503f4bece..4e900bea5 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -4808,6 +4808,16 @@ It has the following attributes: :on_activate: If specified, it is the callback that will be called whenever the hotkey is pressed or the label is clicked. +The HotkeyLabel widget implements the following methods: + +* ``hotkeylabel:setLabel(label)`` + + Updates the label without altering the hotkey text. + +* ``hotkeylabel:setOnActivate(on_activate)`` + + Updates the on_activate callback. + CycleHotkeyLabel class ---------------------- @@ -4958,6 +4968,7 @@ construction that allows filtering the list by subwords of its items. In addition to passing through all attributes supported by List, it supports: +:case_sensitive: If true, matching is case sensitive. Defaults to true. :edit_pen: If specified, used instead of ``cursor_pen`` for the edit field. :edit_below: If true, the edit field is placed below the list instead of above. :edit_key: If specified, the edit field is disabled until this key is pressed. diff --git a/docs/plugins/misery.rst b/docs/plugins/misery.rst index f2c4d1952..8a65d8419 100644 --- a/docs/plugins/misery.rst +++ b/docs/plugins/misery.rst @@ -2,18 +2,35 @@ misery ====== .. dfhack-tool:: - :summary: Increase the intensity of negative dwarven thoughts. - :tags: fort armok auto units + :summary: Increase the intensity of your citizens' negative thoughts. + :tags: fort gameplay units -When enabled, negative thoughts that your dwarves have will multiply by the -specified factor. +When enabled, negative thoughts that your citizens have will multiply by the +specified factor. This makes it more challenging to keep them happy. Usage ----- +:: + + enable misery + misery [status] + misery + misery clear + +The default misery factor is ``2``, meaning that your dwarves will become +miserable twice as fast. + +Examples +-------- + ``enable misery`` - Start multiplying negative thoughts. -``misery `` - Change the multiplicative factor of bad thoughts. The default is ``2``. + Start multiplying bad thoughts for your citizens! + +``misery 5`` + Make dwarves become unhappy 5 times faster than normal -- this is quite + challenging to handle! + ``misery clear`` - Clear away negative thoughts added by ``misery``. + Clear away negative thoughts added by ``misery``. Note that this will not + clear negative thoughts that your dwarves accumulated "naturally". diff --git a/docs/plugins/tailor.rst b/docs/plugins/tailor.rst index 4dc4f53a4..0e8980948 100644 --- a/docs/plugins/tailor.rst +++ b/docs/plugins/tailor.rst @@ -5,16 +5,15 @@ tailor :summary: Automatically keep your dwarves in fresh clothing. :tags: fort auto workorders -Whenever the bookkeeper updates stockpile records, this plugin will scan the -fort. If there are fresh cloths available, dwarves who are wearing tattered -clothing will have their rags confiscated (in the same manner as the -`cleanowned` tool) so that they'll reequip with replacement clothes. +Once a day, this plugin will scan the clothing situation in the fort. If there +are fresh cloths available, dwarves who are wearing tattered clothing will have +their rags confiscated (in the same manner as the `cleanowned` tool) so that +they'll reequip with replacement clothes. -If there are not enough clothes available, manager orders will be generated -to manufacture some more. ``tailor`` will intelligently create orders using -raw materials that you have on hand in the fort. For example, if you have -lots of silk, but no cloth, then ``tailor`` will order only silk clothing to -be made. +If there are not enough clothes available, manager orders will be generated to +manufacture some more. ``tailor`` will intelligently create orders using raw +materials that you have on hand in the fort. For example, if you have lots of +silk, but no cloth, then ``tailor`` will order only silk clothing to be made. Usage ----- @@ -22,7 +21,8 @@ Usage :: enable tailor - tailor status + tailor [status] + tailor now tailor materials [ ...] By default, ``tailor`` will prefer using materials in this order:: @@ -32,12 +32,16 @@ By default, ``tailor`` will prefer using materials in this order:: but you can use the ``tailor materials`` command to restrict which materials are used, and in what order. -Example -------- +Examples +-------- ``enable tailor`` Start replacing tattered clothes with default settings. +``tailor now`` + Run a scan and order cycle right now, regardless of whether the plugin is + enabled. + ``tailor materials silk cloth yarn`` Restrict the materials used for automatically manufacturing clothing to silk, cloth, and yarn, preferred in that order. This saves leather for diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 45ea7d84d..69d43821e 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -478,7 +478,7 @@ static void OpenPersistent(lua_State *state) static int DFHACK_MATINFO_TOKEN = 0; -void Lua::Push(lua_State *state, MaterialInfo &info) +void Lua::Push(lua_State *state, const MaterialInfo &info) { if (!info.isValid()) { diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp index b4d009c7b..ed0b0699d 100644 --- a/library/LuaTools.cpp +++ b/library/LuaTools.cpp @@ -101,7 +101,7 @@ void DFHack::Lua::Push(lua_State *state, const Units::NoblePosition &pos) lua_setfield(state, -2, "position"); } -void DFHack::Lua::Push(lua_State *state, df::coord pos) +void DFHack::Lua::Push(lua_State *state, const df::coord &pos) { lua_createtable(state, 0, 3); lua_pushinteger(state, pos.x); @@ -112,7 +112,7 @@ void DFHack::Lua::Push(lua_State *state, df::coord pos) lua_setfield(state, -2, "z"); } -void DFHack::Lua::Push(lua_State *state, df::coord2d pos) +void DFHack::Lua::Push(lua_State *state, const df::coord2d &pos) { lua_createtable(state, 0, 2); lua_pushinteger(state, pos.x); @@ -191,7 +191,7 @@ void DFHack::Lua::PushInterfaceKeys(lua_State *L, } } -int DFHack::Lua::PushPosXYZ(lua_State *state, df::coord pos) +int DFHack::Lua::PushPosXYZ(lua_State *state, const df::coord &pos) { if (!pos.isValid()) { @@ -207,7 +207,7 @@ int DFHack::Lua::PushPosXYZ(lua_State *state, df::coord pos) } } -int DFHack::Lua::PushPosXY(lua_State *state, df::coord2d pos) +int DFHack::Lua::PushPosXY(lua_State *state, const df::coord2d &pos) { if (!pos.isValid()) { diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index ca9aac788..86d3d0c7e 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -325,10 +325,10 @@ namespace DFHack {namespace Lua { inline void Push(lua_State *state, const std::string &str) { lua_pushlstring(state, str.data(), str.size()); } - DFHACK_EXPORT void Push(lua_State *state, df::coord obj); - DFHACK_EXPORT void Push(lua_State *state, df::coord2d obj); + DFHACK_EXPORT void Push(lua_State *state, const df::coord &obj); + DFHACK_EXPORT void Push(lua_State *state, const df::coord2d &obj); void Push(lua_State *state, const Units::NoblePosition &pos); - DFHACK_EXPORT void Push(lua_State *state, MaterialInfo &info); + DFHACK_EXPORT void Push(lua_State *state, const MaterialInfo &info); DFHACK_EXPORT void Push(lua_State *state, const Screen::Pen &info); template inline void Push(lua_State *state, T *ptr) { PushDFObject(state, ptr); @@ -361,29 +361,34 @@ namespace DFHack {namespace Lua { DFHACK_EXPORT void GetVector(lua_State *state, std::vector &pvec); - DFHACK_EXPORT int PushPosXYZ(lua_State *state, df::coord pos); - DFHACK_EXPORT int PushPosXY(lua_State *state, df::coord2d pos); - - template - inline void TableInsert(lua_State *state, T_Key key, T_Value value) - { - Lua::Push(state, key); - Lua::Push(state, value); - lua_settable(state, -3); - } + DFHACK_EXPORT int PushPosXYZ(lua_State *state, const df::coord &pos); + DFHACK_EXPORT int PushPosXY(lua_State *state, const df::coord2d &pos); template void Push(lua_State *L, const std::map &pmap) { lua_createtable(L, 0, pmap.size()); - for (auto &entry : pmap) - TableInsert(L, entry.first, entry.second); + for (auto &entry : pmap) { + Lua::Push(L, entry.first); + Lua::Push(L, entry.second); + lua_settable(L, -3); + } } template void Push(lua_State *L, const std::unordered_map &pmap) { lua_createtable(L, 0, pmap.size()); - for (auto &entry : pmap) - TableInsert(L, entry.first, entry.second); + for (auto &entry : pmap) { + Lua::Push(L, entry.first); + Lua::Push(L, entry.second); + lua_settable(L, -3); + } + } + + template + inline void TableInsert(lua_State *state, const T_Key &key, const T_Value &value) { + Lua::Push(state, key); + Lua::Push(state, value); + lua_settable(state, -3); } DFHACK_EXPORT void CheckPen(lua_State *L, Screen::Pen *pen, int index, bool allow_nil = false, bool allow_color = true); diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index dfc584fff..a5840a1b1 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -1419,6 +1419,20 @@ HotkeyLabel.ATTRS{ } function HotkeyLabel:init() + self:initializeLabel() +end + +function HotkeyLabel:setOnActivate(on_activate) + self.on_activate = on_activate + self:initializeLabel() +end + +function HotkeyLabel:setLabel(label) + self.label = label + self:initializeLabel() +end + +function HotkeyLabel:initializeLabel() self:setText{{key=self.key, key_sep=self.key_sep, text=self.label, on_activate=self.on_activate}} end @@ -1868,6 +1882,7 @@ end FilteredList = defclass(FilteredList, Widget) FilteredList.ATTRS { + case_sensitive = true, edit_below = false, edit_key = DEFAULT_NIL, edit_ignore_keys = DEFAULT_NIL, @@ -2028,11 +2043,17 @@ function FilteredList:setFilter(filter, pos) -- start matches at non-space or non-punctuation. this allows -- punctuation itself to be matched if that is useful (e.g. -- filenames or parameter names) - if key ~= '' and - not search_key:match('%f[^%p\x00]'..key) and + if key ~= '' then + if not self.case_sensitive then + search_key = string.lower(search_key) + key = string.lower(key) + end + + if not search_key:match('%f[^%p\x00]'..key) and not search_key:match('%f[^%s\x00]'..key) then - ok = false - break + ok = false + break + end end end if ok then diff --git a/library/xml b/library/xml index 68f8bfa92..eff493010 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 68f8bfa92b68ef9bc30da7f5a8ea5cf91f69ea16 +Subproject commit eff493010d11358fc8243239dbf8d07024eedb0c diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index df49c3d59..7d4ff760e 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -130,7 +130,7 @@ dfhack_plugin(liquids liquids.cpp Brushes.h LINK_LIBRARIES lua) #dfhack_plugin(luasocket luasocket.cpp LINK_LIBRARIES clsocket lua dfhack-tinythread) #dfhack_plugin(manipulator manipulator.cpp) #dfhack_plugin(map-render map-render.cpp LINK_LIBRARIES lua) -dfhack_plugin(misery misery.cpp) +dfhack_plugin(misery misery.cpp LINK_LIBRARIES lua) #dfhack_plugin(mode mode.cpp) #dfhack_plugin(mousequery mousequery.cpp) dfhack_plugin(nestboxes nestboxes.cpp) @@ -159,7 +159,7 @@ dfhack_plugin(showmood showmood.cpp) #add_subdirectory(stockpiles) #dfhack_plugin(stocks stocks.cpp) #dfhack_plugin(strangemood strangemood.cpp) -dfhack_plugin(tailor tailor.cpp) +dfhack_plugin(tailor tailor.cpp LINK_LIBRARIES lua) dfhack_plugin(tiletypes tiletypes.cpp Brushes.h LINK_LIBRARIES lua) #dfhack_plugin(title-folder title-folder.cpp) #dfhack_plugin(title-version title-version.cpp) diff --git a/plugins/autochop.cpp b/plugins/autochop.cpp index bf7540e29..3a433f7a9 100644 --- a/plugins/autochop.cpp +++ b/plugins/autochop.cpp @@ -803,6 +803,30 @@ static void push_burrow_config(lua_State *L, PersistentDataItem &c) { get_config_bool(c, BURROW_CONFIG_PROTECT_COOKABLE)); } +static void emplace_bulk_burrow_config(lua_State *L, map> &burrows, int id, bool chop = false, + bool clearcut = false, bool protect_brewable = false, + bool protect_edible = false, bool protect_cookable = false) { + + map burrow_config; + burrow_config.emplace("id", id); + burrow_config.emplace("chop", chop); + burrow_config.emplace("clearcut", clearcut); + burrow_config.emplace("protect_brewable", protect_brewable); + burrow_config.emplace("protect_edible", protect_edible); + burrow_config.emplace("protect_cookable", protect_cookable); + + burrows.emplace(id, burrow_config); +} + +static void emplace_bulk_burrow_config(lua_State *L, map> &burrows, PersistentDataItem &c) { + emplace_bulk_burrow_config(L, burrows, get_config_val(c, BURROW_CONFIG_ID), + get_config_bool(c, BURROW_CONFIG_CHOP), + get_config_bool(c, BURROW_CONFIG_CLEARCUT), + get_config_bool(c, BURROW_CONFIG_PROTECT_BREWABLE), + get_config_bool(c, BURROW_CONFIG_PROTECT_EDIBLE), + get_config_bool(c, BURROW_CONFIG_PROTECT_COOKABLE)); +} + static int autochop_getTreeCountsAndBurrowConfigs(lua_State *L) { color_ostream *out = Lua::GetOutput(L); if (!out) @@ -818,6 +842,9 @@ static int autochop_getTreeCountsAndBurrowConfigs(lua_State *L) { &designated_trees, &accessible_yield, &tree_counts, &designated_tree_counts); map summary; + + map> burrow_config_map; + summary.emplace("accessible_trees", accessible_trees); summary.emplace("inaccessible_trees", inaccessible_trees); summary.emplace("designated_trees", designated_trees); @@ -831,13 +858,16 @@ static int autochop_getTreeCountsAndBurrowConfigs(lua_State *L) { for (auto &burrow : plotinfo->burrows.list) { int id = burrow->id; if (watched_burrows_indices.count(id)) { - push_burrow_config(L, watched_burrows[watched_burrows_indices[id]]); + // push_burrow_config(L, watched_burrows[watched_burrows_indices[id]]); + emplace_bulk_burrow_config(L, burrow_config_map, watched_burrows[watched_burrows_indices[id]]); } else { - push_burrow_config(L, id); + emplace_bulk_burrow_config(L, burrow_config_map, id); } } - return 3 + plotinfo->burrows.list.size(); + Lua::Push(L, burrow_config_map); + + return 4; } static int autochop_getBurrowConfig(lua_State *L) { diff --git a/plugins/automelt.cpp b/plugins/automelt.cpp index e8001c48b..0cd9e8131 100644 --- a/plugins/automelt.cpp +++ b/plugins/automelt.cpp @@ -567,6 +567,20 @@ static void push_stockpile_config(lua_State *L, PersistentDataItem &c) { get_config_bool(c, STOCKPILE_CONFIG_MONITORED)); } +static void emplace_bulk_stockpile_config(lua_State *L, int id, bool monitored, map> &stockpiles) { + map stockpile_config; + stockpile_config.emplace("id", id); + stockpile_config.emplace("monitored", monitored); + + stockpiles.emplace(id, stockpile_config); +} + +static void emplace_bulk_stockpile_config(lua_State *L, PersistentDataItem &c, map> &stockpiles) { + int32_t id = get_config_val(c, STOCKPILE_CONFIG_ID); + bool monitored = get_config_bool(c, STOCKPILE_CONFIG_MONITORED); + emplace_bulk_stockpile_config(L, id, monitored, stockpiles); +} + static void automelt_designate(color_ostream &out) { DEBUG(status, out).print("entering automelt designate\n"); out.print("designated %d item(s) for melting\n", do_cycle(out)); @@ -762,24 +776,28 @@ static int automelt_getItemCountsAndStockpileConfigs(lua_State *L) { Lua::Push(L, item_count_piles); Lua::Push(L, marked_item_count_piles); Lua::Push(L, premarked_item_count_piles); - int32_t bldg_count = 0; + + map> stockpile_config_map; for (auto pile : world->buildings.other.STOCKPILE) { if (!isStockpile(pile)) continue; - bldg_count++; int id = pile->id; if (watched_stockpiles.count(id)) { - DEBUG(cycle,*out).print("indexed_id=%d\n", get_config_val(watched_stockpiles[id], STOCKPILE_CONFIG_ID)); - push_stockpile_config(L, watched_stockpiles[id]); + emplace_bulk_stockpile_config(L, watched_stockpiles[id], stockpile_config_map); + } else { - push_stockpile_config(L, id, false); + emplace_bulk_stockpile_config(L, id, false, stockpile_config_map); } } + + Lua::Push(L, stockpile_config_map); + + DEBUG(perf, *out).print("exit automelt_getItemCountsAndStockpileConfigs\n"); - return 4+bldg_count; + return 5; } DFHACK_PLUGIN_LUA_FUNCTIONS{ diff --git a/plugins/channel-safely/channel-manager.cpp b/plugins/channel-safely/channel-manager.cpp index c22bd8a8e..aa7a24461 100644 --- a/plugins/channel-safely/channel-manager.cpp +++ b/plugins/channel-safely/channel-manager.cpp @@ -5,7 +5,17 @@ #include //hash function for df::coord #include +#define NUMARGS(...) std::tuple_size::value +#define d_assert(condition, ...) \ + static_assert(NUMARGS(__VA_ARGS__) >= 1, "d_assert(condition, format, ...) requires at least up to format as arguments"); \ + if (!condition) { \ + DFHack::Core::getInstance().getConsole().printerr(__VA_ARGS__); \ + assert(0); \ + } + + df::unit* find_dwarf(const df::coord &map_pos) { + df::unit* nearest = nullptr; uint32_t distance; for (auto unit : df::global::world->units.active) { @@ -57,6 +67,7 @@ void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool // cavein prevention bool cavein_possible = false; uint8_t least_access = 100; + std::unordered_map cavein_candidates; if (!marker_mode) { /* To prevent cave-ins we're looking at accessibility of tiles with open space below them @@ -111,7 +122,7 @@ void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool // if no cave-in is possible [or we don't check for], we'll just execute normally and move on if (!cavein_possible) { TRACE(manager).print("cave-in evaluated false\n"); - assert(manage_one(pos, true, marker_mode)); + d_assert(manage_one(pos, true, marker_mode), "manage_one() is failing under !cavein"); continue; } // cavein is only possible if marker_mode is false @@ -136,16 +147,16 @@ void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool evT->priority[Coord(local)] = v; } } - assert(manage_one(pos, true, false)); + d_assert(manage_one(pos, true, false), "manage_one() is failing for cavein "); continue; } // cavein possible, but we failed to meet the criteria for activation if (cavein_candidates.count(pos)) { - DEBUG(manager).print("cave-in evaluated true and no dignow and (%d > %d)\n", cavein_candidates[pos], least_access+OFFSET); + DEBUG(manager).print("cave-in evaluated true and the cavein candidate's accessibility check was made as (%d <= %d)\n", cavein_candidates[pos], least_access+OFFSET); } else { - DEBUG(manager).print("cave-in evaluated true and no dignow and pos is not a candidate\n"); + DEBUG(manager).print("cave-in evaluated true and the position was not a candidate, nor was it set for dignow\n"); } - assert(manage_one(pos, true, true)); + d_assert(manage_one(pos, true, true), "manage_one() is failing to set a cave-in causing designation to marker mode"); } INFO(manager).print("manage_group() is done\n"); } diff --git a/plugins/lua/autochop.lua b/plugins/lua/autochop.lua index 6b8c9f943..e9bad2eef 100644 --- a/plugins/lua/autochop.lua +++ b/plugins/lua/autochop.lua @@ -96,14 +96,17 @@ function getTreeCountsAndBurrowConfigs() ret.summary = table.remove(data, 1) ret.tree_counts = table.remove(data, 1) ret.designated_tree_counts = table.remove(data, 1) - ret.burrow_configs = data - for _,c in ipairs(ret.burrow_configs) do + local unparsed_burrow_configs = table.remove(data, 1) + + ret.burrow_configs = {} + for idx,c in pairs(unparsed_burrow_configs) do c.name = df.burrow.find(c.id).name c.chop = c.chop ~= 0 c.clearcut = c.clearcut ~= 0 c.protect_brewable = c.protect_brewable ~= 0 c.protect_edible = c.protect_edible ~= 0 c.protect_cookable = c.protect_cookable ~= 0 + table.insert(ret.burrow_configs, c) end return ret end diff --git a/plugins/lua/automelt.lua b/plugins/lua/automelt.lua index b49a434a1..cbd1bd538 100644 --- a/plugins/lua/automelt.lua +++ b/plugins/lua/automelt.lua @@ -64,8 +64,10 @@ function getItemCountsAndStockpileConfigs() ret.item_counts = table.remove(data, 1) ret.marked_item_counts = table.remove(data, 1) ret.premarked_item_counts = table.remove(data, 1) - ret.stockpile_configs = data - for _,c in ipairs(ret.stockpile_configs) do + local unparsed_stockpile_configs = table.remove(data, 1) + ret.stockpile_configs = {} + + for idx,c in pairs(unparsed_stockpile_configs) do if not c.id or c.id == -1 then c.name = "ERROR" c.monitored = false @@ -76,6 +78,7 @@ function getItemCountsAndStockpileConfigs() end c.monitored = c.monitored ~= 0 end + table.insert(ret.stockpile_configs, c) end return ret diff --git a/plugins/lua/misery.lua b/plugins/lua/misery.lua new file mode 100644 index 000000000..cd507c9b3 --- /dev/null +++ b/plugins/lua/misery.lua @@ -0,0 +1,43 @@ +local _ENV = mkmodule('plugins.misery') + +local argparse = require('argparse') + +local function process_args(opts, args) + if args[1] == 'help' then + opts.help = true + return + end + + return argparse.processArgsGetopt(args, { + {'h', 'help', handler=function() opts.help = true end}, + }) +end + +function status() + print(('misery is %s'):format(isEnabled() and "enabled" or "disabled")) + print(('misery factor is: %d'):format(misery_getFactor())) +end + +function parse_commandline(...) + local args, opts = {...}, {} + local positionals = process_args(opts, args) + + if opts.help then + return false + end + + local command = table.remove(positionals, 1) + if not command or command == 'status' then + status() + elseif command == 'factor' then + misery_setFactor(positionals[1]) + elseif command == 'clear' then + misery_clear() + else + return false + end + + return true +end + +return _ENV diff --git a/plugins/lua/tailor.lua b/plugins/lua/tailor.lua new file mode 100644 index 000000000..bcfd8bc10 --- /dev/null +++ b/plugins/lua/tailor.lua @@ -0,0 +1,56 @@ +local _ENV = mkmodule('plugins.tailor') + +local argparse = require('argparse') +local utils = require('utils') + +local function process_args(opts, args) + if args[1] == 'help' then + opts.help = true + return + end + + return argparse.processArgsGetopt(args, { + {'h', 'help', handler=function() opts.help = true end}, + }) +end + +function status() + print(('tailor is %s'):format(isEnabled() and "enabled" or "disabled")) + print('materials preference order:') + for _,name in ipairs(tailor_getMaterialPreferences()) do + print((' %s'):format(name)) + end +end + +function setMaterials(names) + local idxs = utils.invert(names) + tailor_setMaterialPreferences( + idxs.silk or -1, + idxs.cloth or -1, + idxs.yarn or -1, + idxs.leather or -1) +end + +function parse_commandline(...) + local args, opts = {...}, {} + local positionals = process_args(opts, args) + + if opts.help then + return false + end + + local command = table.remove(positionals, 1) + if not command or command == 'status' then + status() + elseif command == 'now' then + tailor_doCycle() + elseif command == 'materials' then + setMaterials(positionals) + else + return false + end + + return true +end + +return _ENV diff --git a/plugins/misery.cpp b/plugins/misery.cpp index 870c4480c..f241394ae 100644 --- a/plugins/misery.cpp +++ b/plugins/misery.cpp @@ -1,14 +1,7 @@ #include -#include #include #include -#include "DataDefs.h" -#include "Export.h" -#include "PluginManager.h" - -#include "modules/Units.h" - #include "df/emotion_type.h" #include "df/plotinfost.h" #include "df/unit.h" @@ -17,179 +10,266 @@ #include "df/unit_thought_type.h" #include "df/world.h" -using namespace std; +#include "modules/Persistence.h" +#include "modules/Units.h" +#include "modules/World.h" + +#include "Core.h" +#include "Debug.h" +#include "LuaTools.h" +#include "PluginManager.h" + +using std::string; +using std::vector; + using namespace DFHack; DFHACK_PLUGIN("misery"); DFHACK_PLUGIN_IS_ENABLED(is_enabled); -REQUIRE_GLOBAL(world); -REQUIRE_GLOBAL(plotinfo); REQUIRE_GLOBAL(cur_year); REQUIRE_GLOBAL(cur_year_tick); +REQUIRE_GLOBAL(world); -typedef df::unit_personality::T_emotions Emotion; - -static int factor = 1; -static int tick = 0; -const int INTERVAL = 1000; +namespace DFHack { + DBG_DECLARE(misery, cycle, DebugCategory::LINFO); + DBG_DECLARE(misery, config, DebugCategory::LINFO); +} -command_result misery(color_ostream& out, vector& parameters); -void add_misery(df::unit *unit); -void clear_misery(df::unit *unit); +static const string CONFIG_KEY = string(plugin_name) + "/config"; +static PersistentDataItem config; -const int FAKE_EMOTION_FLAG = (1 << 30); -const int STRENGTH_MULTIPLIER = 100; +enum ConfigValues { + CONFIG_IS_ENABLED = 0, + CONFIG_FACTOR = 1, +}; -bool is_valid_unit (df::unit *unit) { - if (!Units::isOwnRace(unit) || !Units::isOwnCiv(unit)) - return false; - if (!Units::isActive(unit)) - return false; - return true; +static int get_config_val(PersistentDataItem &c, int index) { + if (!c.isValid()) + return -1; + return c.ival(index); +} +static bool get_config_bool(PersistentDataItem &c, int index) { + return get_config_val(c, index) == 1; +} +static void set_config_val(PersistentDataItem &c, int index, int value) { + if (c.isValid()) + c.ival(index) = value; +} +static void set_config_bool(PersistentDataItem &c, int index, bool value) { + set_config_val(c, index, value ? 1 : 0); } -inline bool is_fake_emotion (Emotion *e) { - return e->flags.whole & FAKE_EMOTION_FLAG; +static const int32_t CYCLE_TICKS = 1200; // one day +static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle + +static command_result do_command(color_ostream &out, vector ¶meters); +static void do_cycle(color_ostream &out); + +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { + DEBUG(config,out).print("initializing %s\n", plugin_name); + + // provide a configuration interface for the plugin + commands.push_back(PluginCommand( + plugin_name, + "Increase the intensity of negative dwarven thoughts.", + do_command)); + + return CR_OK; } -void add_misery (df::unit *unit) { - // Add a fake miserable thought - // Remove any fake ones that already exist - if (!unit || !unit->status.current_soul) - return; - clear_misery(unit); - auto &emotions = unit->status.current_soul->personality.emotions; - Emotion *e = new Emotion; - e->type = df::emotion_type::MISERY; - e->thought = df::unit_thought_type::SoapyBath; - e->flags.whole |= FAKE_EMOTION_FLAG; - emotions.push_back(e); +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { + if (!Core::getInstance().isWorldLoaded()) { + out.printerr("Cannot enable %s without a loaded world.\n", plugin_name); + return CR_FAILURE; + } - for (Emotion *e : emotions) { - if (is_fake_emotion(e)) { - e->year = *cur_year; - e->year_tick = *cur_year_tick; - e->strength = STRENGTH_MULTIPLIER * factor; - e->severity = STRENGTH_MULTIPLIER * factor; - } + if (enable != is_enabled) { + is_enabled = enable; + DEBUG(config,out).print("%s from the API; persisting\n", + is_enabled ? "enabled" : "disabled"); + set_config_bool(config, CONFIG_IS_ENABLED, is_enabled); + if (enable) + do_cycle(out); + } else { + DEBUG(config,out).print("%s from the API, but already %s; no action\n", + is_enabled ? "enabled" : "disabled", + is_enabled ? "enabled" : "disabled"); } + return CR_OK; } -void clear_misery (df::unit *unit) { - if (!unit || !unit->status.current_soul) - return; - auto &emotions = unit->status.current_soul->personality.emotions; - auto it = remove_if(emotions.begin(), emotions.end(), [](Emotion *e) { - if (is_fake_emotion(e)) { - delete e; - return true; - } - return false; - }); - emotions.erase(it, emotions.end()); -} +DFhackCExport command_result plugin_shutdown (color_ostream &out) { + DEBUG(config,out).print("shutting down %s\n", plugin_name); -DFhackCExport command_result plugin_shutdown(color_ostream& out) { - factor = 0; return CR_OK; } -DFhackCExport command_result plugin_onupdate(color_ostream& out) { - static bool wasLoaded = false; - if ( factor == 0 || !world || !world->map.block_index ) { - if ( wasLoaded ) { - //we just unloaded the game: clear all data - factor = 0; - is_enabled = false; - wasLoaded = false; - } - return CR_OK; - } +DFhackCExport command_result plugin_load_data (color_ostream &out) { + cycle_timestamp = 0; + config = World::GetPersistentData(CONFIG_KEY); - if ( !wasLoaded ) { - wasLoaded = true; + if (!config.isValid()) { + DEBUG(config,out).print("no config found in this save; initializing\n"); + config = World::AddPersistentData(CONFIG_KEY); + set_config_bool(config, CONFIG_IS_ENABLED, is_enabled); + set_config_val(config, CONFIG_FACTOR, 2); } - if ( tick < INTERVAL ) { - tick++; - return CR_OK; - } - tick = 0; + is_enabled = get_config_bool(config, CONFIG_IS_ENABLED); + DEBUG(config,out).print("loading persisted enabled state: %s\n", + is_enabled ? "true" : "false"); - //TODO: consider units.active - for (df::unit *unit : world->units.all) { - if (is_valid_unit(unit)) { - add_misery(unit); + return CR_OK; +} + +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { + if (event == DFHack::SC_WORLD_UNLOADED) { + if (is_enabled) { + DEBUG(config,out).print("world unloaded; disabling %s\n", + plugin_name); + is_enabled = false; } } - return CR_OK; } -DFhackCExport command_result plugin_init(color_ostream& out, vector &commands) { - commands.push_back(PluginCommand( - "misery", - "Increase the intensity of negative dwarven thoughts.", - misery)); +DFhackCExport command_result plugin_onupdate(color_ostream &out) { + if (is_enabled && world->frame_counter - cycle_timestamp >= CYCLE_TICKS) + do_cycle(out); return CR_OK; } -DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) -{ - if (enable != is_enabled) - { - is_enabled = enable; - factor = enable ? 1 : 0; - tick = INTERVAL; - } +static bool call_misery_lua(color_ostream *out, const char *fn_name, + int nargs = 0, int nres = 0, + Lua::LuaLambda && args_lambda = Lua::DEFAULT_LUA_LAMBDA, + Lua::LuaLambda && res_lambda = Lua::DEFAULT_LUA_LAMBDA) { + DEBUG(config).print("calling misery lua function: '%s'\n", fn_name); - return CR_OK; + CoreSuspender guard; + + auto L = Lua::Core::State; + Lua::StackUnwinder top(L); + + if (!out) + out = &Core::getInstance().getConsole(); + + return Lua::CallLuaModuleFunction(*out, L, "plugins.misery", fn_name, + nargs, nres, + std::forward(args_lambda), + std::forward(res_lambda)); } -command_result misery(color_ostream &out, vector& parameters) { - if ( !world || !world->map.block_index ) { - out.printerr("misery can only be enabled in fortress mode with a fully-loaded game.\n"); +static command_result do_command(color_ostream &out, vector ¶meters) { + CoreSuspender suspend; + + if (!Core::getInstance().isWorldLoaded()) { + out.printerr("Cannot run %s without a loaded world.\n", plugin_name); return CR_FAILURE; } - if ( parameters.size() < 1 || parameters.size() > 2 ) { - return CR_WRONG_USAGE; + bool show_help = false; + if (!call_misery_lua(&out, "parse_commandline", parameters.size(), 1, + [&](lua_State *L) { + for (const string ¶m : parameters) + Lua::Push(L, param); + }, + [&](lua_State *L) { + show_help = !lua_toboolean(L, -1); + })) { + return CR_FAILURE; } - if ( parameters[0] == "disable" ) { - if ( parameters.size() > 1 ) { - return CR_WRONG_USAGE; - } - factor = 0; - is_enabled = false; - return CR_OK; - } else if ( parameters[0] == "enable" ) { - is_enabled = true; - factor = 1; - if ( parameters.size() == 2 ) { - int a = atoi(parameters[1].c_str()); - if ( a < 1 ) { - out.printerr("Second argument must be a positive integer.\n"); - return CR_WRONG_USAGE; - } - factor = a; - } - tick = INTERVAL; - } else if ( parameters[0] == "clear" ) { - for (df::unit *unit : world->units.all) { - if (is_valid_unit(unit)) { - clear_misery(unit); - } - } - } else { - int a = atoi(parameters[0].c_str()); - if ( a < 0 ) { - return CR_WRONG_USAGE; + return show_help ? CR_WRONG_USAGE : CR_OK; +} + +///////////////////////////////////////////////////// +// cycle logic +// + +const int FAKE_EMOTION_FLAG = (1 << 30); +const int STRENGTH_MULTIPLIER = 100; + +typedef df::unit_personality::T_emotions Emotion; + +static bool is_fake_emotion(Emotion *e) { + return e->flags.whole & FAKE_EMOTION_FLAG; +} + +static void clear_misery(df::unit *unit) { + if (!unit || !unit->status.current_soul) + return; + auto &emotions = unit->status.current_soul->personality.emotions; + auto it = std::remove_if(emotions.begin(), emotions.end(), [](Emotion *e) { + if (is_fake_emotion(e)) { + delete e; + return true; } - factor = a; - is_enabled = factor > 0; + return false; + }); + emotions.erase(it, emotions.end()); +} +// clears fake negative thoughts then runs the given lambda +static void affect_units( + std::function &&process_unit = [](df::unit *){}) { + for (auto unit : world->units.active) { + if (!Units::isCitizen(unit) || !unit->status.current_soul) + continue; + + clear_misery(unit); + std::forward &&>(process_unit)(unit); } +} - return CR_OK; +static void do_cycle(color_ostream &out) { + // mark that we have recently run + cycle_timestamp = world->frame_counter; + + DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + + int strength = STRENGTH_MULTIPLIER * get_config_val(config, CONFIG_FACTOR); + + affect_units([&](df::unit *unit) { + Emotion *e = new Emotion; + e->type = df::emotion_type::MISERY; + e->thought = df::unit_thought_type::SoapyBath; + e->flags.whole |= FAKE_EMOTION_FLAG; + e->year = *cur_year; + e->year_tick = *cur_year_tick; + e->strength = strength; + e->severity = strength; + unit->status.current_soul->personality.emotions.push_back(e); + }); } + +///////////////////////////////////////////////////// +// Lua API +// + +static void misery_clear(color_ostream &out) { + DEBUG(config,out).print("entering misery_clear\n"); + affect_units(); +} + +static void misery_setFactor(color_ostream &out, int32_t factor) { + DEBUG(config,out).print("entering misery_setFactor\n"); + if (1 >= factor) { + out.printerr("factor must be at least 2\n"); + return; + } + set_config_val(config, CONFIG_FACTOR, factor); + if (is_enabled) + do_cycle(out); +} + +static int misery_getFactor(color_ostream &out) { + DEBUG(config,out).print("entering tailor_getFactor\n"); + return get_config_val(config, CONFIG_FACTOR); +} + +DFHACK_PLUGIN_LUA_FUNCTIONS { + DFHACK_LUA_FUNCTION(misery_clear), + DFHACK_LUA_FUNCTION(misery_setFactor), + DFHACK_LUA_FUNCTION(misery_getFactor), + DFHACK_LUA_END +}; diff --git a/plugins/tailor.cpp b/plugins/tailor.cpp index 2cca8a2c8..5511e2b5d 100644 --- a/plugins/tailor.cpp +++ b/plugins/tailor.cpp @@ -1,136 +1,160 @@ /* * Tailor plugin. Automatically manages keeping your dorfs clothed. - * For best effect, place "tailor enable" in your dfhack.init configuration, - * or set AUTOENABLE to true. */ -#include "Core.h" -#include "DataDefs.h" -#include "Debug.h" -#include "PluginManager.h" +#include +#include #include "df/creature_raw.h" -#include "df/global_objects.h" #include "df/historical_entity.h" +#include "df/item.h" +#include "df/item_flags.h" #include "df/itemdef_armorst.h" #include "df/itemdef_glovesst.h" #include "df/itemdef_helmst.h" #include "df/itemdef_pantsst.h" #include "df/itemdef_shoesst.h" #include "df/items_other_id.h" -#include "df/job.h" -#include "df/job_type.h" #include "df/manager_order.h" #include "df/plotinfost.h" #include "df/world.h" -#include "modules/Maps.h" -#include "modules/Units.h" +#include "Core.h" +#include "Debug.h" +#include "LuaTools.h" +#include "PluginManager.h" + +#include "modules/Materials.h" +#include "modules/Persistence.h" #include "modules/Translation.h" +#include "modules/Units.h" #include "modules/World.h" -using namespace DFHack; +using std::string; +using std::vector; -using df::global::world; -using df::global::plotinfo; +using namespace DFHack; DFHACK_PLUGIN("tailor"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); -#define AUTOENABLE false -DFHACK_PLUGIN_IS_ENABLED(enabled); - -REQUIRE_GLOBAL(world); REQUIRE_GLOBAL(plotinfo); REQUIRE_GLOBAL(standing_orders_use_dyed_cloth); +REQUIRE_GLOBAL(world); namespace DFHack { DBG_DECLARE(tailor, cycle, DebugCategory::LINFO); DBG_DECLARE(tailor, config, DebugCategory::LINFO); } -class Tailor { - // ARMOR, SHOES, HELM, GLOVES, PANTS +static const string CONFIG_KEY = string(plugin_name) + "/config"; +static PersistentDataItem config; - // ah, if only STL had a bimap +enum ConfigValues { + CONFIG_IS_ENABLED = 0, + CONFIG_SILK_IDX = 1, + CONFIG_CLOTH_IDX = 2, + CONFIG_YARN_IDX = 3, + CONFIG_LEATHER_IDX = 4, +}; -private: +static int get_config_val(PersistentDataItem &c, int index) { + if (!c.isValid()) + return -1; + return c.ival(index); +} +static bool get_config_bool(PersistentDataItem &c, int index) { + return get_config_val(c, index) == 1; +} +static void set_config_val(PersistentDataItem &c, int index, int value) { + if (c.isValid()) + c.ival(index) = value; +} +static void set_config_bool(PersistentDataItem &c, int index, bool value) { + set_config_val(c, index, value ? 1 : 0); +} - const std::map jobTypeMap = { - { df::job_type::MakeArmor, df::item_type::ARMOR }, - { df::job_type::MakePants, df::item_type::PANTS }, - { df::job_type::MakeHelm, df::item_type::HELM }, - { df::job_type::MakeGloves, df::item_type::GLOVES }, - { df::job_type::MakeShoes, df::item_type::SHOES } - }; - - const std::map itemTypeMap = { - { df::item_type::ARMOR, df::job_type::MakeArmor }, - { df::item_type::PANTS, df::job_type::MakePants }, - { df::item_type::HELM, df::job_type::MakeHelm }, - { df::item_type::GLOVES, df::job_type::MakeGloves }, - { df::item_type::SHOES, df::job_type::MakeShoes } - }; - -#define F(x) df::item_flags::mask_##x - const df::item_flags bad_flags = { - ( - F(dump) | F(forbid) | F(garbage_collect) | - F(hostile) | F(on_fire) | F(rotten) | F(trader) | - F(in_building) | F(construction) | F(owned) - ) - #undef F - }; - - class MatType { - - public: - std::string name; - df::job_material_category job_material; - df::armor_general_flags armor_flag; - - bool operator==(const MatType& m) const - { - return name == m.name; - } +static const int32_t CYCLE_TICKS = 1200; // one day +static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle - // operator< is required to use this as a std::map key - bool operator<(const MatType& m) const - { - return name < m.name; - } +// ah, if only STL had a bimap +static const std::map jobTypeMap = { + { df::job_type::MakeArmor, df::item_type::ARMOR }, + { df::job_type::MakePants, df::item_type::PANTS }, + { df::job_type::MakeHelm, df::item_type::HELM }, + { df::job_type::MakeGloves, df::item_type::GLOVES }, + { df::job_type::MakeShoes, df::item_type::SHOES } +}; - MatType(std::string& n, df::job_material_category jm, df::armor_general_flags af) - : name(n), job_material(jm), armor_flag(af) {}; - MatType(const char* n, df::job_material_category jm, df::armor_general_flags af) - : name(std::string(n)), job_material(jm), armor_flag(af) {}; +static const std::map itemTypeMap = { + { df::item_type::ARMOR, df::job_type::MakeArmor }, + { df::item_type::PANTS, df::job_type::MakePants }, + { df::item_type::HELM, df::job_type::MakeHelm }, + { df::item_type::GLOVES, df::job_type::MakeGloves }, + { df::item_type::SHOES, df::job_type::MakeShoes } +}; + +class MatType { +public: + const std::string name; + const df::job_material_category job_material; + const df::armor_general_flags armor_flag; + + bool operator==(const MatType& m) const { + return name == m.name; + } - }; + // operator< is required to use this as a std::map key + bool operator<(const MatType& m) const { + return name < m.name; + } - const MatType - M_SILK = MatType("silk", df::job_material_category::mask_silk, df::armor_general_flags::SOFT), - M_CLOTH = MatType("cloth", df::job_material_category::mask_cloth, df::armor_general_flags::SOFT), - M_YARN = MatType("yarn", df::job_material_category::mask_yarn, df::armor_general_flags::SOFT), - M_LEATHER = MatType("leather", df::job_material_category::mask_leather, df::armor_general_flags::LEATHER); + MatType(std::string& n, df::job_material_category jm, df::armor_general_flags af) + : name(n), job_material(jm), armor_flag(af) {}; + MatType(const char* n, df::job_material_category jm, df::armor_general_flags af) + : name(std::string(n)), job_material(jm), armor_flag(af) {}; +}; - std::list all_materials = { M_SILK, M_CLOTH, M_YARN, M_LEATHER }; +static const MatType + M_SILK = MatType("silk", df::job_material_category::mask_silk, df::armor_general_flags::SOFT), + M_CLOTH = MatType("cloth", df::job_material_category::mask_cloth, df::armor_general_flags::SOFT), + M_YARN = MatType("yarn", df::job_material_category::mask_yarn, df::armor_general_flags::SOFT), + M_LEATHER = MatType("leather", df::job_material_category::mask_leather, df::armor_general_flags::LEATHER); + +static const std::list all_materials = { M_SILK, M_CLOTH, M_YARN, M_LEATHER }; +static std::list material_order = all_materials; + +static struct BadFlags { + uint32_t whole; + + BadFlags() { + df::item_flags flags; + #define F(x) flags.bits.x = true; + F(dump); F(forbid); F(garbage_collect); + F(hostile); F(on_fire); F(rotten); F(trader); + F(in_building); F(construction); F(owned); + F(in_chest); F(removed); F(encased); + F(spider_web); + #undef F + whole = flags.whole; + } +} badFlags; +class Tailor { +private: std::map, int> available; // key is item type & size std::map, int> needed; // same std::map, int> queued; // same std::map sizes; // this maps body size to races - std::map, int> orders; // key is item type, item subtype, size std::map supply; - - color_ostream* out; - - std::list material_order = { M_SILK, M_CLOTH, M_YARN, M_LEATHER }; std::map reserves; int default_reserve = 10; +public: void reset() { available.clear(); @@ -145,9 +169,7 @@ private: { for (auto i : world->items.other[df::items_other_id::ANY_GENERIC37]) // GENERIC37 is "clothing" { - if (i->flags.whole & bad_flags.whole) - continue; - if (i->flags.bits.owned) + if (i->flags.whole & badFlags.whole) continue; if (i->getWear() >= 1) continue; @@ -164,7 +186,7 @@ private: for (auto i : world->items.other[df::items_other_id::CLOTH]) { - if (i->flags.whole & bad_flags.whole) + if (i->flags.whole & badFlags.whole) continue; if (require_dyed && !i->hasImprovements()) @@ -197,7 +219,7 @@ private: for (auto i : world->items.other[df::items_other_id::SKIN_TANNED]) { - if (i->flags.whole & bad_flags.whole) + if (i->flags.whole & badFlags.whole) continue; supply[M_LEATHER] += i->getStackSize(); } @@ -369,8 +391,9 @@ private: } - void place_orders() + int place_orders() { + int ordered = 0; auto entity = world->entities.all[plotinfo->civ_id]; for (auto& o : orders) @@ -477,6 +500,7 @@ private: ); count -= c; + ordered += c; } else { @@ -486,215 +510,217 @@ private: } } } + return ordered; } +}; -public: - void do_scan(color_ostream& o) - { - out = &o; - - reset(); - - // scan for useable clothing +static std::unique_ptr tailor_instance; - scan_clothing(); +static command_result do_command(color_ostream &out, vector ¶meters); +static int do_cycle(color_ostream &out); - // scan for clothing raw materials +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { + DEBUG(config,out).print("initializing %s\n", plugin_name); - scan_materials(); + tailor_instance = dts::make_unique(); - // scan for units who need replacement clothing + // provide a configuration interface for the plugin + commands.push_back(PluginCommand( + plugin_name, + "Automatically keep your dwarves in fresh clothing.", + do_command)); - scan_replacements(); + return CR_OK; +} - // create new orders +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { + if (!Core::getInstance().isWorldLoaded()) { + out.printerr("Cannot enable %s without a loaded world.\n", plugin_name); + return CR_FAILURE; + } - create_orders(); + if (enable != is_enabled) { + is_enabled = enable; + DEBUG(config,out).print("%s from the API; persisting\n", + is_enabled ? "enabled" : "disabled"); + set_config_bool(config, CONFIG_IS_ENABLED, is_enabled); + if (enable) + do_cycle(out); + } else { + DEBUG(config,out).print("%s from the API, but already %s; no action\n", + is_enabled ? "enabled" : "disabled", + is_enabled ? "enabled" : "disabled"); + } + return CR_OK; +} - // scan existing orders and subtract +DFhackCExport command_result plugin_shutdown (color_ostream &out) { + DEBUG(config,out).print("shutting down %s\n", plugin_name); - scan_existing_orders(); + tailor_instance.release(); - // place orders + return CR_OK; +} - place_orders(); +static void set_material_order() { + material_order.clear(); + for (size_t i = 0; i < all_materials.size(); ++i) { + if (i == (size_t)get_config_val(config, CONFIG_SILK_IDX)) + material_order.push_back(M_SILK); + else if (i == (size_t)get_config_val(config, CONFIG_CLOTH_IDX)) + material_order.push_back(M_CLOTH); + else if (i == (size_t)get_config_val(config, CONFIG_YARN_IDX)) + material_order.push_back(M_YARN); + else if (i == (size_t)get_config_val(config, CONFIG_LEATHER_IDX)) + material_order.push_back(M_LEATHER); } + if (!material_order.size()) + std::copy(all_materials.begin(), all_materials.end(), std::back_inserter(material_order)); +} -public: - command_result set_materials(color_ostream& out, std::vector& parameters) - { - std::list newmat; - newmat.clear(); - - for (auto m = parameters.begin() + 1; m != parameters.end(); m++) - { - auto nameMatch = [m](MatType& m1) { return *m == m1.name; }; - auto mm = std::find_if(all_materials.begin(), all_materials.end(), nameMatch); - if (mm == all_materials.end()) - { - WARN(config,out).print("tailor: material %s not recognized\n", m->c_str()); - return CR_WRONG_USAGE; - } - else { - newmat.push_back(*mm); - } - } - - material_order = newmat; - INFO(config,out).print("tailor: material list set to %s\n", get_material_list().c_str()); +DFhackCExport command_result plugin_load_data (color_ostream &out) { + cycle_timestamp = 0; + config = World::GetPersistentData(CONFIG_KEY); - return CR_OK; + if (!config.isValid()) { + DEBUG(config,out).print("no config found in this save; initializing\n"); + config = World::AddPersistentData(CONFIG_KEY); + set_config_bool(config, CONFIG_IS_ENABLED, is_enabled); } -public: - std::string get_material_list() - { - std::string s; - for (const auto& m : material_order) - { - if (!s.empty()) s += ", "; - s += m.name; - } - return s; - } + is_enabled = get_config_bool(config, CONFIG_IS_ENABLED); + DEBUG(config,out).print("loading persisted enabled state: %s\n", + is_enabled ? "true" : "false"); + set_material_order(); -public: - void process(color_ostream& out) - { - bool found = false; + return CR_OK; +} - for (df::job_list_link* link = &world->jobs.list; link != NULL; link = link->next) - { - if (link->item == NULL) continue; - if (link->item->job_type == df::enums::job_type::UpdateStockpileRecords) - { - found = true; - break; - } +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { + if (event == DFHack::SC_WORLD_UNLOADED) { + if (is_enabled) { + DEBUG(config,out).print("world unloaded; disabling %s\n", + plugin_name); + is_enabled = false; } + } + return CR_OK; +} - if (found) - { - do_scan(out); - } +DFhackCExport command_result plugin_onupdate(color_ostream &out) { + if (is_enabled && world->frame_counter - cycle_timestamp >= CYCLE_TICKS) { + int ordered = do_cycle(out); + if (0 < ordered) + out.print("tailor: ordered %d items of clothing\n", ordered); } -}; + return CR_OK; +} -static std::unique_ptr tailor_instance; +static bool call_tailor_lua(color_ostream *out, const char *fn_name, + int nargs = 0, int nres = 0, + Lua::LuaLambda && args_lambda = Lua::DEFAULT_LUA_LAMBDA, + Lua::LuaLambda && res_lambda = Lua::DEFAULT_LUA_LAMBDA) { + DEBUG(config).print("calling tailor lua function: '%s'\n", fn_name); -#define DELTA_TICKS 50 + CoreSuspender guard; -DFhackCExport command_result plugin_onupdate(color_ostream& out) -{ - if (!enabled || !tailor_instance) - return CR_OK; + auto L = Lua::Core::State; + Lua::StackUnwinder top(L); - if (!Maps::IsValid()) - return CR_OK; + if (!out) + out = &Core::getInstance().getConsole(); - if (DFHack::World::ReadPauseState()) - return CR_OK; + return Lua::CallLuaModuleFunction(*out, L, "plugins.tailor", fn_name, + nargs, nres, + std::forward(args_lambda), + std::forward(res_lambda)); +} - if (world->frame_counter % DELTA_TICKS != 0) - return CR_OK; +static command_result do_command(color_ostream &out, vector ¶meters) { + CoreSuspender suspend; - { - CoreSuspender suspend; - tailor_instance->process(out); + if (!Core::getInstance().isWorldLoaded()) { + out.printerr("Cannot run %s without a loaded world.\n", plugin_name); + return CR_FAILURE; } - return CR_OK; + bool show_help = false; + if (!call_tailor_lua(&out, "parse_commandline", parameters.size(), 1, + [&](lua_State *L) { + for (const string ¶m : parameters) + Lua::Push(L, param); + }, + [&](lua_State *L) { + show_help = !lua_toboolean(L, -1); + })) { + return CR_FAILURE; + } + + return show_help ? CR_WRONG_USAGE : CR_OK; } -static command_result tailor_cmd(color_ostream& out, std::vector & parameters) { - bool desired = enabled; - if (parameters.size() == 1 && (parameters[0] == "enable" || parameters[0] == "on" || parameters[0] == "1")) - { - desired = true; - } - else if (parameters.size() == 1 && (parameters[0] == "disable" || parameters[0] == "off" || parameters[0] == "0")) - { - desired = false; - } - else if (parameters.size() == 1 && (parameters[0] == "usage" || parameters[0] == "help" || parameters[0] == "?")) - { - return CR_WRONG_USAGE; - } - else if (parameters.size() == 1 && parameters[0] == "test") - { - if (tailor_instance) - { - tailor_instance->do_scan(out); - return CR_OK; - } - else - { - out.print("%s: not instantiated\n", plugin_name); - return CR_FAILURE; - } - } - else if (parameters.size() > 1 && parameters[0] == "materials") - { - if (tailor_instance) - { - return tailor_instance->set_materials(out, parameters); - } - else - { - out.print("%s: not instantiated\n", plugin_name); - return CR_FAILURE; - } - } - else if (parameters.size() == 1 && parameters[0] != "status") - { - return CR_WRONG_USAGE; - } +///////////////////////////////////////////////////// +// cycle logic +// - out.print("Tailor is %s %s.\n", (desired == enabled) ? "currently" : "now", desired ? "enabled" : "disabled"); - if (tailor_instance) - { - out.print("Material list is: %s\n", tailor_instance->get_material_list().c_str()); - } - else - { - out.print("%s: not instantiated\n", plugin_name); - } +static int do_cycle(color_ostream &out) { + // mark that we have recently run + cycle_timestamp = world->frame_counter; - enabled = desired; + DEBUG(cycle,out).print("running %s cycle\n", plugin_name); - return CR_OK; + tailor_instance->reset(); + tailor_instance->scan_clothing(); + tailor_instance->scan_materials(); + tailor_instance->scan_replacements(); + tailor_instance->create_orders(); + tailor_instance->scan_existing_orders(); + return tailor_instance->place_orders(); } +///////////////////////////////////////////////////// +// Lua API +// -DFhackCExport command_result plugin_onstatechange(color_ostream& out, state_change_event event) -{ - return CR_OK; +static void tailor_doCycle(color_ostream &out) { + DEBUG(config,out).print("entering tailor_doCycle\n"); + out.print("ordered %d items of clothing\n", do_cycle(out)); } -DFhackCExport command_result plugin_enable(color_ostream& out, bool enable) -{ - enabled = enable; - return CR_OK; -} +// remember, these are ONE-based indices from Lua +static void tailor_setMaterialPreferences(color_ostream &out, int32_t silkIdx, + int32_t clothIdx, int32_t yarnIdx, int32_t leatherIdx) { + DEBUG(config,out).print("entering tailor_setMaterialPreferences\n"); -DFhackCExport command_result plugin_init(color_ostream& out, std::vector & commands) -{ - tailor_instance = std::move(dts::make_unique()); + // it doesn't really matter if these are invalid. set_material_order will do + // the right thing. + set_config_val(config, CONFIG_SILK_IDX, silkIdx - 1); + set_config_val(config, CONFIG_CLOTH_IDX, clothIdx - 1); + set_config_val(config, CONFIG_YARN_IDX, yarnIdx - 1); + set_config_val(config, CONFIG_LEATHER_IDX, leatherIdx - 1); - if (AUTOENABLE) { - enabled = true; - } + set_material_order(); +} - commands.push_back(PluginCommand( - plugin_name, - "Automatically keep your dwarves in fresh clothing.", - tailor_cmd)); - return CR_OK; +static int tailor_getMaterialPreferences(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(config,*out).print("entering tailor_getMaterialPreferences\n"); + vector names; + for (const auto& m : material_order) + names.emplace_back(m.name); + Lua::PushVector(L, names); + return 1; } -DFhackCExport command_result plugin_shutdown(color_ostream& out) -{ - tailor_instance.release(); +DFHACK_PLUGIN_LUA_FUNCTIONS { + DFHACK_LUA_FUNCTION(tailor_doCycle), + DFHACK_LUA_FUNCTION(tailor_setMaterialPreferences), + DFHACK_LUA_END +}; - return plugin_enable(out, false); -} +DFHACK_PLUGIN_LUA_COMMANDS { + DFHACK_LUA_COMMAND(tailor_getMaterialPreferences), + DFHACK_LUA_END +}; diff --git a/scripts b/scripts index 112d69ed3..8807e7a78 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 112d69ed3e0efb22e3ec6a6c8e58ad3135b80351 +Subproject commit 8807e7a7845611b2cc613fb3903f6f8763e91300