update misery; persist state

develop
Myk Taylor 2023-02-08 14:02:44 -08:00
parent 5113823d8c
commit 9f76d64e42
No known key found for this signature in database
GPG Key ID: 8A39CA0FA0C16E78
4 changed files with 285 additions and 145 deletions

@ -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 <factor>
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 <factor>``
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".

@ -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)

@ -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

@ -1,14 +1,7 @@
#include <algorithm>
#include <map>
#include <string>
#include <vector>
#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<string>& 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);
}
inline bool is_fake_emotion (Emotion *e) {
return e->flags.whole & FAKE_EMOTION_FLAG;
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);
}
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);
static const int32_t CYCLE_TICKS = 1200; // one day
static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle
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;
}
}
static command_result do_command(color_ostream &out, vector<string> &parameters);
static void do_cycle(color_ostream &out);
DFhackCExport command_result plugin_init(color_ostream &out, std::vector <PluginCommand> &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 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;
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;
}
return false;
});
emotions.erase(it, emotions.end());
}
DFhackCExport command_result plugin_shutdown(color_ostream& out) {
factor = 0;
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;
}
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;
}
DFhackCExport command_result plugin_shutdown (color_ostream &out) {
DEBUG(config,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 ( !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++;
is_enabled = get_config_bool(config, CONFIG_IS_ENABLED);
DEBUG(config,out).print("loading persisted enabled state: %s\n",
is_enabled ? "true" : "false");
return CR_OK;
}
tick = 0;
}
//TODO: consider units.active
for (df::unit *unit : world->units.all) {
if (is_valid_unit(unit)) {
add_misery(unit);
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<PluginCommand> &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<Lua::LuaLambda&&>(args_lambda),
std::forward<Lua::LuaLambda&&>(res_lambda));
}
command_result misery(color_ostream &out, vector<string>& 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<string> &parameters) {
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 &param : 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;
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;
}
tick = INTERVAL;
} else if ( parameters[0] == "clear" ) {
for (df::unit *unit : world->units.all) {
if (is_valid_unit(unit)) {
return false;
});
emotions.erase(it, emotions.end());
}
// clears fake negative thoughts then runs the given lambda
static void affect_units(
std::function<void(df::unit *)> &&process_unit = [](df::unit *){}) {
for (auto unit : world->units.active) {
if (!Units::isCitizen(unit) || !unit->status.current_soul)
continue;
clear_misery(unit);
std::forward<std::function<void(df::unit *)> &&>(process_unit)(unit);
}
}
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;
}
} else {
int a = atoi(parameters[0].c_str());
if ( a < 0 ) {
return CR_WRONG_USAGE;
}
factor = a;
is_enabled = factor > 0;
}
set_config_val(config, CONFIG_FACTOR, factor);
if (is_enabled)
do_cycle(out);
}
return CR_OK;
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
};