diff --git a/docs/plugins/misery.rst b/docs/plugins/misery.rst index f2c4d1952..8a65d8419 100644 --- a/docs/plugins/misery.rst +++ b/docs/plugins/misery.rst @@ -2,18 +2,35 @@ misery ====== .. dfhack-tool:: - :summary: Increase the intensity of negative dwarven thoughts. - :tags: fort armok auto units + :summary: Increase the intensity of your citizens' negative thoughts. + :tags: fort gameplay units -When enabled, negative thoughts that your dwarves have will multiply by the -specified factor. +When enabled, negative thoughts that your citizens have will multiply by the +specified factor. This makes it more challenging to keep them happy. Usage ----- +:: + + enable misery + misery [status] + misery + misery clear + +The default misery factor is ``2``, meaning that your dwarves will become +miserable twice as fast. + +Examples +-------- + ``enable misery`` - Start multiplying negative thoughts. -``misery `` - Change the multiplicative factor of bad thoughts. The default is ``2``. + Start multiplying bad thoughts for your citizens! + +``misery 5`` + Make dwarves become unhappy 5 times faster than normal -- this is quite + challenging to handle! + ``misery clear`` - Clear away negative thoughts added by ``misery``. + Clear away negative thoughts added by ``misery``. Note that this will not + clear negative thoughts that your dwarves accumulated "naturally". diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index df49c3d59..89733bf20 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -130,7 +130,7 @@ dfhack_plugin(liquids liquids.cpp Brushes.h LINK_LIBRARIES lua) #dfhack_plugin(luasocket luasocket.cpp LINK_LIBRARIES clsocket lua dfhack-tinythread) #dfhack_plugin(manipulator manipulator.cpp) #dfhack_plugin(map-render map-render.cpp LINK_LIBRARIES lua) -dfhack_plugin(misery misery.cpp) +dfhack_plugin(misery misery.cpp LINK_LIBRARIES lua) #dfhack_plugin(mode mode.cpp) #dfhack_plugin(mousequery mousequery.cpp) dfhack_plugin(nestboxes nestboxes.cpp) diff --git a/plugins/lua/misery.lua b/plugins/lua/misery.lua new file mode 100644 index 000000000..cd507c9b3 --- /dev/null +++ b/plugins/lua/misery.lua @@ -0,0 +1,43 @@ +local _ENV = mkmodule('plugins.misery') + +local argparse = require('argparse') + +local function process_args(opts, args) + if args[1] == 'help' then + opts.help = true + return + end + + return argparse.processArgsGetopt(args, { + {'h', 'help', handler=function() opts.help = true end}, + }) +end + +function status() + print(('misery is %s'):format(isEnabled() and "enabled" or "disabled")) + print(('misery factor is: %d'):format(misery_getFactor())) +end + +function parse_commandline(...) + local args, opts = {...}, {} + local positionals = process_args(opts, args) + + if opts.help then + return false + end + + local command = table.remove(positionals, 1) + if not command or command == 'status' then + status() + elseif command == 'factor' then + misery_setFactor(positionals[1]) + elseif command == 'clear' then + misery_clear() + else + return false + end + + return true +end + +return _ENV diff --git a/plugins/misery.cpp b/plugins/misery.cpp index 870c4480c..f241394ae 100644 --- a/plugins/misery.cpp +++ b/plugins/misery.cpp @@ -1,14 +1,7 @@ #include -#include #include #include -#include "DataDefs.h" -#include "Export.h" -#include "PluginManager.h" - -#include "modules/Units.h" - #include "df/emotion_type.h" #include "df/plotinfost.h" #include "df/unit.h" @@ -17,179 +10,266 @@ #include "df/unit_thought_type.h" #include "df/world.h" -using namespace std; +#include "modules/Persistence.h" +#include "modules/Units.h" +#include "modules/World.h" + +#include "Core.h" +#include "Debug.h" +#include "LuaTools.h" +#include "PluginManager.h" + +using std::string; +using std::vector; + using namespace DFHack; DFHACK_PLUGIN("misery"); DFHACK_PLUGIN_IS_ENABLED(is_enabled); -REQUIRE_GLOBAL(world); -REQUIRE_GLOBAL(plotinfo); REQUIRE_GLOBAL(cur_year); REQUIRE_GLOBAL(cur_year_tick); +REQUIRE_GLOBAL(world); -typedef df::unit_personality::T_emotions Emotion; - -static int factor = 1; -static int tick = 0; -const int INTERVAL = 1000; +namespace DFHack { + DBG_DECLARE(misery, cycle, DebugCategory::LINFO); + DBG_DECLARE(misery, config, DebugCategory::LINFO); +} -command_result misery(color_ostream& out, vector& parameters); -void add_misery(df::unit *unit); -void clear_misery(df::unit *unit); +static const string CONFIG_KEY = string(plugin_name) + "/config"; +static PersistentDataItem config; -const int FAKE_EMOTION_FLAG = (1 << 30); -const int STRENGTH_MULTIPLIER = 100; +enum ConfigValues { + CONFIG_IS_ENABLED = 0, + CONFIG_FACTOR = 1, +}; -bool is_valid_unit (df::unit *unit) { - if (!Units::isOwnRace(unit) || !Units::isOwnCiv(unit)) - return false; - if (!Units::isActive(unit)) - return false; - return true; +static int get_config_val(PersistentDataItem &c, int index) { + if (!c.isValid()) + return -1; + return c.ival(index); +} +static bool get_config_bool(PersistentDataItem &c, int index) { + return get_config_val(c, index) == 1; +} +static void set_config_val(PersistentDataItem &c, int index, int value) { + if (c.isValid()) + c.ival(index) = value; +} +static void set_config_bool(PersistentDataItem &c, int index, bool value) { + set_config_val(c, index, value ? 1 : 0); } -inline bool is_fake_emotion (Emotion *e) { - return e->flags.whole & FAKE_EMOTION_FLAG; +static const int32_t CYCLE_TICKS = 1200; // one day +static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle + +static command_result do_command(color_ostream &out, vector ¶meters); +static void do_cycle(color_ostream &out); + +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { + DEBUG(config,out).print("initializing %s\n", plugin_name); + + // provide a configuration interface for the plugin + commands.push_back(PluginCommand( + plugin_name, + "Increase the intensity of negative dwarven thoughts.", + do_command)); + + return CR_OK; } -void add_misery (df::unit *unit) { - // Add a fake miserable thought - // Remove any fake ones that already exist - if (!unit || !unit->status.current_soul) - return; - clear_misery(unit); - auto &emotions = unit->status.current_soul->personality.emotions; - Emotion *e = new Emotion; - e->type = df::emotion_type::MISERY; - e->thought = df::unit_thought_type::SoapyBath; - e->flags.whole |= FAKE_EMOTION_FLAG; - emotions.push_back(e); +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { + if (!Core::getInstance().isWorldLoaded()) { + out.printerr("Cannot enable %s without a loaded world.\n", plugin_name); + return CR_FAILURE; + } - for (Emotion *e : emotions) { - if (is_fake_emotion(e)) { - e->year = *cur_year; - e->year_tick = *cur_year_tick; - e->strength = STRENGTH_MULTIPLIER * factor; - e->severity = STRENGTH_MULTIPLIER * factor; - } + if (enable != is_enabled) { + is_enabled = enable; + DEBUG(config,out).print("%s from the API; persisting\n", + is_enabled ? "enabled" : "disabled"); + set_config_bool(config, CONFIG_IS_ENABLED, is_enabled); + if (enable) + do_cycle(out); + } else { + DEBUG(config,out).print("%s from the API, but already %s; no action\n", + is_enabled ? "enabled" : "disabled", + is_enabled ? "enabled" : "disabled"); } + return CR_OK; } -void clear_misery (df::unit *unit) { - if (!unit || !unit->status.current_soul) - return; - auto &emotions = unit->status.current_soul->personality.emotions; - auto it = remove_if(emotions.begin(), emotions.end(), [](Emotion *e) { - if (is_fake_emotion(e)) { - delete e; - return true; - } - return false; - }); - emotions.erase(it, emotions.end()); -} +DFhackCExport command_result plugin_shutdown (color_ostream &out) { + DEBUG(config,out).print("shutting down %s\n", plugin_name); -DFhackCExport command_result plugin_shutdown(color_ostream& out) { - factor = 0; return CR_OK; } -DFhackCExport command_result plugin_onupdate(color_ostream& out) { - static bool wasLoaded = false; - if ( factor == 0 || !world || !world->map.block_index ) { - if ( wasLoaded ) { - //we just unloaded the game: clear all data - factor = 0; - is_enabled = false; - wasLoaded = false; - } - return CR_OK; - } +DFhackCExport command_result plugin_load_data (color_ostream &out) { + cycle_timestamp = 0; + config = World::GetPersistentData(CONFIG_KEY); - if ( !wasLoaded ) { - wasLoaded = true; + if (!config.isValid()) { + DEBUG(config,out).print("no config found in this save; initializing\n"); + config = World::AddPersistentData(CONFIG_KEY); + set_config_bool(config, CONFIG_IS_ENABLED, is_enabled); + set_config_val(config, CONFIG_FACTOR, 2); } - if ( tick < INTERVAL ) { - tick++; - return CR_OK; - } - tick = 0; + is_enabled = get_config_bool(config, CONFIG_IS_ENABLED); + DEBUG(config,out).print("loading persisted enabled state: %s\n", + is_enabled ? "true" : "false"); - //TODO: consider units.active - for (df::unit *unit : world->units.all) { - if (is_valid_unit(unit)) { - add_misery(unit); + return CR_OK; +} + +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { + if (event == DFHack::SC_WORLD_UNLOADED) { + if (is_enabled) { + DEBUG(config,out).print("world unloaded; disabling %s\n", + plugin_name); + is_enabled = false; } } - return CR_OK; } -DFhackCExport command_result plugin_init(color_ostream& out, vector &commands) { - commands.push_back(PluginCommand( - "misery", - "Increase the intensity of negative dwarven thoughts.", - misery)); +DFhackCExport command_result plugin_onupdate(color_ostream &out) { + if (is_enabled && world->frame_counter - cycle_timestamp >= CYCLE_TICKS) + do_cycle(out); return CR_OK; } -DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) -{ - if (enable != is_enabled) - { - is_enabled = enable; - factor = enable ? 1 : 0; - tick = INTERVAL; - } +static bool call_misery_lua(color_ostream *out, const char *fn_name, + int nargs = 0, int nres = 0, + Lua::LuaLambda && args_lambda = Lua::DEFAULT_LUA_LAMBDA, + Lua::LuaLambda && res_lambda = Lua::DEFAULT_LUA_LAMBDA) { + DEBUG(config).print("calling misery lua function: '%s'\n", fn_name); - return CR_OK; + CoreSuspender guard; + + auto L = Lua::Core::State; + Lua::StackUnwinder top(L); + + if (!out) + out = &Core::getInstance().getConsole(); + + return Lua::CallLuaModuleFunction(*out, L, "plugins.misery", fn_name, + nargs, nres, + std::forward(args_lambda), + std::forward(res_lambda)); } -command_result misery(color_ostream &out, vector& parameters) { - if ( !world || !world->map.block_index ) { - out.printerr("misery can only be enabled in fortress mode with a fully-loaded game.\n"); +static command_result do_command(color_ostream &out, vector ¶meters) { + CoreSuspender suspend; + + if (!Core::getInstance().isWorldLoaded()) { + out.printerr("Cannot run %s without a loaded world.\n", plugin_name); return CR_FAILURE; } - if ( parameters.size() < 1 || parameters.size() > 2 ) { - return CR_WRONG_USAGE; + bool show_help = false; + if (!call_misery_lua(&out, "parse_commandline", parameters.size(), 1, + [&](lua_State *L) { + for (const string ¶m : parameters) + Lua::Push(L, param); + }, + [&](lua_State *L) { + show_help = !lua_toboolean(L, -1); + })) { + return CR_FAILURE; } - if ( parameters[0] == "disable" ) { - if ( parameters.size() > 1 ) { - return CR_WRONG_USAGE; - } - factor = 0; - is_enabled = false; - return CR_OK; - } else if ( parameters[0] == "enable" ) { - is_enabled = true; - factor = 1; - if ( parameters.size() == 2 ) { - int a = atoi(parameters[1].c_str()); - if ( a < 1 ) { - out.printerr("Second argument must be a positive integer.\n"); - return CR_WRONG_USAGE; - } - factor = a; - } - tick = INTERVAL; - } else if ( parameters[0] == "clear" ) { - for (df::unit *unit : world->units.all) { - if (is_valid_unit(unit)) { - clear_misery(unit); - } - } - } else { - int a = atoi(parameters[0].c_str()); - if ( a < 0 ) { - return CR_WRONG_USAGE; + return show_help ? CR_WRONG_USAGE : CR_OK; +} + +///////////////////////////////////////////////////// +// cycle logic +// + +const int FAKE_EMOTION_FLAG = (1 << 30); +const int STRENGTH_MULTIPLIER = 100; + +typedef df::unit_personality::T_emotions Emotion; + +static bool is_fake_emotion(Emotion *e) { + return e->flags.whole & FAKE_EMOTION_FLAG; +} + +static void clear_misery(df::unit *unit) { + if (!unit || !unit->status.current_soul) + return; + auto &emotions = unit->status.current_soul->personality.emotions; + auto it = std::remove_if(emotions.begin(), emotions.end(), [](Emotion *e) { + if (is_fake_emotion(e)) { + delete e; + return true; } - factor = a; - is_enabled = factor > 0; + return false; + }); + emotions.erase(it, emotions.end()); +} +// clears fake negative thoughts then runs the given lambda +static void affect_units( + std::function &&process_unit = [](df::unit *){}) { + for (auto unit : world->units.active) { + if (!Units::isCitizen(unit) || !unit->status.current_soul) + continue; + + clear_misery(unit); + std::forward &&>(process_unit)(unit); } +} - return CR_OK; +static void do_cycle(color_ostream &out) { + // mark that we have recently run + cycle_timestamp = world->frame_counter; + + DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + + int strength = STRENGTH_MULTIPLIER * get_config_val(config, CONFIG_FACTOR); + + affect_units([&](df::unit *unit) { + Emotion *e = new Emotion; + e->type = df::emotion_type::MISERY; + e->thought = df::unit_thought_type::SoapyBath; + e->flags.whole |= FAKE_EMOTION_FLAG; + e->year = *cur_year; + e->year_tick = *cur_year_tick; + e->strength = strength; + e->severity = strength; + unit->status.current_soul->personality.emotions.push_back(e); + }); } + +///////////////////////////////////////////////////// +// Lua API +// + +static void misery_clear(color_ostream &out) { + DEBUG(config,out).print("entering misery_clear\n"); + affect_units(); +} + +static void misery_setFactor(color_ostream &out, int32_t factor) { + DEBUG(config,out).print("entering misery_setFactor\n"); + if (1 >= factor) { + out.printerr("factor must be at least 2\n"); + return; + } + set_config_val(config, CONFIG_FACTOR, factor); + if (is_enabled) + do_cycle(out); +} + +static int misery_getFactor(color_ostream &out) { + DEBUG(config,out).print("entering tailor_getFactor\n"); + return get_config_val(config, CONFIG_FACTOR); +} + +DFHACK_PLUGIN_LUA_FUNCTIONS { + DFHACK_LUA_FUNCTION(misery_clear), + DFHACK_LUA_FUNCTION(misery_setFactor), + DFHACK_LUA_FUNCTION(misery_getFactor), + DFHACK_LUA_END +};