diff --git a/data/init/dfhack.tools.init b/data/init/dfhack.tools.init index fdad95be6..e2abf3dbe 100644 --- a/data/init/dfhack.tools.init +++ b/data/init/dfhack.tools.init @@ -84,6 +84,9 @@ enable overlay # Allow buildings to be placed now and built later when materials are available enable buildingplan +#Allow designated stockpiles to automatically mark items for melting +enable automelt + # Dwarf Manipulator (simple in-game Dwarf Therapist replacement) #enable manipulator diff --git a/docs/plugins/automelt.rst b/docs/plugins/automelt.rst index 9abe2cc9c..6b14d9dc4 100644 --- a/docs/plugins/automelt.rst +++ b/docs/plugins/automelt.rst @@ -6,9 +6,9 @@ automelt :tags: untested fort productivity items stockpiles :no-command: -When `enabled `, this plugin adds an option to the :kbd:`q` menu for -stockpiles. When the ``automelt`` option is selected for the stockpile, any -items placed in the stockpile will automatically be designated to be melted. +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 ----- @@ -16,3 +16,40 @@ 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/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index a05ae6a83..95b1d9ba8 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -83,7 +83,7 @@ dfhack_plugin(autofarm autofarm.cpp) #dfhack_plugin(autogems autogems.cpp LINK_LIBRARIES jsoncpp_static) #add_subdirectory(autolabor) #dfhack_plugin(automaterial automaterial.cpp LINK_LIBRARIES lua) -#dfhack_plugin(automelt automelt.cpp) +dfhack_plugin(automelt automelt.cpp LINK_LIBRARIES lua) #dfhack_plugin(autonestbox autonestbox.cpp LINK_LIBRARIES lua) #dfhack_plugin(autotrade autotrade.cpp) dfhack_plugin(blueprint blueprint.cpp LINK_LIBRARIES lua) diff --git a/plugins/automelt.cpp b/plugins/automelt.cpp index 9ca20e9ea..5b68bb10b 100644 --- a/plugins/automelt.cpp +++ b/plugins/automelt.cpp @@ -1,294 +1,827 @@ -#include "uicommon.h" +#include "Debug.h" +#include "LuaTools.h" +#include "PluginManager.h" +#include "TileTypes.h" + +#include "modules/Buildings.h" +#include "modules/Maps.h" +#include "modules/Items.h" +#include "modules/World.h" +#include "modules/Designations.h" +#include "modules/Persistence.h" +#include "modules/Units.h" +#include "modules/Screen.h" #include "modules/Gui.h" +// #include "uicommon.h" + #include "df/world.h" +#include "df/building.h" #include "df/world_raws.h" #include "df/building_def.h" #include "df/viewscreen_dwarfmodest.h" #include "df/building_stockpilest.h" -#include "modules/Buildings.h" -#include "modules/Items.h" #include "df/plotinfost.h" -#include "modules/Maps.h" -#include "modules/World.h" #include "df/item_quality.h" +#include +#include + using df::building_stockpilest; +using std::map; +using std::multimap; +using std::pair; +using std::string; +using std::unordered_map; +using std::vector; + +using namespace DFHack; +using namespace df::enums; DFHACK_PLUGIN("automelt"); -#define PLUGIN_VERSION 0.3 +DFHACK_PLUGIN_IS_ENABLED(is_enabled); REQUIRE_GLOBAL(gps); REQUIRE_GLOBAL(world); REQUIRE_GLOBAL(cursor); REQUIRE_GLOBAL(plotinfo); -static const string PERSISTENCE_KEY = "automelt/stockpiles"; +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 vector watched_stockpiles; +// static unordered_map watched_stockpiles_indices; + +static unordered_map watched_stockpiles; -static int mark_item(df::item *item, df::item_flags bad_flags, int32_t stockpile_id) +enum StockpileConfigValues { - if (item->flags.whole & bad_flags.whole) - return 0; + STOCKPILE_CONFIG_ID = 0, + STOCKPILE_CONFIG_MONITORED = 1, - if (item->isAssignedToThisStockpile(stockpile_id)) { - size_t marked_count = 0; - std::vector contents; - Items::getContainedItems(item, &contents); - for (auto child = contents.begin(); child != contents.end(); child++) - { - marked_count += mark_item(*child, bad_flags, stockpile_id); - } +}; - return marked_count; - } +static int get_config_val(PersistentDataItem &c, int index) +{ + if (!c.isValid()) + return -1; + return c.ival(index); +} - if (!can_melt(item)) - return 0; +static bool get_config_bool(PersistentDataItem &c, int index) +{ + return get_config_val(c, index) == 1; +} - if (is_set_to_melt(item)) - return 0; +static void set_config_val(PersistentDataItem &c, int index, int value) +{ + if (c.isValid()) + c.ival(index) = value; +} - insert_into_vector(world->items.other[items_other_id::ANY_MELT_DESIGNATED], &df::item::id, item); - item->flags.bits.melt = true; - return 1; +static void set_config_bool(PersistentDataItem &c, int index, bool value) +{ + set_config_val(c, index, value ? 1 : 0); } -static void mark_all_in_stockpiles(vector &stockpiles) +static PersistentDataItem &ensure_stockpile_config(color_ostream &out, int id) { - // 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 (auto it = stockpiles.begin(); it != stockpiles.end(); it++) - { - if (!it->isValid()) - continue; + 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]; + } - auto spid = it->getId(); - Buildings::StockpileIterator stored; - for (stored.begin(it->getStockpile()); !stored.done(); ++stored) - { - marked_count += mark_item(*stored, bad_flags, spid); + 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 void validate_stockpile_configs(color_ostream &out) +{ + for (auto &c : watched_stockpiles) { + int id = get_config_val(c.second, STOCKPILE_CONFIG_ID); + if (!df::building::find(id)){ + 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, std::vector &commands) +{ + DEBUG(status, out).print("initializing %s\n", plugin_name); - if (marked_count) - Gui::showAnnouncement("Marked " + int_to_string(marked_count) + " items to melt", COLOR_GREEN, false); + // provide a configuration interface for the plugin + commands.push_back(PluginCommand( + plugin_name, + "Auto melt items in designated stockpiles.", + do_command)); + + return CR_OK; } -/* - * Stockpile Monitoring - */ +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; +} -class StockpileMonitor +DFhackCExport command_result plugin_load_data(color_ostream &out) { -public: - bool isMonitored(df::building_stockpilest *sp) + config = World::GetPersistentData(CONFIG_KEY); + + if (!config.isValid()) { - for (auto it = monitored_stockpiles.begin(); it != monitored_stockpiles.end(); it++) - { - if (it->matches(sp)) - return true; - } + DEBUG(status, out).print("no config found in this save; initializing\n"); + config = World::AddPersistentData(CONFIG_KEY); + } - return false; + 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; +} - void add(df::building_stockpilest *sp) +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) +{ + if (event == DFHack::SC_WORLD_UNLOADED) { - auto pile = PersistentStockpileInfo(sp, PERSISTENCE_KEY); - if (pile.isValid()) + if (is_enabled) { - monitored_stockpiles.push_back(PersistentStockpileInfo(pile)); - monitored_stockpiles.back().save(); + DEBUG(status, out).print("world unloaded; disabling %s\n", plugin_name); + is_enabled = false; } } - void remove(df::building_stockpilest *sp) + 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) { - for (auto it = monitored_stockpiles.begin(); it != monitored_stockpiles.end(); it++) + 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) +{ + MaterialInfo mat(item); + return (mat.getCraftClass() == craft_material_class::Metal); +} + +static bool isStockpile(df::building * building) { + return building->getType() == df::building_type::Stockpile; +} + +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 (item->flags.whole & bad_flags.whole) + return false; + + df::item_type t = item->getType(); + + if (t == df::enums::item_type::BOX || t == df::enums::item_type::BAR) + return false; + + if (!is_metal_item(item)) + return false; + + for (auto &g : item->general_refs) + { + switch (g->getType()) { - if (it->matches(sp)) + 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) { - it->remove(); - monitored_stockpiles.erase(it); - break; + if (gg->getType() == general_ref_type::UNIT_HOLDER) + return false; } } + break; + default: + break; + } } - void doCycle() - { - for (auto it = monitored_stockpiles.begin(); it != monitored_stockpiles.end();) - { - if (!it->isValid()) - it = monitored_stockpiles.erase(it); - else - ++it; - } + if (item->getQuality() >= item_quality::Masterful) + return false; + + return true; +} - mark_all_in_stockpiles(monitored_stockpiles); +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\nshould_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); } - void reset() - { - monitored_stockpiles.clear(); - std::vector items; - DFHack::World::GetPersistentData(&items, PERSISTENCE_KEY); + if (item->flags.whole & bad_flags.whole){ + DEBUG(perf,out).print("rejected flag check\n"); + return 0; + } - for (auto i = items.begin(); i != items.end(); i++) + if (item->isAssignedToThisStockpile(stockpile_id)) + { + DEBUG(perf,out).print("assignedToStockpile\n"); + size_t marked_count = 0; + std::vector contents; + Items::getContainedItems(item, &contents); + for (auto child = contents.begin(); child != contents.end(); child++) { - auto pile = PersistentStockpileInfo(*i, PERSISTENCE_KEY); - if (pile.load()) - monitored_stockpiles.push_back(PersistentStockpileInfo(pile)); - else - pile.remove(); + 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"); -private: - vector monitored_stockpiles; -}; + 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; + } -static StockpileMonitor monitor; + if (!can_melt(item)) { + DEBUG(perf,out).print("cannot melt\n"); + return 0; + } -#define DELTA_TICKS 610 + 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; + } + +} -DFhackCExport command_result plugin_onupdate ( color_ostream &out ) + +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) { - if(!Maps::IsValid()) - return CR_OK; + DEBUG(perf,out).print("%s running mark_all_in_stockpile\nshould_melt=%d\n", plugin_name, should_melt); - if (DFHack::World::ReadPauseState()) - return CR_OK; + static const BadFlagsMarkItem bad_flags; - if (world->frame_counter % DELTA_TICKS != 0) - return CR_OK; + int32_t marked_count = 0; - monitor.doCycle(); + if(!stockpile.isValid()) { + return 0; + } - return CR_OK; + 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; } -/* - * Interface - */ +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; -struct melt_hook : public df::viewscreen_dwarfmodest -{ - typedef df::viewscreen_dwarfmodest interpose_base; + item_count_piles.clear(); + premarked_item_count_piles.clear(); + marked_item_count_piles.clear(); - bool handleInput(set *input) - { - if (Gui::inRenameBuilding()) - return false; + //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); - building_stockpilest *sp = get_selected_stockpile(); - if (!sp) - return false; + if (!monitored) continue; - if (input->count(interface_key::CUSTOM_SHIFT_M)) - { - if (monitor.isMonitored(sp)) - monitor.remove(sp); - else - monitor.add(sp); + 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; +} - return false; +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; + + tracked_item_map_piles.clear(); + + 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]; } - DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) - { - if (!handleInput(input)) - INTERPOSE_NEXT(feed)(input); + 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 *selected_bldg = NULL; + selected_bldg = Gui::getSelectedBuilding(out, true); + if (selected_bldg->getType() != df::building_type::Stockpile) { + DEBUG(status,out).print("Selected building is not stockpile\n"); + return -1; } - 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 - 6; - - 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 += 5; - OutputString(COLOR_LIGHTRED, x, y, "M"); - OutputString(state? COLOR_LIGHTGREEN: COLOR_GREY, x, y, "elt "); + return selected_bldg->id; +} + +static PersistentDataItem *getSelectedStockpileConfig(color_ostream &out) { + int32_t bldg_id = getSelectedStockpile(out); + if (bldg_id == -1) { + DEBUG(status,out).print("Selected bldg invalid\n"); + return NULL; + } + + validate_stockpile_configs(out); + PersistentDataItem *c = NULL; + if (watched_stockpiles.count(bldg_id)) { + c = &(watched_stockpiles[bldg_id]); + return c; + } else { + 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 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 { - OutputToggleString(x, y, "Auto melt", "M", state, true, left_margin, COLOR_WHITE, COLOR_LIGHTRED); + 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]; + } -IMPLEMENT_VMETHOD_INTERPOSE(melt_hook, feed); -IMPLEMENT_VMETHOD_INTERPOSE(melt_hook, render); + 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()); + } -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; + DEBUG(status,out).print("exiting automelt_printStatus\n"); + } -DFHACK_PLUGIN_IS_ENABLED(is_enabled); +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 = !bldg || !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; + } -DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) -{ - if (enable != is_enabled) - { - if (!INTERPOSE_HOOK(melt_hook, feed).apply(enable) || - !INTERPOSE_HOOK(melt_hook, render).apply(enable)) - return CR_FAILURE; + PersistentDataItem &c = ensure_stockpile_config(out, id); + set_config_val(c, STOCKPILE_CONFIG_ID, id); + set_config_bool(c, STOCKPILE_CONFIG_MONITORED, monitored); - is_enabled = enable; + //If we're marking something as monitored, go ahead and designate contents. + if (monitored) { + automelt_designate(out); } +} - return CR_OK; +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; } -DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) -{ - return CR_OK; +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; } -DFhackCExport command_result plugin_shutdown ( color_ostream &out ) -{ - // ensure we disengage our hooks - plugin_enable(out, false); - return CR_OK; +//TODO +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); + int32_t bldg_count = 0; + + 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]); + } else { + push_stockpile_config(L, id, false); + } + } + DEBUG(perf, *out).print("exit automelt_getItemCountsAndStockpileConfigs\n"); + + return 4+bldg_count; } + +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/lua/automelt.lua b/plugins/lua/automelt.lua new file mode 100644 index 000000000..4f1d82ad9 --- /dev/null +++ b/plugins/lua/automelt.lua @@ -0,0 +1,80 @@ +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) + config[var_name] = val + automelt_setStockpileConfig(config.id, config.monitor, config.melt) + 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) + ret.stockpile_configs = data + for _,c in ipairs(ret.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 + + end + return ret +end + +return _ENV