dfhack/plugins/stockflow.cpp

404 lines
11 KiB
C++

/*
* 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(ui);
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.
ui->nobles.bookkeeper_precision += ui->nobles.bookkeeper_precision >> 3;
if (!bookkeeping) {
command_method("start_bookkeeping", out);
bookkeeping = true;
}
} else {
// Entice the bookkeeper to update records more often.
ui->nobles.bookkeeper_precision -= ui->nobles.bookkeeper_precision >> 5;
ui->nobles.bookkeeper_cooldown -= ui->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<df::interface_key> *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<df::interface_key> *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 <string> & 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 <PluginCommand> &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);
}