From 5ffbb4d0efc01edeee6d404a9e7843d0cc289eef Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 18 Apr 2023 08:22:52 -0700 Subject: [PATCH] unify and update automelt/autotrade/autodump --- data/init/dfhack.tools.init | 23 +- docs/about/Removed.rst | 23 +- docs/plugins/automelt.rst | 55 -- docs/plugins/autotrade.rst | 18 - docs/plugins/logistics.rst | 71 ++ docs/plugins/stockpiles.rst | 13 +- plugins/CMakeLists.txt | 5 +- plugins/autodump.cpp | 230 ------ plugins/automelt.cpp | 799 --------------------- plugins/autotrade.cpp | 467 ------------ plugins/logistics.cpp | 676 +++++++++++++++++ plugins/lua/automelt.lua | 87 --- plugins/lua/logistics.lua | 146 ++++ plugins/lua/stockpiles.lua | 9 +- plugins/stockpiles/OrganicMatLookup.cpp | 5 +- plugins/stockpiles/StockpileSerializer.cpp | 27 +- plugins/stockpiles/StockpileSerializer.h | 2 + plugins/stockpiles/proto/stockpiles.proto | 5 + plugins/stockpiles/stockpiles.cpp | 14 +- 19 files changed, 973 insertions(+), 1702 deletions(-) delete mode 100644 docs/plugins/automelt.rst delete mode 100644 docs/plugins/autotrade.rst create mode 100644 docs/plugins/logistics.rst delete mode 100644 plugins/automelt.cpp delete mode 100644 plugins/autotrade.cpp create mode 100644 plugins/logistics.cpp delete mode 100644 plugins/lua/automelt.lua create mode 100644 plugins/lua/logistics.lua diff --git a/data/init/dfhack.tools.init b/data/init/dfhack.tools.init index 9d8c0dd33..38221883a 100644 --- a/data/init/dfhack.tools.init +++ b/data/init/dfhack.tools.init @@ -78,14 +78,11 @@ # Display DFHack version on title screen #enable title-version -# Allow DFHack tools to overlay functionality and information on the DF screen -enable overlay - -# Allow buildings to be placed now and built later when materials are available +# Enable system services enable buildingplan - -#Allow designated stockpiles to automatically mark items for melting -enable automelt +enable confirm +enable logistics +enable overlay # Dwarf Manipulator (simple in-game Dwarf Therapist replacement) #enable manipulator @@ -97,28 +94,18 @@ enable automelt #enable automaterial # Other interface improvement tools -enable \ - confirm # dwarfmonitor \ # mousequery \ # autogems \ -# autodump \ -# automelt \ -# autotrade \ -# buildingplan \ # trackstop \ # zone \ # stocks \ -# autochop \ -# stockpiles +# #end a line with a backslash to make it continue to the next line. The \ is deleted for the final command. # Multiline commands are ONLY supported for scripts like dfhack.init. You cannot do multiline command manually on the DFHack console. # You cannot extend a commented line. # You can comment out the extension of a line. -# enable mouse controls and sand indicator in embark screen -#embark-tools enable sticky sand mouse - # enable option to enter embark assistant #enable embark-assistant diff --git a/docs/about/Removed.rst b/docs/about/Removed.rst index 174fc56c4..d402eefb6 100644 --- a/docs/about/Removed.rst +++ b/docs/about/Removed.rst @@ -26,6 +26,20 @@ Moved frequently used materials to the top of the materials list when building buildings. Also offered extended options when building constructions. All functionality has been merged into `buildingplan`. +.. _automelt: + +automelt +======== +Automatically mark items for melting when they are brought to a monitored +stockpile. Merged into `logistics`. + +.. _autotrade: + +autotrade +========= +Automatically mark items for trading when they are brought to a monitored +stockpile. Merged into `logistics`. + .. _autounsuspend: autounsuspend @@ -56,7 +70,7 @@ Replaced by `gui/launcher --minimal `. create-items ============ -Replaced by `gui/create-item --multi `. +Replaced by `gui/create-item`. .. _deteriorateclothes: @@ -172,6 +186,13 @@ Tool that warned the user when the ``dfhack.init`` file did not exist. Now that ``dfhack.init`` is autogenerated in ``dfhack-config/init``, this warning is no longer necessary. +.. _gui/stockpiles: + +gui/stockpiles +============== +Provided import/export dialogs. Converted to an `overlay` that displays when +a stockpile is selected. + .. _masspit: masspit diff --git a/docs/plugins/automelt.rst b/docs/plugins/automelt.rst deleted file mode 100644 index f652e52b8..000000000 --- a/docs/plugins/automelt.rst +++ /dev/null @@ -1,55 +0,0 @@ -automelt -======== - -.. dfhack-tool:: - :summary: Quickly designate items to be melted. - :tags: fort productivity items stockpiles - :no-command: - -Automelt checks monitor-enabled stockpiles once every in-game day, and will mark valid items for melting. - -Please see `gui/automelt` for the interactive configuration dialog. - -Usage ------ - -:: - - enable automelt - automelt [status] - automelt designate - automelt (monitor|nomonitor) [,...] - -Examples --------- - -Automatically monitor stockpile ("melt"), marking new valid items for melting. This also immediately marks all present items for melting:: - - enable automelt - automelt monitor melt - -Enable monitoring for ("Stockpile #52"), which has not been given a custom name:: - - automelt monitor "Stockpile #52" - -Enable monitoring for ("Stockpile #52"), which has not been given a custom name:: - - automelt monitor 52 - -Enable monitoring for multiple stockpiles ("Stockpile #52", "Stockpile #35", and "melt"):: - - automelt monitor 52,"Stockpile #35",melt - -Commands --------- - -``status`` - Shows current configuration and relevant statistics - -``designate`` - Designates items in monitored stockpiles for melting right now. This works even if ``automelt`` is not currently enabled. - -``(no)monitor `` - Enable/disable monitoring of a given stockpile. Works with either the stockpile's name (if set) or ID. - If the stockpile has no custom name set, you may designate it by either the full name as reported by - the status command, or by just the number. diff --git a/docs/plugins/autotrade.rst b/docs/plugins/autotrade.rst deleted file mode 100644 index 24c1b42fa..000000000 --- a/docs/plugins/autotrade.rst +++ /dev/null @@ -1,18 +0,0 @@ -autotrade -========= - -.. dfhack-tool:: - :summary: Quickly designate items to be traded. - :tags: unavailable fort productivity items stockpiles - :no-command: - -When `enabled `, this plugin adds an option to the :kbd:`q` menu for -stockpiles. When the ``autotrade`` option is selected for the stockpile, any -items placed in the stockpile will automatically be designated to be traded. - -Usage ------ - -:: - - enable autotrade diff --git a/docs/plugins/logistics.rst b/docs/plugins/logistics.rst new file mode 100644 index 000000000..59f71ab8e --- /dev/null +++ b/docs/plugins/logistics.rst @@ -0,0 +1,71 @@ +logistics +========= + +.. dfhack-tool:: + :summary: Automatically mark and route items in monitored stockpiles. + :tags: fort productivity items stockpiles + :no-command: + +Commands act upon the stockpile selected in the UI unless another stockpile +identifier is specified on the commandline. + +When the plugin is enabled, it checks stockpiles marked with automelt, +autotrade, and/or autodump features twice every in-game day, and will mark valid +items in those stockpiles for melting, trading, and/or dumping, respectively. +Note that items will only be marked for trading if a caravan is approaching or +is already at the trade depot. + +Please see `gui/logistics` for the interactive status and configuration dialog. + +Usage +----- + +:: + + enable logistics + logistics [status] + logistics now + logistics add [melt] [trade] [dump] [] + logistics clear [all] [] + +Examples +-------- + +``logistics`` + Print a summary of all your stockpiles, their ``logistics`` configuration, + and the number of items that are designated (or can be designated) by each + of the ``logistics`` processors. + +``logistics now`` + Designate items in monitored stockpiles according to the current + configuration. This works regardless of whether ``logistics`` is currently + enabled. + +``logistics add melt`` + Register the currently selected stockpile for automelting. Meltable items + that are brought to this stockpile will be designated for melting. + +``logistics add melt trade -s goblinite`` + Register the stockpile(s) named "goblinite" for automatic melting and + automatic trading. Items will be marked for melting, but any items still in + the stockpile when a caravan shows up will be brought to the trade depot + for trading. + +``logistics clear`` + Unregisters the currently selected stockpile from any monitoring. Any + currently designated items will remain designated. + +``logistics clear -s 12,15,goblinite`` + Unregisters the stockpiles with stockpile numbers 12 and 15, along with any + stockpiles named "goblinite", from any monitoring. + +``logistics clear all`` + Unregister all stockpiles from any monitoring. + +Options +------- + +``-s``, ``--stockpile [,...]`` + Causes the command to act upon stockpiles with the given names or numbers + instead of the stockpile that is currently selected in the UI. Note that + the numbers are the stockpile numbers, not the building ids. diff --git a/docs/plugins/stockpiles.rst b/docs/plugins/stockpiles.rst index cc1f3c016..0fed3e25f 100644 --- a/docs/plugins/stockpiles.rst +++ b/docs/plugins/stockpiles.rst @@ -5,8 +5,8 @@ stockpiles :summary: Import, export, or modify stockpile settings and features. :tags: fort design productivity stockpiles -If you are importing or exporting setting and don't want to specify a building -ID, select a stockpile in the UI before running the command. +Commands act upon the stockpile selected in the UI unless another stockpile +identifier is specified on the commandline. Usage ----- @@ -15,8 +15,8 @@ Usage stockpiles [status] stockpiles list [] - stockpiles export [] stockpiles import [] + stockpiles export [] Exported stockpile settings are saved in the ``dfhack-config/stockpiles`` folder, where you can view and delete them, if desired. Names can only @@ -61,9 +61,9 @@ Examples Options ------- -``-s``, ``--stockpile `` - Specify a specific stockpile ID instead of using the one currently selected - in the UI. +``-s``, ``--stockpile `` + Specify a specific stockpile by name or internal ID instead of using the + stockpile currently selected in the UI. ``-i``, ``--include `` When exporting, you can include this option to select only specific elements of the stockpile to record. If not specified, everything is included. When @@ -94,6 +94,7 @@ file are: :types: The elements below the categories, which include the sub-categories, the specific item types, and any toggles the category might have (like Prepared meals for the Food category). +:features: DFHack `logistics` features. .. _stockpiles-library: diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 3ae78d320..4029b8e2e 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -83,9 +83,8 @@ dfhack_plugin(autodump autodump.cpp) dfhack_plugin(autofarm autofarm.cpp) #dfhack_plugin(autogems autogems.cpp LINK_LIBRARIES jsoncpp_static) add_subdirectory(autolabor) -dfhack_plugin(automelt automelt.cpp LINK_LIBRARIES lua) dfhack_plugin(autonestbox autonestbox.cpp LINK_LIBRARIES lua) -#dfhack_plugin(autotrade autotrade.cpp) +dfhack_plugin(autoslab autoslab.cpp) dfhack_plugin(blueprint blueprint.cpp LINK_LIBRARIES lua) #dfhack_plugin(burrows burrows.cpp LINK_LIBRARIES lua) #dfhack_plugin(building-hacks building-hacks.cpp LINK_LIBRARIES lua) @@ -129,13 +128,13 @@ dfhack_plugin(hotkeys hotkeys.cpp LINK_LIBRARIES lua) dfhack_plugin(lair lair.cpp) dfhack_plugin(liquids liquids.cpp Brushes.h LINK_LIBRARIES lua) dfhack_plugin(luasocket luasocket.cpp LINK_LIBRARIES clsocket lua dfhack-tinythread) +dfhack_plugin(logistics logistics.cpp LINK_LIBRARIES lua) #dfhack_plugin(manipulator manipulator.cpp) #dfhack_plugin(map-render map-render.cpp LINK_LIBRARIES lua) dfhack_plugin(misery misery.cpp LINK_LIBRARIES lua) #dfhack_plugin(mode mode.cpp) #dfhack_plugin(mousequery mousequery.cpp) dfhack_plugin(nestboxes nestboxes.cpp) -dfhack_plugin(autoslab autoslab.cpp) dfhack_plugin(orders orders.cpp LINK_LIBRARIES jsoncpp_static lua) dfhack_plugin(overlay overlay.cpp LINK_LIBRARIES lua) dfhack_plugin(pathable pathable.cpp LINK_LIBRARIES lua) diff --git a/plugins/autodump.cpp b/plugins/autodump.cpp index 839b3b727..a214f5c94 100644 --- a/plugins/autodump.cpp +++ b/plugins/autodump.cpp @@ -41,236 +41,6 @@ DFHACK_PLUGIN("autodump"); REQUIRE_GLOBAL(gps); REQUIRE_GLOBAL(world); -/* TODO: merge with stockpiles plugin -// Stockpile interface START -static const string PERSISTENCE_KEY = "autodump/stockpiles"; - -static void mark_all_in_stockpiles(vector &stockpiles) -{ - std::vector &items = world->items.other[items_other_id::IN_PLAY]; - - // Precompute a bitmask with the bad flags - df::item_flags bad_flags; - bad_flags.whole = 0; - -#define F(x) bad_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(artifact); - F(spider_web); F(owned); F(in_job); -#undef F - - size_t marked_count = 0; - for (size_t i = 0; i < items.size(); i++) - { - df::item *item = items[i]; - if (item->flags.whole & bad_flags.whole) - continue; - - for (auto it = stockpiles.begin(); it != stockpiles.end(); it++) - { - if (!it->inStockpile(item)) - continue; - - ++marked_count; - item->flags.bits.dump = true; - } - } - - if (marked_count) - Gui::showAnnouncement("Marked " + int_to_string(marked_count) + " items to dump", COLOR_GREEN, false); -} - -class StockpileMonitor -{ -public: - bool isMonitored(df::building_stockpilest *sp) - { - for (auto it = monitored_stockpiles.begin(); it != monitored_stockpiles.end(); it++) - { - if (it->matches(sp)) - return true; - } - - return false; - } - - void add(df::building_stockpilest *sp) - { - auto pile = PersistentStockpileInfo(sp, PERSISTENCE_KEY); - if (pile.isValid()) - { - monitored_stockpiles.push_back(PersistentStockpileInfo(pile)); - monitored_stockpiles.back().save(); - } - } - - void remove(df::building_stockpilest *sp) - { - for (auto it = monitored_stockpiles.begin(); it != monitored_stockpiles.end(); it++) - { - if (it->matches(sp)) - { - it->remove(); - monitored_stockpiles.erase(it); - break; - } - } - } - - void doCycle() - { - for (auto it = monitored_stockpiles.begin(); it != monitored_stockpiles.end();) - { - if (!it->isValid()) - it = monitored_stockpiles.erase(it); - else - ++it; - } - - mark_all_in_stockpiles(monitored_stockpiles); - } - - void reset() - { - monitored_stockpiles.clear(); - std::vector items; - DFHack::World::GetPersistentData(&items, PERSISTENCE_KEY); - - for (auto i = items.begin(); i != items.end(); i++) - { - auto pile = PersistentStockpileInfo(*i, PERSISTENCE_KEY); - if (pile.load()) - monitored_stockpiles.push_back(PersistentStockpileInfo(pile)); - else - pile.remove(); - } - } - - -private: - vector monitored_stockpiles; -}; - -static StockpileMonitor monitor; - -#define DELTA_TICKS 620 - - -DFhackCExport command_result plugin_onupdate ( color_ostream &out ) -{ - if(!Maps::IsValid()) - return CR_OK; - - if (DFHack::World::ReadPauseState()) - return CR_OK; - - if (world->frame_counter % DELTA_TICKS != 0) - return CR_OK; - - monitor.doCycle(); - - return CR_OK; -} - -struct dump_hook : public df::viewscreen_dwarfmodest -{ - typedef df::viewscreen_dwarfmodest interpose_base; - - bool handleInput(set *input) - { - if (Gui::inRenameBuilding()) - return false; - - building_stockpilest *sp = get_selected_stockpile(); - if (!sp) - return false; - - if (input->count(interface_key::CUSTOM_SHIFT_D)) - { - if (monitor.isMonitored(sp)) - monitor.remove(sp); - else - monitor.add(sp); - } - - return false; - } - - DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) - { - if (!handleInput(input)) - INTERPOSE_NEXT(feed)(input); - } - - DEFINE_VMETHOD_INTERPOSE(void, render, ()) - { - INTERPOSE_NEXT(render)(); - - building_stockpilest *sp = get_selected_stockpile(); - if (!sp) - return; - - auto dims = Gui::getDwarfmodeViewDims(); - int left_margin = dims.menu_x1 + 1; - int x = left_margin; - int y = dims.y2 - 7; - - int links = 0; - links += sp->links.give_to_pile.size(); - links += sp->links.take_from_pile.size(); - links += sp->links.give_to_workshop.size(); - links += sp->links.take_from_workshop.size(); - bool state = monitor.isMonitored(sp); - - if (links + 12 >= y) { - y = dims.y2; - OutputString(COLOR_WHITE, x, y, "Auto: "); - OutputString(COLOR_LIGHTRED, x, y, "D"); - OutputString(state? COLOR_LIGHTGREEN: COLOR_GREY, x, y, "ump "); - } else { - OutputToggleString(x, y, "Auto dump", "D", state, true, left_margin, COLOR_WHITE, COLOR_LIGHTRED); - } - } -}; - -IMPLEMENT_VMETHOD_INTERPOSE(dump_hook, feed); -IMPLEMENT_VMETHOD_INTERPOSE(dump_hook, render); - -DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) -{ - switch (event) - { - case DFHack::SC_MAP_LOADED: - monitor.reset(); - break; - case DFHack::SC_MAP_UNLOADED: - break; - default: - break; - } - return CR_OK; -} - -DFHACK_PLUGIN_IS_ENABLED(is_enabled); - -DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) -{ - if (enable != is_enabled) - { - if (!INTERPOSE_HOOK(dump_hook, feed).apply(enable) || - !INTERPOSE_HOOK(dump_hook, render).apply(enable)) - return CR_FAILURE; - - is_enabled = enable; - } - - return CR_OK; -} - -// Stockpile interface END -*/ - command_result df_autodump(color_ostream &out, vector & parameters); command_result df_autodump_destroy_here(color_ostream &out, vector & parameters); command_result df_autodump_destroy_item(color_ostream &out, vector & parameters); diff --git a/plugins/automelt.cpp b/plugins/automelt.cpp deleted file mode 100644 index dc0bc0f79..000000000 --- a/plugins/automelt.cpp +++ /dev/null @@ -1,799 +0,0 @@ -#include "Debug.h" -#include "LuaTools.h" -#include "PluginManager.h" - -#include "modules/Buildings.h" -#include "modules/Items.h" -#include "modules/World.h" -#include "modules/Persistence.h" -#include "modules/Gui.h" - -#include "df/world.h" -#include "df/building_stockpilest.h" -#include "df/item_quality.h" - -#include -#include - -using std::map; -using std::string; -using std::unordered_map; -using std::vector; - -using namespace DFHack; -using namespace df::enums; - -DFHACK_PLUGIN("automelt"); -DFHACK_PLUGIN_IS_ENABLED(is_enabled); -REQUIRE_GLOBAL(world); - -namespace DFHack -{ - DBG_DECLARE(automelt, status, DebugCategory::LINFO); - DBG_DECLARE(automelt, cycle, DebugCategory::LINFO); - DBG_DECLARE(automelt, perf, DebugCategory::LINFO); -} - -static const string CONFIG_KEY = string(plugin_name) + "/config"; -static const string STOCKPILE_CONFIG_KEY_PREFIX = string(plugin_name) + "/stockpile/"; -static PersistentDataItem config; - -static unordered_map watched_stockpiles; - -enum StockpileConfigValues -{ - STOCKPILE_CONFIG_ID = 0, - STOCKPILE_CONFIG_MONITORED = 1, -}; - -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); -} - -static PersistentDataItem &ensure_stockpile_config(color_ostream &out, int id) -{ - DEBUG(cycle,out).print("ensuring stockpile config id=%d\n", id); - if (watched_stockpiles.count(id)){ - DEBUG(cycle,out).print("stockpile exists in watched_indices\n"); - return watched_stockpiles[id]; - } - - string keyname = STOCKPILE_CONFIG_KEY_PREFIX + int_to_string(id); - DEBUG(status,out).print("creating new persistent key for stockpile %d\n", id); - watched_stockpiles.emplace(id, World::GetPersistentData(keyname, NULL)); - return watched_stockpiles[id]; -} - -static void remove_stockpile_config(color_ostream &out, int id) -{ - if (!watched_stockpiles.count(id)) - return; - DEBUG(status, out).print("removing persistent key for stockpile %d\n", id); - World::DeletePersistentData(watched_stockpiles[id]); - watched_stockpiles.erase(id); -} - -static bool isStockpile(df::building * bld) { - return bld && bld->getType() == df::building_type::Stockpile; -} - -static void validate_stockpile_configs(color_ostream &out) -{ - for (auto &c : watched_stockpiles) { - int id = get_config_val(c.second, STOCKPILE_CONFIG_ID); - auto bld = df::building::find(id); - if (!isStockpile(bld)) - remove_stockpile_config(out, id); - } -} - -static const int32_t CYCLE_TICKS = 1200; -static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle - -static command_result do_command(color_ostream &out, vector ¶meters); -static int32_t do_cycle(color_ostream &out); - -DFhackCExport command_result plugin_init(color_ostream &out, vector &commands) -{ - DEBUG(status, out).print("initializing %s\n", plugin_name); - - // provide a configuration interface for the plugin - commands.push_back(PluginCommand( - plugin_name, - "Auto melt items in designated stockpiles.", - do_command)); - - return CR_OK; -} - -DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) -{ - if (enable != is_enabled) - { - is_enabled = enable; - DEBUG(status, out).print("%s from the API; persisting\n", is_enabled ? "enabled" : "disabled"); - } - else - { - DEBUG(status, out).print("%s from the API, but already %s; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); - } - return CR_OK; -} - -DFhackCExport command_result plugin_shutdown(color_ostream &out) -{ - DEBUG(status, out).print("shutting down %s\n", plugin_name); - - return CR_OK; -} - -DFhackCExport command_result plugin_load_data(color_ostream &out) -{ - cycle_timestamp = 0; - config = World::GetPersistentData(CONFIG_KEY); - - if (!config.isValid()) - { - DEBUG(status, out).print("no config found in this save; initializing\n"); - config = World::AddPersistentData(CONFIG_KEY); - } - - DEBUG(status, out).print("loading persisted enabled state: %s\n", is_enabled ? "true" : "false"); - vector loaded_persist_data; - World::GetPersistentData(&loaded_persist_data, STOCKPILE_CONFIG_KEY_PREFIX, true); - watched_stockpiles.clear(); - const size_t num_watched_stockpiles = loaded_persist_data.size(); - for (size_t idx = 0; idx < num_watched_stockpiles; ++idx) - { - auto &c = loaded_persist_data[idx]; - watched_stockpiles.emplace(get_config_val(c, STOCKPILE_CONFIG_ID), c); - } - validate_stockpile_configs(out); - - return CR_OK; -} - -DFhackCExport command_result plugin_onupdate(color_ostream &out) -{ - if (!Core::getInstance().isWorldLoaded()) - return CR_OK; - if (is_enabled && world->frame_counter - cycle_timestamp >= CYCLE_TICKS) - { - int32_t marked = do_cycle(out); - if (0 < marked) - out.print("automelt: marked %d item(s) for melting\n", marked); - } - return CR_OK; -} - -static bool call_automelt_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(status).print("calling automelt lua function: '%s'\n", fn_name); - - CoreSuspender guard; - - auto L = Lua::Core::State; - Lua::StackUnwinder top(L); - - if (!out) - out = &Core::getInstance().getConsole(); - - return Lua::CallLuaModuleFunction(*out, L, "plugins.automelt", fn_name, - nargs, nres, - std::forward(args_lambda), - std::forward(res_lambda)); -} - -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; - } - - bool show_help = false; - if (!call_automelt_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 inline bool is_metal_item(df::item *item) -{ - if (!item) - return false; - MaterialInfo mat(item); - return (mat.getCraftClass() == craft_material_class::Metal); -} - -struct BadFlagsCanMelt { - uint32_t whole; - - BadFlagsCanMelt() { - 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(artifact); - F(spider_web); F(owned); F(in_job); - #undef F - whole = flags.whole; - } -}; - -struct BadFlagsMarkItem { - uint32_t whole; - - BadFlagsMarkItem() { - 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(artifact); - F(spider_web); F(owned); - #undef F - whole = flags.whole; - } -}; - -// Copied from Kelly Martin's code -static inline bool can_melt(df::item *item) -{ - static const BadFlagsCanMelt bad_flags; - - if (!is_metal_item(item)) - return false; - - if (item->flags.whole & bad_flags.whole) - return false; - - df::item_type t = item->getType(); - - if (t == df::enums::item_type::BAR) - return false; - - for (auto &g : item->general_refs) - { - switch (g->getType()) - { - case general_ref_type::CONTAINS_ITEM: - case general_ref_type::UNIT_HOLDER: - case general_ref_type::CONTAINS_UNIT: - return false; - case general_ref_type::CONTAINED_IN_ITEM: - { - df::item *c = g->getItem(); - for (auto &gg : c->general_refs) - { - if (gg->getType() == general_ref_type::UNIT_HOLDER) - return false; - } - } - break; - default: - break; - } - } - - if (item->getQuality() >= item_quality::Masterful) - return false; - - return true; -} - -static inline bool is_set_to_melt(df::item *item) -{ - return item->flags.bits.melt; -} - -static int mark_item(color_ostream &out, df::item *item, BadFlagsMarkItem bad_flags, int32_t stockpile_id, - int32_t &premarked_item_count, int32_t &item_count, map &tracked_item_map, bool should_melt) -{ - DEBUG(perf,out).print("%s running mark_item: should_melt=%d\n", plugin_name, should_melt); - - if (DBG_NAME(perf).isEnabled(DebugCategory::LDEBUG)) { - string name = ""; - item->getItemDescription(&name, 0); - DEBUG(perf,out).print("item %s %d\n", name.c_str(), item->id); - } - - if (item->flags.whole & bad_flags.whole){ - DEBUG(perf,out).print("rejected flag check\n"); - return 0; - } - - if (item->isAssignedToThisStockpile(stockpile_id)) - { - DEBUG(perf,out).print("assignedToStockpile\n"); - size_t marked_count = 0; - vector contents; - Items::getContainedItems(item, &contents); - for (auto child = contents.begin(); child != contents.end(); child++) - { - DEBUG(perf,out).print("inside child loop\n"); - marked_count += mark_item(out, *child, bad_flags, stockpile_id, premarked_item_count, item_count, tracked_item_map, should_melt); - } - return marked_count; - } - - DEBUG(perf,out).print("past recurse loop\n"); - - if (is_set_to_melt(item)) { - DEBUG(perf,out).print("already set to melt\n"); - tracked_item_map.emplace(item->id, true); - premarked_item_count++; - DEBUG(perf,out).print("premarked_item_count=%d\n", premarked_item_count); - item_count++; - return 0; - } - - if (!can_melt(item)) { - DEBUG(perf,out).print("cannot melt\n"); - return 0; - } - - DEBUG(perf,out).print("increment item count\n"); - item_count++; - - //Only melt if told to, else count - if (should_melt) { - DEBUG(perf,out).print("should_melt hit\n"); - insert_into_vector(world->items.other[items_other_id::ANY_MELT_DESIGNATED], &df::item::id, item); - item->flags.bits.melt = 1; - tracked_item_map.emplace(item->id, true); - return 1; - } else { - return 0; - } - -} - -static int32_t mark_all_in_stockpile(color_ostream &out, PersistentDataItem & stockpile, int32_t &premarked_item_count, int32_t &item_count, map &tracked_item_map, bool should_melt) -{ - DEBUG(perf,out).print("%s running mark_all_in_stockpile\nshould_melt=%d\n", plugin_name, should_melt); - - static const BadFlagsMarkItem bad_flags; - - int32_t marked_count = 0; - - if(!stockpile.isValid()) { - return 0; - } - - int spid = get_config_val(stockpile, STOCKPILE_CONFIG_ID); - auto found = df::building::find(spid); - if (!isStockpile(found)) - return 0; - - df::building_stockpilest * pile_cast = virtual_cast(found); - - if (!pile_cast) - return 0; - - Buildings::StockpileIterator stored; - DEBUG(perf,out).print("starting item iter. loop\n"); - for (stored.begin(pile_cast); !stored.done(); ++stored) { - DEBUG(perf,out).print("calling mark_item\n"); - marked_count += mark_item(out, *stored, bad_flags, spid, premarked_item_count, item_count, tracked_item_map, should_melt); - DEBUG(perf,out).print("end mark_item call\npremarked_item_count=%d\n", premarked_item_count); - } - DEBUG(perf,out).print("end item iter. loop\n"); - DEBUG(perf,out).print("exit mark_all_in_stockpile\nmarked_count %d\npremarked_count %d\n", marked_count, premarked_item_count); - return marked_count; -} - -static int32_t scan_stockpiles(color_ostream &out, bool should_melt, map &item_count_piles, map &premarked_item_count_piles, - map &marked_item_count_piles, map &tracked_item_map) { - DEBUG(perf,out).print("running scan_stockpiles\n"); - int32_t newly_marked = 0; - - item_count_piles.clear(); - premarked_item_count_piles.clear(); - marked_item_count_piles.clear(); - - //Parse all the watched piles - for (auto &c : watched_stockpiles) { - int id = get_config_val(c.second, STOCKPILE_CONFIG_ID); - //Check monitor status - bool monitored = get_config_bool(c.second, STOCKPILE_CONFIG_MONITORED); - - if (!monitored) continue; - - DEBUG(perf,out).print("%s past monitor check\nmonitored=%d\n", plugin_name, monitored); - - int32_t item_count = 0; - - int32_t premarked_count = 0; - - int32_t marked = mark_all_in_stockpile(out, c.second, premarked_count, item_count, tracked_item_map, should_melt); - - DEBUG(perf,out).print("post mark_all_in_stockpile premarked_count=%d\n", premarked_count); - - item_count_piles.emplace(id, item_count); - - premarked_item_count_piles.emplace(id, premarked_count); - - marked_item_count_piles.emplace(id, marked); - - newly_marked += marked; - - } - DEBUG(perf,out).print("exit scan_stockpiles\n"); - return newly_marked; -} - -static int32_t scan_all_melt_designated(color_ostream &out, map &tracked_item_map) { - - DEBUG(perf,out).print("running scan_all_melt_designated\n"); - int32_t marked_item_count = 0; - //loop over all items marked as melt-designated - for (auto item : world->items.other[items_other_id::ANY_MELT_DESIGNATED]) { - //item has already been marked/counted as inside a stockpile. Don't double-count. - if (tracked_item_map.count(item->id)) { - continue; - } - marked_item_count++; - } - DEBUG(perf,out).print("exiting scan_all_melt_designated\n"); - return marked_item_count; -} - -static int32_t scan_count_all(color_ostream &out, bool should_melt, int32_t &marked_item_count_total, int32_t &marked_total_count_all_piles, int32_t &marked_item_count_global, - int32_t &total_items_all_piles, map &item_count_piles, map &premarked_item_count_piles, map &marked_item_count_piles) { - - //Wraps both scan_stockpiles and scan_all_melt_designated - //Returns count of items in piles freshly marked - - int32_t newly_marked_items_piles = 0; - - map tracked_item_map_piles; - - newly_marked_items_piles = scan_stockpiles(out, should_melt, item_count_piles, premarked_item_count_piles, marked_item_count_piles, tracked_item_map_piles); - marked_item_count_global = scan_all_melt_designated(out, tracked_item_map_piles); - - for (auto &i : watched_stockpiles) { - int id = get_config_val(i.second, STOCKPILE_CONFIG_ID); - total_items_all_piles+= item_count_piles[id]; - marked_total_count_all_piles += premarked_item_count_piles[id]; - } - - marked_item_count_total = marked_item_count_global + marked_total_count_all_piles; - - - return newly_marked_items_piles; - -} - -static int32_t do_cycle(color_ostream &out) { - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); - cycle_timestamp = world->frame_counter; - - validate_stockpile_configs(out); - - int32_t marked_item_count_total = 0; - int32_t marked_item_count_global = 0; - int32_t newly_marked_items_piles = 0; - int32_t total_items_all_piles = 0; - int32_t marked_total_count_all_piles = 0; - map item_count_piles, premarked_item_count_piles, marked_item_count_piles; - - newly_marked_items_piles = scan_count_all(out, true, marked_item_count_total, marked_total_count_all_piles, marked_item_count_global, - total_items_all_piles, item_count_piles, premarked_item_count_piles, marked_item_count_piles); - - DEBUG(perf,out).print("exit %s do_cycle\n", plugin_name); - return newly_marked_items_piles; -} - -static int getSelectedStockpile(color_ostream &out) { - df::building *bld = Gui::getSelectedBuilding(out, true); - if (!isStockpile(bld)) { - DEBUG(status,out).print("Selected building is not stockpile\n"); - return -1; - } - - return bld->id; -} - -static PersistentDataItem *getSelectedStockpileConfig(color_ostream &out) { - int32_t bldg_id = getSelectedStockpile(out); - if (bldg_id == -1) { - return NULL; - } - - validate_stockpile_configs(out); - PersistentDataItem *c = NULL; - if (watched_stockpiles.count(bldg_id)) { - c = &(watched_stockpiles[bldg_id]); - return c; - } - - DEBUG(status,out).print("No existing config\n"); - return NULL; -} - -static void push_stockpile_config(lua_State *L, int id, bool monitored) { - map stockpile_config; - stockpile_config.emplace("id", id); - stockpile_config.emplace("monitored", monitored); - Lua::Push(L, stockpile_config); -} - -static void push_stockpile_config(lua_State *L, PersistentDataItem &c) { - push_stockpile_config(L, get_config_val(c, STOCKPILE_CONFIG_ID), - 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)); -} - -static void automelt_printStatus(color_ostream &out) { - DEBUG(status,out).print("entering automelt_printStatus\n"); - validate_stockpile_configs(out); - out.print("automelt is %s\n\n", is_enabled ? "enabled" : "disabled"); - - int32_t marked_item_count_total = 0; - int32_t marked_item_count_global = 0; - int32_t total_items_all_piles = 0; - int32_t marked_total_count_all_piles = 0; - map item_count_piles, premarked_item_count_piles, marked_item_count_piles; - - scan_count_all(out, false, marked_item_count_total, marked_total_count_all_piles, marked_item_count_global, - total_items_all_piles, item_count_piles, premarked_item_count_piles, marked_item_count_piles); - - out.print("summary:\n"); - out.print(" total items in monitored piles: %d\n", total_items_all_piles); - out.print(" marked items in monitored piles: %d\n", marked_total_count_all_piles); - out.print("marked items global (excludes those in monitored piles): %d\n", marked_item_count_global); - out.print(" marked items global (monitored piles + others): %d\n", marked_item_count_total); - - int name_width = 11; - for (auto &stockpile : world->buildings.other.STOCKPILE) { - if (!isStockpile(stockpile)) continue; - if (stockpile->name.empty()) { - string stock_name = "Stockpile #" + int_to_string(stockpile->stockpile_number); - name_width = std::max(name_width, (int)(stock_name.size())); - } else { - name_width = std::max(name_width, (int)stockpile->name.size()); - } - } - name_width = -name_width; - - const char *fmt = "%*s %9s %5s %6s\n"; - out.print(fmt, name_width, "name", "monitored", "items", "marked"); - out.print(fmt, name_width, "----", "---------", "-----", "------"); - - for (auto &stockpile : world->buildings.other.STOCKPILE) { - if (!isStockpile(stockpile)) continue; - bool monitored = false; - int32_t item_count = 0; - int32_t marked_item_count = 0; - if (watched_stockpiles.count(stockpile->id)) { - auto &c = watched_stockpiles[stockpile->id]; - monitored = get_config_bool(c, STOCKPILE_CONFIG_MONITORED); - int id = get_config_val(c, STOCKPILE_CONFIG_ID); - item_count = item_count_piles[id]; - marked_item_count = premarked_item_count_piles[id]; - } - - if (stockpile->name.empty()) { - string stock_name = "Stockpile #" + int_to_string(stockpile->stockpile_number); - out.print(fmt, name_width, stock_name.c_str(), monitored ? "[x]": "[ ]", - int_to_string(item_count).c_str(), int_to_string(marked_item_count).c_str()); - } else { - out.print(fmt, name_width, stockpile->name.c_str(), monitored ? "[x]": "[ ]", - int_to_string(item_count).c_str(), int_to_string(marked_item_count).c_str()); - } - - - } - DEBUG(status,out).print("exiting automelt_printStatus\n"); - -} - -static void automelt_setStockpileConfig(color_ostream &out, int id, bool monitored) { - DEBUG(status,out).print("entering automelt_setStockpileConfig for id=%d and monitored=%d\n", id, monitored); - validate_stockpile_configs(out); - auto bldg = df::building::find(id); - bool isInvalidStockpile = !isStockpile(bldg); - bool hasNoData = !monitored; - if (isInvalidStockpile || hasNoData) { - DEBUG(cycle,out).print("calling remove_stockpile_config with id=%d monitored=%d\n", id, monitored); - remove_stockpile_config(out, id); - return; - } - - PersistentDataItem &c = ensure_stockpile_config(out, id); - set_config_val(c, STOCKPILE_CONFIG_ID, id); - set_config_bool(c, STOCKPILE_CONFIG_MONITORED, monitored); - - //If we're marking something as monitored, go ahead and designate contents. - if (monitored) { - automelt_designate(out); - } -} - -static int automelt_getStockpileConfig(lua_State *L) { - color_ostream *out = Lua::GetOutput(L); - if (!out) - out = &Core::getInstance().getConsole(); - DEBUG(status, *out).print("entering automelt_getStockpileConfig\n"); - validate_stockpile_configs(*out); - - int id; - if (lua_isnumber(L, -1)) { - bool found = false; - id = lua_tointeger(L, -1); - - for (auto &stockpile : world->buildings.other.STOCKPILE) { - if (!isStockpile(stockpile)) continue; - if (id == stockpile->stockpile_number){ - id = stockpile->id; - found = true; - break; - } - } - - if (!found) - return 0; - - } else { - const char * name = lua_tostring(L, -1); - if (!name) - return 0; - string nameStr = name; - bool found = false; - for (auto &stockpile : world->buildings.other.STOCKPILE) { - if (!isStockpile(stockpile)) continue; - if (nameStr == stockpile->name) { - id = stockpile->id; - found = true; - break; - } else { - string stock_name = "Stockpile #" + int_to_string(stockpile->stockpile_number); - if (stock_name == nameStr) { - id = stockpile->id; - found = true; - break; - } - } - - } - if (!found) - return 0; - } - - if (watched_stockpiles.count(id)) { - push_stockpile_config(L, watched_stockpiles[id]); - } else { - push_stockpile_config(L, id, false); - } - return 1; -} - -static int automelt_getSelectedStockpileConfig(lua_State *L){ - color_ostream *out = Lua::GetOutput(L); - if (!out) - out = &Core::getInstance().getConsole(); - DEBUG(status, *out).print("entering automelt_getSelectedStockpileConfig\n"); - validate_stockpile_configs(*out); - - int32_t stock_id = getSelectedStockpile(*out); - PersistentDataItem *c = getSelectedStockpileConfig(*out); - - //If we have a valid config entry, use that. Else assume all false. - if (c) { - push_stockpile_config(L, *c); - } else { - push_stockpile_config(L, stock_id, false); - } - - return 1; -} - -static int automelt_getItemCountsAndStockpileConfigs(lua_State *L) { - color_ostream *out = Lua::GetOutput(L); - if (!out) - out = &Core::getInstance().getConsole(); - DEBUG(status,*out).print("entering automelt_getItemCountsAndStockpileConfigs\n"); - validate_stockpile_configs(*out); - - int32_t marked_item_count_total = 0; - int32_t marked_item_count_global = 0; - int32_t total_items_all_piles = 0; - int32_t marked_total_count_all_piles = 0; - map item_count_piles, premarked_item_count_piles, marked_item_count_piles; - - scan_count_all(*out, false, marked_item_count_total, marked_total_count_all_piles, marked_item_count_global, - total_items_all_piles, item_count_piles, premarked_item_count_piles, marked_item_count_piles); - - map summary; - summary.emplace("total_items", total_items_all_piles); - summary.emplace("premarked_items", marked_total_count_all_piles); - summary.emplace("marked_item_count_global", marked_item_count_global); - summary.emplace("marked_item_count_total", marked_item_count_total); - - Lua::Push(L, summary); - Lua::Push(L, item_count_piles); - Lua::Push(L, marked_item_count_piles); - Lua::Push(L, premarked_item_count_piles); - - map> stockpile_config_map; - - for (auto pile : world->buildings.other.STOCKPILE) { - if (!isStockpile(pile)) - continue; - - int id = pile->id; - if (watched_stockpiles.count(id)) { - emplace_bulk_stockpile_config(L, watched_stockpiles[id], stockpile_config_map); - - } else { - 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 5; -} - -DFHACK_PLUGIN_LUA_FUNCTIONS{ - DFHACK_LUA_FUNCTION(automelt_printStatus), - DFHACK_LUA_FUNCTION(automelt_designate), - DFHACK_LUA_FUNCTION(automelt_setStockpileConfig), - DFHACK_LUA_END}; - -DFHACK_PLUGIN_LUA_COMMANDS{ - DFHACK_LUA_COMMAND(automelt_getStockpileConfig), - DFHACK_LUA_COMMAND(automelt_getItemCountsAndStockpileConfigs), - DFHACK_LUA_COMMAND(automelt_getSelectedStockpileConfig), - DFHACK_LUA_END}; diff --git a/plugins/autotrade.cpp b/plugins/autotrade.cpp deleted file mode 100644 index 333e34e95..000000000 --- a/plugins/autotrade.cpp +++ /dev/null @@ -1,467 +0,0 @@ -#include "uicommon.h" - -#include "modules/Gui.h" - -#include "df/world.h" -#include "df/world_raws.h" -#include "df/building_def.h" -#include "df/viewscreen_dwarfmodest.h" -#include "df/viewscreen_tradegoodsst.h" -#include "df/building_stockpilest.h" -#include "modules/Buildings.h" -#include "modules/Items.h" -#include "df/building_tradedepotst.h" -#include "df/general_ref_building_holderst.h" -#include "df/job.h" -#include "df/job_item_ref.h" -#include "modules/Job.h" -#include "df/plotinfost.h" -#include "df/mandate.h" -#include "modules/Maps.h" - -using df::building_stockpilest; - -DFHACK_PLUGIN("autotrade"); -REQUIRE_GLOBAL(gps); -REQUIRE_GLOBAL(world); -REQUIRE_GLOBAL(cursor); -REQUIRE_GLOBAL(plotinfo); - -static const string PERSISTENCE_KEY = "autotrade/stockpiles"; - -/* - * Depot Access - */ - -class TradeDepotInfo -{ -public: - TradeDepotInfo() : depot(0) - { - - } - - bool findDepot() - { - if (isValid()) - return true; - - reset(); - for(auto bld_it = world->buildings.all.begin(); bld_it != world->buildings.all.end(); bld_it++) - { - auto bld = *bld_it; - if (!isUsableDepot(bld)) - continue; - - depot = bld; - id = depot->id; - break; - } - - return depot; - } - - bool assignItem(df::item *item) - { - auto href = df::allocate(); - if (!href) - return false; - - auto job = new df::job(); - - df::coord tpos(depot->centerx, depot->centery, depot->z); - job->pos = tpos; - - job->job_type = job_type::BringItemToDepot; - - // job <-> item link - if (!Job::attachJobItem(job, item, df::job_item_ref::Hauled)) - { - delete job; - delete href; - return false; - } - - // job <-> building link - href->building_id = id; - depot->jobs.push_back(job); - job->general_refs.push_back(href); - - // add to job list - Job::linkIntoWorld(job); - - return true; - } - - void reset() - { - depot = 0; - } - -private: - int32_t id; - df::building *depot; - - bool isUsableDepot(df::building* bld) - { - if (bld->getType() != building_type::TradeDepot) - return false; - - if (bld->getBuildStage() < bld->getMaxBuildStage()) - return false; - - if (bld->jobs.size() == 1 && bld->jobs[0]->job_type == job_type::DestroyBuilding) - return false; - - return true; - } - - bool isValid() - { - if (!depot) - return false; - - auto found = df::building::find(id); - return found && found == depot && isUsableDepot(found); - } - -}; - -static TradeDepotInfo depot_info; - - -/* - * Item Manipulation - */ - -static bool is_valid_item(df::item *item) -{ - // Similar to Items::canTrade() with a few checks changed - for (size_t i = 0; i < item->general_refs.size(); i++) - { - df::general_ref *ref = item->general_refs[i]; - - switch (ref->getType()) - { - case general_ref_type::CONTAINED_IN_ITEM: - return false; - - case general_ref_type::UNIT_HOLDER: - return false; - - case general_ref_type::BUILDING_HOLDER: - return false; - - default: - break; - } - } - - for (size_t i = 0; i < item->specific_refs.size(); i++) - { - df::specific_ref *ref = item->specific_refs[i]; - - if (ref->type == specific_ref_type::JOB) - { - // Ignore any items assigned to a job - return false; - } - } - - if (!Items::checkMandates(item)) - return false; - - return true; -} - -static void mark_all_in_stockpiles(vector &stockpiles) -{ - if (!depot_info.findDepot()) - return; - - - // Precompute a bitmask with the bad flags - df::item_flags bad_flags; - bad_flags.whole = 0; - -#define F(x) bad_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(artifact); - F(spider_web); F(owned); F(in_job); -#undef F - - size_t marked_count = 0; - size_t error_count = 0; - for (auto it = stockpiles.begin(); it != stockpiles.end(); it++) - { - if (!it->isValid()) - continue; - - Buildings::StockpileIterator stored; - for (stored.begin(it->getStockpile()); !stored.done(); ++stored) - { - df::item *item = *stored; - if (item->flags.whole & bad_flags.whole) - continue; - - if (!is_valid_item(item)) - continue; - - // In case of container, check contained items for mandates - bool mandates_ok = true; - vector contained_items; - Items::getContainedItems(item, &contained_items); - for (df::item *cit : contained_items) - { - if (!Items::checkMandates(cit)) - { - mandates_ok = false; - break; - } - } - - if (!mandates_ok) - continue; - - if (depot_info.assignItem(item)) - { - ++marked_count; - } - else - { - if (++error_count < 5) - { - Gui::showZoomAnnouncement(df::announcement_type::CANCEL_JOB, item->pos, - "Cannot trade item from stockpile " + int_to_string(it->getId()), COLOR_RED, true); - } - } - } - } - - if (marked_count) - Gui::showAnnouncement("Marked " + int_to_string(marked_count) + " items for trade", COLOR_GREEN, false); - - if (error_count >= 5) - { - Gui::showAnnouncement(int_to_string(error_count) + " items were not marked", COLOR_RED, true); - } -} - - -/* - * Stockpile Monitoring - */ - -class StockpileMonitor -{ -public: - bool isMonitored(df::building_stockpilest *sp) - { - for (auto it = monitored_stockpiles.begin(); it != monitored_stockpiles.end(); it++) - { - if (it->matches(sp)) - return true; - } - - return false; - } - - void add(df::building_stockpilest *sp) - { - auto pile = PersistentStockpileInfo(sp, PERSISTENCE_KEY); - if (pile.isValid()) - { - monitored_stockpiles.push_back(pile); - monitored_stockpiles.back().save(); - } - } - - void remove(df::building_stockpilest *sp) - { - for (auto it = monitored_stockpiles.begin(); it != monitored_stockpiles.end(); it++) - { - if (it->matches(sp)) - { - it->remove(); - monitored_stockpiles.erase(it); - break; - } - } - } - - void doCycle() - { - if (!can_trade()) - return; - - for (auto it = monitored_stockpiles.begin(); it != monitored_stockpiles.end();) - { - if (!it->isValid()) - { - it = monitored_stockpiles.erase(it); - continue; - } - - ++it; - } - - mark_all_in_stockpiles(monitored_stockpiles); - } - - void reset() - { - monitored_stockpiles.clear(); - std::vector items; - DFHack::World::GetPersistentData(&items, PERSISTENCE_KEY); - - for (auto i = items.begin(); i != items.end(); i++) - { - auto pile = PersistentStockpileInfo(*i, PERSISTENCE_KEY); - if (pile.load()) - monitored_stockpiles.push_back(pile); - else - pile.remove(); - } - } - - -private: - vector monitored_stockpiles; -}; - -static StockpileMonitor monitor; - -#define DELTA_TICKS 600 - -DFhackCExport command_result plugin_onupdate ( color_ostream &out ) -{ - if(!Maps::IsValid()) - return CR_OK; - - if (DFHack::World::ReadPauseState()) - return CR_OK; - - if (world->frame_counter % DELTA_TICKS != 0) - return CR_OK; - - monitor.doCycle(); - - return CR_OK; -} - - -/* - * Interface - */ - -struct trade_hook : public df::viewscreen_dwarfmodest -{ - typedef df::viewscreen_dwarfmodest interpose_base; - - bool handleInput(set *input) - { - if (Gui::inRenameBuilding()) - return false; - - building_stockpilest *sp = get_selected_stockpile(); - if (!sp) - return false; - - if (input->count(interface_key::CUSTOM_SHIFT_T)) - { - if (monitor.isMonitored(sp)) - monitor.remove(sp); - else - monitor.add(sp); - } - - return false; - } - - DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) - { - if (!handleInput(input)) - INTERPOSE_NEXT(feed)(input); - } - - DEFINE_VMETHOD_INTERPOSE(void, render, ()) - { - INTERPOSE_NEXT(render)(); - - building_stockpilest *sp = get_selected_stockpile(); - if (!sp) - return; - - auto dims = Gui::getDwarfmodeViewDims(); - int left_margin = dims.menu_x1 + 1; - int x = left_margin; - int y = dims.y2 - 5; - - int links = 0; - links += sp->links.give_to_pile.size(); - links += sp->links.take_from_pile.size(); - links += sp->links.give_to_workshop.size(); - links += sp->links.take_from_workshop.size(); - bool state = monitor.isMonitored(sp); - - if (links + 12 >= y) { - y = dims.y2; - OutputString(COLOR_WHITE, x, y, "Auto: "); - x += 11; - OutputString(COLOR_LIGHTRED, x, y, "T"); - OutputString(state? COLOR_LIGHTGREEN: COLOR_GREY, x, y, "rade "); - } else { - OutputToggleString(x, y, "Auto trade", "T", state, true, left_margin, COLOR_WHITE, COLOR_LIGHTRED); - } - } -}; - -IMPLEMENT_VMETHOD_INTERPOSE(trade_hook, feed); -IMPLEMENT_VMETHOD_INTERPOSE(trade_hook, render); - - -DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) -{ - switch (event) - { - case DFHack::SC_MAP_LOADED: - depot_info.reset(); - monitor.reset(); - break; - case DFHack::SC_MAP_UNLOADED: - break; - default: - break; - } - return CR_OK; -} - -DFHACK_PLUGIN_IS_ENABLED(is_enabled); - -DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) -{ - if (enable != is_enabled) - { - depot_info.reset(); - monitor.reset(); - - if (!INTERPOSE_HOOK(trade_hook, feed).apply(enable) || - !INTERPOSE_HOOK(trade_hook, render).apply(enable)) - return CR_FAILURE; - - is_enabled = enable; - } - - return CR_OK; -} - -DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) -{ - return CR_OK; -} - -DFhackCExport command_result plugin_shutdown ( color_ostream &out ) -{ - return CR_OK; -} diff --git a/plugins/logistics.cpp b/plugins/logistics.cpp new file mode 100644 index 000000000..77b462bbb --- /dev/null +++ b/plugins/logistics.cpp @@ -0,0 +1,676 @@ +#include "Debug.h" +#include "LuaTools.h" +#include "PluginManager.h" + +#include "modules/Buildings.h" +#include "modules/Job.h" +#include "modules/Persistence.h" +#include "modules/World.h" + +#include "df/building.h" +#include "df/building_stockpilest.h" +#include "df/building_tradedepotst.h" +#include "df/caravan_state.h" +#include "df/general_ref_building_holderst.h" +#include "df/plotinfost.h" +#include "df/world.h" + +using std::string; +using std::unordered_map; +using std::vector; + +using namespace DFHack; + +DFHACK_PLUGIN("logistics"); + +DFHACK_PLUGIN_IS_ENABLED(is_enabled); + +REQUIRE_GLOBAL(plotinfo); +REQUIRE_GLOBAL(world); + +namespace DFHack { +DBG_DECLARE(logistics, status, DebugCategory::LINFO); +DBG_DECLARE(logistics, cycle, DebugCategory::LINFO); +} + +static const string CONFIG_KEY_PREFIX = string(plugin_name) + "/"; +static unordered_map watched_stockpiles; + +enum StockpileConfigValues { + STOCKPILE_CONFIG_STOCKPILE_NUMBER = 0, + STOCKPILE_CONFIG_MELT = 1, + STOCKPILE_CONFIG_TRADE = 2, + STOCKPILE_CONFIG_DUMP = 3, +}; + +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); +} + +static PersistentDataItem& ensure_stockpile_config(color_ostream& out, int stockpile_number) { + DEBUG(cycle, out).print("ensuring stockpile config stockpile_number=%d\n", stockpile_number); + if (watched_stockpiles.count(stockpile_number)) { + DEBUG(cycle, out).print("stockpile exists in watched_stockpiles\n"); + return watched_stockpiles[stockpile_number]; + } + + string keyname = CONFIG_KEY_PREFIX + int_to_string(stockpile_number); + DEBUG(status, out).print("creating new persistent key for stockpile %d\n", stockpile_number); + watched_stockpiles.emplace(stockpile_number, World::GetPersistentData(keyname, NULL)); + PersistentDataItem& c = watched_stockpiles[stockpile_number]; + set_config_val(c, STOCKPILE_CONFIG_STOCKPILE_NUMBER, stockpile_number); + set_config_bool(c, STOCKPILE_CONFIG_MELT, false); + set_config_bool(c, STOCKPILE_CONFIG_TRADE, false); + set_config_bool(c, STOCKPILE_CONFIG_DUMP, false); + return c; +} + +static void remove_stockpile_config(color_ostream& out, int stockpile_number) { + if (!watched_stockpiles.count(stockpile_number)) + return; + DEBUG(status, out).print("removing persistent key for stockpile %d\n", stockpile_number); + World::DeletePersistentData(watched_stockpiles[stockpile_number]); + watched_stockpiles.erase(stockpile_number); +} + +static const int32_t CYCLE_TICKS = 600; +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, int32_t &melt_count, int32_t &trade_count, int32_t &dump_count); + +DFhackCExport command_result plugin_init(color_ostream &out, vector &commands) { + DEBUG(status, out).print("initializing %s\n", plugin_name); + + commands.push_back(PluginCommand( + plugin_name, + "Automatically mark and route items in monitored stockpiles.", + do_command)); + + return CR_OK; +} + +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { + is_enabled = enable; + DEBUG(status, out).print("now %s\n", is_enabled ? "enabled" : "disabled"); + return CR_OK; +} + +static df::building_stockpilest* find_stockpile(int32_t stockpile_number) { + return binsearch_in_vector(world->buildings.other.STOCKPILE, + &df::building_stockpilest::stockpile_number, stockpile_number); +} + +static void validate_stockpile_configs(color_ostream& out, + unordered_map &cache) { + for (auto& entry : watched_stockpiles) { + int stockpile_number = entry.first; + PersistentDataItem &c = entry.second; + auto bld = find_stockpile(stockpile_number); + if (!bld || ( + !get_config_bool(c, STOCKPILE_CONFIG_MELT) && + !get_config_bool(c, STOCKPILE_CONFIG_TRADE) && + !get_config_bool(c, STOCKPILE_CONFIG_DUMP))) { + remove_stockpile_config(out, stockpile_number); + continue; + } + cache.emplace(bld, c); + } +} + +// remove this function once saves from 50.08 are no longer compatible +static void migrate_old_keys(color_ostream &out) { + vector old_data; + World::GetPersistentData(&old_data, "automelt/stockpile/", true); + const size_t num_old_keys = old_data.size(); + for (size_t idx = 0; idx < num_old_keys; ++idx) { + auto& old_c = old_data[idx]; + int32_t bld_id = get_config_val(old_c, 0); + bool melt_was_on = get_config_bool(old_c, 1); + World::DeletePersistentData(old_c); + auto bld = df::building::find(bld_id); + if (!bld || bld->getType() != df::building_type::Stockpile || + watched_stockpiles.count(static_cast(bld)->stockpile_number)) + continue; + auto &c = ensure_stockpile_config(out, static_cast(bld)->stockpile_number); + set_config_bool(c, STOCKPILE_CONFIG_MELT, melt_was_on); + } +} + +DFhackCExport command_result plugin_load_data(color_ostream &out) { + cycle_timestamp = 0; + + vector loaded_persist_data; + World::GetPersistentData(&loaded_persist_data, CONFIG_KEY_PREFIX, true); + watched_stockpiles.clear(); + const size_t num_watched_stockpiles = loaded_persist_data.size(); + for (size_t idx = 0; idx < num_watched_stockpiles; ++idx) { + auto& c = loaded_persist_data[idx]; + watched_stockpiles.emplace(get_config_val(c, STOCKPILE_CONFIG_STOCKPILE_NUMBER), c); + } + migrate_old_keys(out); + + return CR_OK; +} + +DFhackCExport command_result plugin_onupdate(color_ostream &out) { + if (!is_enabled || !Core::getInstance().isWorldLoaded()) + return CR_OK; + if (world->frame_counter - cycle_timestamp >= CYCLE_TICKS) { + int32_t melt_count = 0, trade_count = 0, dump_count = 0; + do_cycle(out, melt_count, trade_count, dump_count); + if (0 < melt_count) + out.print("logistics: marked %d item(s) for melting\n", melt_count); + if (0 < trade_count) + out.print("logistics: marked %d item(s) for trading\n", trade_count); + if (0 < dump_count) + out.print("logistics: marked %d item(s) for dumping\n", dump_count); + } + return CR_OK; +} + +static bool call_logistics_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(status).print("calling logistics lua function: '%s'\n", fn_name); + + CoreSuspender guard; + + auto L = Lua::Core::State; + Lua::StackUnwinder top(L); + + if (!out) + out = &Core::getInstance().getConsole(); + + return Lua::CallLuaModuleFunction(*out, L, "plugins.logistics", fn_name, + nargs, nres, + std::forward(args_lambda), + std::forward(res_lambda)); +} + +static command_result do_command(color_ostream &out, vector ¶meters) { + CoreSuspender suspend; + + bool show_help = false; + if (!call_logistics_lua(&out, "parse_commandline", 1, 1, + [&](lua_State *L) { + Lua::PushVector(L, parameters); + }, + [&](lua_State *L) { + show_help = !lua_toboolean(L, -1); + })) { + return CR_FAILURE; + } + + return show_help ? CR_WRONG_USAGE : CR_OK; +} + +///////////////////////////////////////////////////// +// cycle +// + +typedef unordered_map StatMap; + +struct ProcessorStats { + size_t newly_designated = 0; + StatMap designated_counts, can_designate_counts; +}; + +class StockProcessor { +public: + const string name; + const int32_t stockpile_number; + const bool enabled; + ProcessorStats &stats; +protected: + StockProcessor(const string &name, int32_t stockpile_number, bool enabled, ProcessorStats &stats) + : name(name), stockpile_number(stockpile_number), enabled(enabled), stats(stats) { } +public: + virtual bool is_designated(color_ostream &out, df::item *item) = 0; + virtual bool can_designate(color_ostream &out, df::item *item) = 0; + virtual bool designate(color_ostream &out, df::item *item) = 0; +}; + +class MeltStockProcessor : public StockProcessor { +public: + MeltStockProcessor(int32_t stockpile_number, bool enabled, ProcessorStats &stats) + : StockProcessor("melt", stockpile_number, enabled, stats) { } + + virtual bool is_designated(color_ostream &out, df::item *item) override { + return item->flags.bits.melt; + } + + virtual bool can_designate(color_ostream &out, df::item *item) override { + MaterialInfo mat(item); + if (mat.getCraftClass() != df::craft_material_class::Metal) + return false; + + if (item->getType() == df::item_type::BAR) + return false; + + for (auto &g : item->general_refs) { + switch (g->getType()) { + case df::general_ref_type::CONTAINS_ITEM: + case df::general_ref_type::UNIT_HOLDER: + case df::general_ref_type::CONTAINS_UNIT: + return false; + case df::general_ref_type::CONTAINED_IN_ITEM: + { + df::item *c = g->getItem(); + for (auto &gg : c->general_refs) { + if (gg->getType() == df::general_ref_type::UNIT_HOLDER) + return false; + } + break; + } + default: + break; + } + } + + if (item->getQuality() >= df::item_quality::Masterful) + return false; + + return true; + } + + virtual bool designate(color_ostream &out, df::item *item) override { + insert_into_vector(world->items.other.ANY_MELT_DESIGNATED, &df::item::id, item); + item->flags.bits.melt = 1; + return true; + } +}; + +class TradeStockProcessor: public StockProcessor { +public: + TradeStockProcessor(int32_t stockpile_number, bool enabled, ProcessorStats& stats) + : StockProcessor("trade", stockpile_number, enabled && get_active_trade_depot(), stats), + depot(get_active_trade_depot()) { } + + virtual bool is_designated(color_ostream& out, df::item* item) override { + auto ref = Items::getSpecificRef(item, df::specific_ref_type::JOB); + return ref && ref->data.job && + ref->data.job->job_type == df::job_type::BringItemToDepot; + } + + virtual bool can_designate(color_ostream& out, df::item* item) override { + return Items::canTradeWithContents(item); + } + + virtual bool designate(color_ostream& out, df::item* item) override { + if (!depot) + return false; + + auto href = df::allocate(); + if (!href) + return false; + + auto job = new df::job(); + job->job_type = df::job_type::BringItemToDepot; + job->pos = df::coord(depot->centerx, depot->centery, depot->z); + + // job <-> item link + if (!Job::attachJobItem(job, item, df::job_item_ref::Hauled)) { + delete job; + delete href; + return false; + } + + // job <-> building link + href->building_id = depot->id; + depot->jobs.push_back(job); + job->general_refs.push_back(href); + + // add to job list + Job::linkIntoWorld(job); + + return true; + } + +private: + df::building_tradedepotst * const depot; + + static df::building_tradedepotst * get_active_trade_depot() { + // at least one caravan must be approaching or ready to trade + if (!plotinfo->caravans.size()) + return NULL; + bool found = false; + for (auto caravan : plotinfo->caravans) { + auto trade_state = caravan->trade_state; + auto time_remaining = caravan->time_remaining; + if ((trade_state == df::caravan_state::T_trade_state::Approaching || + trade_state == df::caravan_state::T_trade_state::AtDepot) && time_remaining != 0) { + found = true; + break; + } + } + if (!found) + return NULL; + + // at least one trade depot must be ready to receive goods + for (auto bld : world->buildings.other.TRADE_DEPOT) { + if (bld->getBuildStage() < bld->getMaxBuildStage()) + continue; + + if (bld->jobs.size() == 1 && + bld->jobs[0]->job_type == df::job_type::DestroyBuilding) + continue; + + return bld; + } + return NULL; + } +}; + +class DumpStockProcessor: public StockProcessor { +public: + DumpStockProcessor(int32_t stockpile_number, bool enabled, ProcessorStats& stats) + : StockProcessor("dump", stockpile_number, enabled, stats) { } + + virtual bool is_designated(color_ostream& out, df::item* item) override { + return item->flags.bits.dump; + } + + virtual bool can_designate(color_ostream& out, df::item* item) override { + return true; + } + + virtual bool designate(color_ostream& out, df::item* item) override { + item->flags.bits.dump = true; + return true; + } +}; + +static const struct BadFlags { + const uint32_t whole; + + BadFlags() : whole(get_bad_flags()) { } + +private: + uint32_t get_bad_flags() { + df::item_flags flags; + #define F(x) flags.bits.x = true; + F(forbid); F(garbage_collect); F(hostile); F(on_fire); + F(rotten); F(trader); F(in_building); F(construction); + F(artifact); F(spider_web); F(owned); F(in_job); + #undef F + return flags.whole; + } +} bad_flags; + +static void scan_item(color_ostream &out, df::item *item, StockProcessor &processor) { + DEBUG(cycle,out).print("scan_item [%s] item_id=%d\n", processor.name.c_str(), item->id); + + if (DBG_NAME(cycle).isEnabled(DebugCategory::LTRACE)) { + string name = ""; + item->getItemDescription(&name, 0); + TRACE(cycle,out).print("item: %s\n", name.c_str()); + } + + if (item->flags.whole & bad_flags.whole) { + TRACE(cycle,out).print("rejected flag check\n"); + return; + } + + if (processor.is_designated(out, item)) { + TRACE(cycle,out).print("already designated\n"); + ++processor.stats.designated_counts[processor.stockpile_number]; + return; + } + + if (!processor.can_designate(out, item)) { + TRACE(cycle,out).print("cannot designate\n"); + return; + } + + if (!processor.enabled) { + ++processor.stats.can_designate_counts[processor.stockpile_number]; + return; + } + + processor.designate(out, item); + ++processor.stats.newly_designated; + ++processor.stats.designated_counts[processor.stockpile_number]; +} + +static void scan_stockpile(color_ostream &out, df::building_stockpilest *bld, + MeltStockProcessor &melt_stock_processor, + TradeStockProcessor &trade_stock_processor, + DumpStockProcessor &dump_stock_processor) { + auto id = bld->id; + Buildings::StockpileIterator items; + for (items.begin(bld); !items.done(); ++items) { + df::item *item = *items; + scan_item(out, item, trade_stock_processor); + if (0 == (item->flags.whole & bad_flags.whole) && + item->isAssignedToThisStockpile(id)) { + TRACE(cycle,out).print("assignedToStockpile\n"); + vector contents; + Items::getContainedItems(item, &contents); + for (df::item *contained_item : contents) { + scan_item(out, contained_item, melt_stock_processor); + scan_item(out, contained_item, dump_stock_processor); + } + continue; + } + scan_item(out, item, melt_stock_processor); + scan_item(out, item, dump_stock_processor); + } +} + +static void do_cycle(color_ostream &out, int32_t &melt_count, int32_t &trade_count, int32_t &dump_count) { + DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + cycle_timestamp = world->frame_counter; + + ProcessorStats melt_stats, trade_stats, dump_stats; + unordered_map cache; + validate_stockpile_configs(out, cache); + + for (auto &entry : cache) { + df::building_stockpilest *bld = entry.first; + PersistentDataItem &c = entry.second; + int32_t stockpile_number = bld->stockpile_number; + + bool melt = get_config_bool(c, STOCKPILE_CONFIG_MELT); + bool trade = get_config_bool(c, STOCKPILE_CONFIG_TRADE); + bool dump = get_config_bool(c, STOCKPILE_CONFIG_DUMP); + + MeltStockProcessor melt_stock_processor(stockpile_number, melt, melt_stats); + TradeStockProcessor trade_stock_processor(stockpile_number, trade, trade_stats); + DumpStockProcessor dump_stock_processor(stockpile_number, dump, dump_stats); + + scan_stockpile(out, bld, melt_stock_processor, + trade_stock_processor, dump_stock_processor); + } + + melt_count = melt_stats.newly_designated; + trade_count = trade_stats.newly_designated; + dump_count = dump_stats.newly_designated; + TRACE(cycle,out).print("exit %s do_cycle\n", plugin_name); +} + +///////////////////////////////////////////////////// +// Lua API +// + +static int logistics_getStockpileData(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(status,*out).print("entering logistics_getStockpileData\n"); + + unordered_map cache; + validate_stockpile_configs(*out, cache); + + ProcessorStats melt_stats, trade_stats, dump_stats; + + for (auto bld : df::global::world->buildings.other.STOCKPILE) { + int32_t stockpile_number = bld->stockpile_number; + MeltStockProcessor melt_stock_processor(stockpile_number, false, melt_stats); + TradeStockProcessor trade_stock_processor(stockpile_number, false, trade_stats); + DumpStockProcessor dump_stock_processor(stockpile_number, false, dump_stats); + + scan_stockpile(*out, bld, melt_stock_processor, + trade_stock_processor, dump_stock_processor); + } + + unordered_map stats; + stats.emplace("melt_designated", melt_stats.designated_counts); + stats.emplace("melt_can_designate", melt_stats.can_designate_counts); + stats.emplace("trade_designated", trade_stats.designated_counts); + stats.emplace("trade_can_designate", trade_stats.can_designate_counts); + stats.emplace("dump_designated", dump_stats.designated_counts); + stats.emplace("dump_can_designate", dump_stats.can_designate_counts); + Lua::Push(L, stats); + + unordered_map> configs; + for (auto &entry : cache) { + df::building_stockpilest *bld = entry.first; + PersistentDataItem &c = entry.second; + + bool melt = get_config_bool(c, STOCKPILE_CONFIG_MELT); + bool trade = get_config_bool(c, STOCKPILE_CONFIG_TRADE); + bool dump = get_config_bool(c, STOCKPILE_CONFIG_DUMP); + + unordered_map config; + config.emplace("melt", melt ? "true" : "false"); + config.emplace("trade", trade ? "true" : "false"); + config.emplace("dump", dump ? "true" : "false"); + + configs.emplace(bld->stockpile_number, config); + } + Lua::Push(L, configs); + + TRACE(cycle, *out).print("exit logistics_getStockpileData\n"); + + return 2; +} + +static void logistics_cycle(color_ostream &out) { + DEBUG(status, out).print("entering logistics_cycle\n"); + int32_t melt_count = 0, trade_count = 0, dump_count = 0; + do_cycle(out, melt_count, trade_count, dump_count); + out.print("logistics: marked %d item(s) for melting\n", melt_count); + out.print("logistics: marked %d item(s) for trading\n", trade_count); + out.print("logistics: marked %d item(s) for dumping\n", dump_count); +} + +static void find_stockpiles(lua_State *L, int idx, + vector &sps) { + if (lua_isnumber(L, idx)) { + sps.emplace_back(find_stockpile(lua_tointeger(L, -1))); + return; + } + + const char * pname = lua_tostring(L, -1); + if (!pname || !*pname) + return; + string name(pname); + for (auto sp : world->buildings.other.STOCKPILE) { + if (name == sp->name) + sps.emplace_back(sp); + } +} + +static unordered_map get_stockpile_config(int32_t stockpile_number) { + unordered_map stockpile_config; + stockpile_config.emplace("stockpile_number", stockpile_number); + if (watched_stockpiles.count(stockpile_number)) { + PersistentDataItem &c = watched_stockpiles[stockpile_number]; + stockpile_config.emplace("melt", get_config_bool(c, STOCKPILE_CONFIG_MELT)); + stockpile_config.emplace("trade", get_config_bool(c, STOCKPILE_CONFIG_TRADE)); + stockpile_config.emplace("dump", get_config_bool(c, STOCKPILE_CONFIG_DUMP)); + } else { + stockpile_config.emplace("melt", false); + stockpile_config.emplace("trade", false); + stockpile_config.emplace("dump", false); + } + return stockpile_config; +} + +static int logistics_getStockpileConfigs(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(status, *out).print("entering logistics_getStockpileConfig\n"); + + unordered_map cache; + validate_stockpile_configs(*out, cache); + + vector sps; + find_stockpiles(L, -1, sps); + if (sps.empty()) + return 0; + + vector> configs; + for (auto sp : sps) + configs.emplace_back(get_stockpile_config(sp->stockpile_number)); + Lua::PushVector(L, configs); + return 1; +} + +static void logistics_setStockpileConfig(color_ostream &out, int stockpile_number, bool melt, bool trade, bool dump) { + DEBUG(status, out).print("entering logistics_setStockpileConfig stockpile_number=%d, melt=%d, trade=%d, dump=%d\n", + stockpile_number, melt, trade, dump); + + if (!find_stockpile(stockpile_number)) { + out.printerr("invalid stockpile number: %d\n", stockpile_number); + return; + } + + auto &c = ensure_stockpile_config(out, stockpile_number); + set_config_bool(c, STOCKPILE_CONFIG_MELT, melt); + set_config_bool(c, STOCKPILE_CONFIG_TRADE, trade); + set_config_bool(c, STOCKPILE_CONFIG_DUMP, dump); +} + +static int logistics_clearStockpileConfig(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(status, *out).print("entering logistics_clearStockpileConfig\n"); + + vector sps; + find_stockpiles(L, -1, sps); + if (sps.empty()) + return 0; + + for (auto sp : sps) + remove_stockpile_config(*out, sp->stockpile_number); + return 0; +} + +static void logistics_clearAllStockpileConfigs(color_ostream &out) { + DEBUG(status, out).print("entering logistics_clearAllStockpileConfigs\n"); + for (auto &entry : watched_stockpiles) + World::DeletePersistentData(entry.second); + watched_stockpiles.clear(); +} + +DFHACK_PLUGIN_LUA_FUNCTIONS{ + DFHACK_LUA_FUNCTION(logistics_cycle), + DFHACK_LUA_FUNCTION(logistics_setStockpileConfig), + DFHACK_LUA_FUNCTION(logistics_clearAllStockpileConfigs), + DFHACK_LUA_END}; + +DFHACK_PLUGIN_LUA_COMMANDS{ + DFHACK_LUA_COMMAND(logistics_getStockpileData), + DFHACK_LUA_COMMAND(logistics_getStockpileConfigs), + DFHACK_LUA_COMMAND(logistics_clearStockpileConfig), + DFHACK_LUA_END}; diff --git a/plugins/lua/automelt.lua b/plugins/lua/automelt.lua deleted file mode 100644 index cbd1bd538..000000000 --- a/plugins/lua/automelt.lua +++ /dev/null @@ -1,87 +0,0 @@ -local _ENV = mkmodule('plugins.automelt') - -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 - - -local function do_set_stockpile_config(var_name, val, stockpiles) - for _,bspec in ipairs(argparse.stringList(stockpiles)) do - local config = automelt_getStockpileConfig(bspec) - if not config then - dfhack.printerr('invalid stockpile: '..tostring(bspec)) - else - config[var_name] = val - automelt_setStockpileConfig(config.id, config.monitor, config.melt) - end - end -end - - -function parse_commandline(...) - local args, opts = {...}, {} - local positionals = process_args(opts, args) - - if opts.help then - return false - end - - local command = positionals[1] - if not command or command == 'status' then - automelt_printStatus() - elseif command == 'designate' then - automelt_designate() - elseif command == 'monitor' then - do_set_stockpile_config('monitor', true, args[2]) - elseif command == 'nomonitor' or command == 'unmonitor' then - do_set_stockpile_config('monitor', false, args[2]) - else - return false - end - - return true -end - --- used by gui/automelt -function setStockpileConfig(config) - automelt_setStockpileConfig(config.id, config.monitored) -end - -function getItemCountsAndStockpileConfigs() - local fmt = 'Stockpile #%-5s' - local data = {automelt_getItemCountsAndStockpileConfigs()} - local ret = {} - ret.summary = table.remove(data, 1) - ret.item_counts = table.remove(data, 1) - ret.marked_item_counts = table.remove(data, 1) - ret.premarked_item_counts = table.remove(data, 1) - 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 - else - c.name = df.building.find(c.id).name - if not c.name or c.name == '' then - c.name = (fmt):format(tostring(df.building.find(c.id).stockpile_number)) - end - c.monitored = c.monitored ~= 0 - end - table.insert(ret.stockpile_configs, c) - - end - return ret -end - -return _ENV diff --git a/plugins/lua/logistics.lua b/plugins/lua/logistics.lua new file mode 100644 index 000000000..806d1aa97 --- /dev/null +++ b/plugins/lua/logistics.lua @@ -0,0 +1,146 @@ +local _ENV = mkmodule('plugins.logistics') + +local argparse = require('argparse') +local utils = require('utils') + +local function make_stat(name, stockpile_number, stats, configs) + return { + enabled=configs[stockpile_number] and configs[stockpile_number][name] == 'true', + designated=stats[name..'_designated'][stockpile_number] or 0, + can_designate=stats[name..'_can_designate'][stockpile_number] or 0, + } +end + +function getStockpileData() + local stats, configs = logistics_getStockpileData() + local data = {} + for _,bld in ipairs(df.global.world.buildings.other.STOCKPILE) do + local stockpile_number, name = bld.stockpile_number, bld.name + local sort_key = name + if #name == 0 then + name = ('Stockpile #%d'):format(bld.stockpile_number) + sort_key = ('Stockpile #%09d'):format(bld.stockpile_number) + end + table.insert(data, { + stockpile_number=stockpile_number, + name=name, + sort_key=sort_key, + melt=make_stat('melt', stockpile_number, stats, configs), + trade=make_stat('trade', stockpile_number, stats, configs), + dump=make_stat('dump', stockpile_number, stats, configs), + }) + end + table.sort(data, function(a, b) return a.sort_key < b.sort_key end) + return data +end + +local function print_status() + print(('logistics is %sactively monitoring stockpiles and marking items') + :format(isEnabled() and '' or 'not ')) + + if df.global.gamemode ~= df.game_mode.DWARF or not dfhack.isMapLoaded() then + return + end + + local data = getStockpileData() + + print() + if not data[1] then + print 'No stockpiles defined -- go make some!' + return + end + + local name_len = 12 + for _,sp in ipairs(data) do + name_len = math.min(40, math.max(name_len, #sp.name)) + end + + print('Designated/designatable items in stockpiles:') + print() + local fmt = '%6s %-' .. name_len .. 's %11s %11s %11s'; + print(fmt:format('number', 'name', 'melt', 'trade', 'dump')) + local function uline(len) return ('-'):rep(len) end + print(fmt:format(uline(6), uline(name_len), uline(11), uline(11), uline(11))) + local function get_enab(stats) return ('[%s]'):format(stats.enabled and 'x' or ' ') end + local function get_dstat(stats) return ('%d/%d '):format(stats.designated, stats.designated + stats.can_designate) end + for _,sp in ipairs(data) do + print(fmt:format(sp.stockpile_number, sp.name, get_enab(sp.melt), get_enab(sp.trade), get_enab(sp.dump))) + print(fmt:format('', '', get_dstat(sp.melt), get_dstat(sp.trade), get_dstat(sp.dump))) + end +end + +local function for_stockpiles(opts, fn) + if not opts.sp then + local selected_sp = dfhack.gui.getSelectedStockpile() + if not selected_sp then qerror('please specify or select a stockpile') end + fn(selected_sp.stockpile_number) + return + end + for _,sp in ipairs(argparse.stringList(opts.sp)) do + fn(sp) + end +end + +local function do_add_stockpile_config(features, opts) + for_stockpiles(opts, function(sp) + local configs = logistics_getStockpileConfigs(tonumber(sp) or sp) + if not configs then + dfhack.printerr('invalid stockpile: '..sp) + else + for _,config in ipairs(configs) do + logistics_setStockpileConfig(config.stockpile_number, + features.melt or config.melt, + features.trade or config.trade, + features.dump or config.dump) + end + end + end) +end + +local function do_clear_stockpile_config(all, opts) + if all then + logistics_clearAllStockpileConfigs() + return + end + for_stockpiles(opts, function(sp) + logistics_clearStockpileConfig(tonumber(sp) or sp) + end) +end + +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}, + {'s', 'stockpile', hasArg=true, handler=function(arg) opts.sp = arg end}, + }) +end + +function parse_commandline(args) + local opts = {} + local positionals = process_args(opts, args) + + if opts.help or not positionals then + return false + end + + local command = table.remove(positionals, 1) + if not command or command == 'status' then + print_status() + elseif command == 'now' then + logistics_cycle() + elseif command == 'add' then + do_add_stockpile_config(utils.invert(positionals), opts) + elseif command == 'clear' then + do_clear_stockpile_config(utils.invert(positionals).all, opts) + else + return false + end + + return true +end + +return _ENV diff --git a/plugins/lua/stockpiles.lua b/plugins/lua/stockpiles.lua index 3e127de4d..901a65000 100644 --- a/plugins/lua/stockpiles.lua +++ b/plugins/lua/stockpiles.lua @@ -79,6 +79,7 @@ local included_elements = { general=2, categories=4, types=8, + features=16, } function export_stockpile(name, opts) @@ -87,9 +88,7 @@ function export_stockpile(name, opts) local includedElements = 0 for _,inc in ipairs(opts.includes) do - if included_elements[inc] then - includedElements = includedElements | included_elements[inc] - end + includedElements = includedElements | included_elements[inc] end if includedElements == 0 then @@ -124,12 +123,10 @@ function import_route(name, route_id, stop_id, mode, filters) stockpiles_route_import(name, route_id, stop_id, mode, table.concat(filters or {}, ',')) end -local valid_includes = {general=true, categories=true, types=true} - local function parse_include(arg) local includes = argparse.stringList(arg, 'include') for _,v in ipairs(includes) do - if not valid_includes[v] then + if not included_elements[v] then qerror(('invalid included element: "%s"'):format(v)) end end diff --git a/plugins/stockpiles/OrganicMatLookup.cpp b/plugins/stockpiles/OrganicMatLookup.cpp index a9e2210c9..91294809b 100644 --- a/plugins/stockpiles/OrganicMatLookup.cpp +++ b/plugins/stockpiles/OrganicMatLookup.cpp @@ -11,8 +11,9 @@ using namespace DFHack; using namespace df::enums; using df::global::world; -namespace DFHack { - DBG_EXTERN(stockpiles, log); +namespace DFHack +{ +DBG_EXTERN(stockpiles, log); } /** diff --git a/plugins/stockpiles/StockpileSerializer.cpp b/plugins/stockpiles/StockpileSerializer.cpp index b650ec618..b030cd873 100644 --- a/plugins/stockpiles/StockpileSerializer.cpp +++ b/plugins/stockpiles/StockpileSerializer.cpp @@ -42,8 +42,9 @@ using df::global::world; using std::placeholders::_1; -namespace DFHack { - DBG_EXTERN(stockpiles, log); +namespace DFHack +{ +DBG_EXTERN(stockpiles, log); } static struct OtherMatsFurniture { @@ -905,6 +906,28 @@ void StockpileSerializer::read_general(DeserializeMode mode) { mPile->use_links_only); } +void StockpileSerializer::write_features() { + DEBUG(log).print("writing feature settings\n"); + mBuffer.set_melt(mPile->use_links_only); + mBuffer.set_trade(mPile->settings.allow_inorganic); + mBuffer.set_dump(mPile->settings.allow_organic); +} + +void StockpileSerializer::read_features(DeserializeMode mode) { + read_elem("use_links_only", mode, + std::bind(&StockpileSettings::has_use_links_only, mBuffer), + std::bind(&StockpileSettings::use_links_only, mBuffer), + mPile->use_links_only); + read_elem("allow_inorganic", mode, + std::bind(&StockpileSettings::has_allow_inorganic, mBuffer), + std::bind(&StockpileSettings::allow_inorganic, mBuffer), + mPile->settings.allow_inorganic); + read_elem("allow_organic", mode, + std::bind(&StockpileSettings::has_allow_organic, mBuffer), + std::bind(&StockpileSettings::allow_organic, mBuffer), + mPile->settings.allow_organic); +} + static bool ammo_mat_is_allowed(const MaterialInfo& mi) { return mi.isValid() && mi.material && mi.material->flags.is_set(material_flags::IS_METAL); } diff --git a/plugins/stockpiles/StockpileSerializer.h b/plugins/stockpiles/StockpileSerializer.h index 4798fba5e..70e8909da 100644 --- a/plugins/stockpiles/StockpileSerializer.h +++ b/plugins/stockpiles/StockpileSerializer.h @@ -168,4 +168,6 @@ private: void write_containers(); void read_containers(DeserializeMode mode); + void write_features(); + void read_features(DeserializeMode mode); }; diff --git a/plugins/stockpiles/proto/stockpiles.proto b/plugins/stockpiles/proto/stockpiles.proto index 681e7d927..0ad89e143 100644 --- a/plugins/stockpiles/proto/stockpiles.proto +++ b/plugins/stockpiles/proto/stockpiles.proto @@ -183,6 +183,11 @@ message StockpileSettings { optional WeaponsSet weapons = 16; optional WoodSet wood = 15; + // DFHack features + optional bool melt = 27; + optional bool trade = 28; + optional bool dump = 29; + // deprecated optional bool corpses = 24; // not marked as deprecated since we still read it optional OreSet ore = 7 [deprecated=true]; diff --git a/plugins/stockpiles/stockpiles.cpp b/plugins/stockpiles/stockpiles.cpp index de57598c4..23ceb849b 100644 --- a/plugins/stockpiles/stockpiles.cpp +++ b/plugins/stockpiles/stockpiles.cpp @@ -11,9 +11,6 @@ #include "df/hauling_route.h" #include "df/hauling_stop.h" -#include -#include - using std::string; using std::vector; @@ -23,14 +20,15 @@ DFHACK_PLUGIN("stockpiles"); REQUIRE_GLOBAL(world); -namespace DFHack { - DBG_DECLARE(stockpiles, log, DebugCategory::LINFO); +namespace DFHack +{ +DBG_DECLARE(stockpiles, log, DebugCategory::LINFO); } -static command_result do_command(color_ostream &out, vector ¶meters); +static command_result do_command(color_ostream& out, vector& parameters); -DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(log,out).print("initializing %s\n", plugin_name); +DFhackCExport command_result plugin_init(color_ostream &out, vector &commands) { + DEBUG(log, out).print("initializing %s\n", plugin_name); commands.push_back(PluginCommand( plugin_name,