/* * Stockflow plugin. * For best effect, place "stockflow enable" in your dfhack.init configuration, * or set AUTOENABLE to true. */ #include "uicommon.h" #include "LuaTools.h" #include "df/building_stockpilest.h" #include "df/job.h" #include "df/viewscreen_dwarfmodest.h" #include "modules/Gui.h" #include "modules/Maps.h" #include "modules/World.h" using namespace DFHack; using namespace std; using df::building_stockpilest; DFHACK_PLUGIN("stockflow"); #define AUTOENABLE false DFHACK_PLUGIN_IS_ENABLED(enabled); REQUIRE_GLOBAL(gps); REQUIRE_GLOBAL(world); REQUIRE_GLOBAL(plotinfo); bool fast = false; /* * Lua interface. * Currently calls out to Lua functions, but never back in. */ class LuaHelper { public: void cycle(color_ostream &out) { bool found = false; if (fast) { // Ignore the bookkeeper; either gather or enqueue orders every cycle. found = !bookkeeping; } else { // Gather orders when the bookkeeper starts updating stockpile records, // and enqueue them when the job is done. for (df::job_list_link* link = &world->jobs.list; link != NULL; link = link->next) { if (link->item == NULL) continue; if (link->item->job_type == job_type::UpdateStockpileRecords) { found = true; break; } } } if (found) { // Entice the bookkeeper to spend less time update records. plotinfo->nobles.bookkeeper_precision += plotinfo->nobles.bookkeeper_precision >> 3; if (!bookkeeping) { command_method("start_bookkeeping", out); bookkeeping = true; } } else { // Entice the bookkeeper to update records more often. plotinfo->nobles.bookkeeper_precision -= plotinfo->nobles.bookkeeper_precision >> 5; plotinfo->nobles.bookkeeper_cooldown -= plotinfo->nobles.bookkeeper_cooldown >> 2; if (bookkeeping) { command_method("finish_bookkeeping", out); bookkeeping = false; } } } void init() { stockpile_id = -1; initialized = false; bookkeeping = false; } bool reset(color_ostream &out, bool load) { stockpile_id = -1; bookkeeping = false; if (load) { return initialized = command_method("initialize_world", out); } else if (initialized) { initialized = false; return command_method("clear_caches", out); } return true; } bool command_method(const char *method, color_ostream &out) { // Calls a lua function with no parameters. // Suspension is required for "stockflow enable" from the command line, // but may be overkill for other situations. CoreSuspender suspend; auto L = Lua::Core::State; Lua::StackUnwinder top(L); if (!lua_checkstack(L, 1)) return false; if (!Lua::PushModulePublic(out, L, "plugins.stockflow", method)) return false; if (!Lua::SafeCall(out, L, 0, 0)) return false; return true; } bool stockpile_method(const char *method, building_stockpilest *sp) { // Combines the select_order and toggle_trigger method calls, // because they share the same signature. CoreSuspendClaimer suspend; auto L = Lua::Core::State; color_ostream_proxy out(Core::getInstance().getConsole()); Lua::StackUnwinder top(L); if (!lua_checkstack(L, 2)) return false; if (!Lua::PushModulePublic(out, L, "plugins.stockflow", method)) return false; Lua::Push(L, sp); if (!Lua::SafeCall(out, L, 1, 0)) return false; // Invalidate the string cache. stockpile_id = -1; return true; } bool collect_settings(building_stockpilest *sp) { // Find strings representing the job to order, and the trigger condition. // There might be a memory leak here; C++ is odd like that. auto L = Lua::Core::State; color_ostream_proxy out(Core::getInstance().getConsole()); CoreSuspendClaimer suspend; Lua::StackUnwinder top(L); if (!lua_checkstack(L, 2)) return false; if (!Lua::PushModulePublic(out, L, "plugins.stockflow", "stockpile_settings")) return false; Lua::Push(L, sp); if (!Lua::SafeCall(out, L, 1, 2)) return false; if (!lua_isstring(L, -1)) return false; current_trigger = lua_tostring(L, -1); lua_pop(L, 1); if (!lua_isstring(L, -1)) return false; current_job = lua_tostring(L, -1); lua_pop(L, 1); stockpile_id = sp->id; return true; } void draw(building_stockpilest *sp) { if (sp->id != stockpile_id) { if (!collect_settings(sp)) { Core::printerr("Stockflow job collection failed!\n"); return; } } auto dims = Gui::getDwarfmodeViewDims(); int left_margin = dims.menu_x1 + 1; int x = left_margin; int y = dims.y2 - 2; // below autodump, automelt, autotrade, stocks, stockpiles 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(); if (links + 12 >= y) y += 1; OutputHotkeyString(x, y, current_job, "j", true, left_margin, COLOR_WHITE, COLOR_LIGHTRED); if (*current_trigger) OutputHotkeyString(x, y, current_trigger, " J", true, left_margin, COLOR_WHITE, COLOR_LIGHTRED); } private: long stockpile_id; bool initialized; bool bookkeeping; const char *current_job; const char *current_trigger; }; static LuaHelper helper; #define DELTA_TICKS 600 DFhackCExport command_result plugin_onupdate(color_ostream &out) { if (!enabled) return CR_OK; if (!Maps::IsValid()) return CR_OK; if (DFHack::World::ReadPauseState()) return CR_OK; if (world->frame_counter % DELTA_TICKS != 0) return CR_OK; helper.cycle(out); return CR_OK; } /* * Interface hooks */ struct stockflow_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_J)) { // Select a new order for this stockpile. if (!helper.stockpile_method("select_order", sp)) { Core::printerr("Stockflow order selection failed!\n"); } return true; } else if (input->count(interface_key::CUSTOM_SHIFT_J)) { // Toggle the order trigger for this stockpile. if (!helper.stockpile_method("toggle_trigger", sp)) { Core::printerr("Stockflow trigger toggle failed!\n"); } return true; } 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) helper.draw(sp); } }; IMPLEMENT_VMETHOD_INTERPOSE(stockflow_hook, feed); IMPLEMENT_VMETHOD_INTERPOSE(stockflow_hook, render); static bool apply_hooks(color_ostream &out, bool enabling) { if (enabling && !gps) { out.printerr("Stockflow needs graphics.\n"); return false; } if (!INTERPOSE_HOOK(stockflow_hook, feed).apply(enabling) || !INTERPOSE_HOOK(stockflow_hook, render).apply(enabling)) { out.printerr("Could not %s stockflow hooks!\n", enabling? "insert": "remove"); return false; } if (!helper.reset(out, enabling && Maps::IsValid())) { out.printerr("Could not reset stockflow world data!\n"); return false; } return true; } static command_result stockflow_cmd(color_ostream &out, vector & parameters) { bool desired = enabled; if (parameters.size() == 1) { if (parameters[0] == "enable" || parameters[0] == "on" || parameters[0] == "1") { desired = true; fast = false; } else if (parameters[0] == "disable" || parameters[0] == "off" || parameters[0] == "0") { desired = false; fast = false; } else if (parameters[0] == "fast" || parameters[0] == "always" || parameters[0] == "2") { desired = true; fast = true; } else if (parameters[0] == "usage" || parameters[0] == "help" || parameters[0] == "?") { return CR_WRONG_USAGE; } else if (parameters[0] == "list") { if (!enabled) { out.printerr("Stockflow is not currently enabled.\n"); return CR_FAILURE; } if (!Maps::IsValid()) { out.printerr("You haven't loaded a map yet.\n"); return CR_FAILURE; } // Tell Lua to list any saved stockpile orders. return helper.command_method("list_orders", out)? CR_OK: CR_FAILURE; } else if (parameters[0] != "status") { return CR_WRONG_USAGE; } } else if (parameters.size() > 1) { return CR_WRONG_USAGE; } if (desired != enabled) { if (!apply_hooks(out, desired)) { return CR_FAILURE; } } out.print("Stockflow is %s %s%s.\n", (desired == enabled)? "currently": "now", desired? "enabled": "disabled", fast? ", in fast mode": ""); enabled = desired; return CR_OK; } DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { if (event == DFHack::SC_MAP_LOADED) { if (!helper.reset(out, enabled)) { out.printerr("Could not load stockflow world data!\n"); return CR_FAILURE; } } else if (event == DFHack::SC_MAP_UNLOADED) { if (!helper.reset(out, false)) { out.printerr("Could not unload stockflow world data!\n"); return CR_FAILURE; } } return CR_OK; } DFhackCExport command_result plugin_enable(color_ostream& out, bool enable) { /* Accept the "enable stockflow"/"disable stockflow" syntax, where available. */ /* Same as "stockflow enable"/"stockflow disable" except without the status line. */ if (enable != enabled) { if (!apply_hooks(out, enable)) { return CR_FAILURE; } enabled = enable; } return CR_OK; } DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { helper.init(); if (AUTOENABLE) { if (!apply_hooks(out, true)) { return CR_FAILURE; } enabled = true; } commands.push_back(PluginCommand( plugin_name, "Queue manager jobs based on free space in stockpiles.", stockflow_cmd)); return CR_OK; } DFhackCExport command_result plugin_shutdown(color_ostream &out) { return plugin_enable(out, false); }