Merge pull request #2852 from myk002/myk_misery

update misery
develop
Myk 2023-02-08 14:22:33 -08:00 committed by GitHub
commit 90767b83d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 286 additions and 145 deletions

@ -46,6 +46,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
## Misc Improvements ## Misc Improvements
- `automelt`: is now more resistent to savegame corruption - `automelt`: is now more resistent to savegame corruption
- `hotkeys`: DFHack logo is now hidden on screens where it covers important information when in the default position (e.g. when choosing an embark site) - `hotkeys`: DFHack logo is now hidden on screens where it covers important information when in the default position (e.g. when choosing an embark site)
- `misery`: now persists state with the fort
- `autodump`: reinstate ``autodump-destroy-item``, hotkey: Ctrl-K - `autodump`: reinstate ``autodump-destroy-item``, hotkey: Ctrl-K
- `autodump`: new hotkey for ``autodump-destroy-here``: Ctrl-H - `autodump`: new hotkey for ``autodump-destroy-here``: Ctrl-H
- `dig`: new hotkeys for vein designation on z-level (Ctrl-V) and vein designation across z-levels (Ctrl-Shift-V) - `dig`: new hotkeys for vein designation on z-level (Ctrl-V) and vein designation across z-levels (Ctrl-Shift-V)

@ -2,18 +2,35 @@ misery
====== ======
.. dfhack-tool:: .. dfhack-tool::
:summary: Increase the intensity of negative dwarven thoughts. :summary: Increase the intensity of your citizens' negative thoughts.
:tags: fort armok auto units :tags: fort gameplay units
When enabled, negative thoughts that your dwarves have will multiply by the When enabled, negative thoughts that your citizens have will multiply by the
specified factor. specified factor. This makes it more challenging to keep them happy.
Usage 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`` ``enable misery``
Start multiplying negative thoughts. Start multiplying bad thoughts for your citizens!
``misery <factor>``
Change the multiplicative factor of bad thoughts. The default is ``2``. ``misery 5``
Make dwarves become unhappy 5 times faster than normal -- this is quite
challenging to handle!
``misery clear`` ``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(luasocket luasocket.cpp LINK_LIBRARIES clsocket lua dfhack-tinythread)
#dfhack_plugin(manipulator manipulator.cpp) #dfhack_plugin(manipulator manipulator.cpp)
#dfhack_plugin(map-render map-render.cpp LINK_LIBRARIES lua) #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(mode mode.cpp)
#dfhack_plugin(mousequery mousequery.cpp) #dfhack_plugin(mousequery mousequery.cpp)
dfhack_plugin(nestboxes nestboxes.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 <algorithm>
#include <map>
#include <string> #include <string>
#include <vector> #include <vector>
#include "DataDefs.h"
#include "Export.h"
#include "PluginManager.h"
#include "modules/Units.h"
#include "df/emotion_type.h" #include "df/emotion_type.h"
#include "df/plotinfost.h" #include "df/plotinfost.h"
#include "df/unit.h" #include "df/unit.h"
@ -17,179 +10,266 @@
#include "df/unit_thought_type.h" #include "df/unit_thought_type.h"
#include "df/world.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; using namespace DFHack;
DFHACK_PLUGIN("misery"); DFHACK_PLUGIN("misery");
DFHACK_PLUGIN_IS_ENABLED(is_enabled); DFHACK_PLUGIN_IS_ENABLED(is_enabled);
REQUIRE_GLOBAL(world);
REQUIRE_GLOBAL(plotinfo);
REQUIRE_GLOBAL(cur_year); REQUIRE_GLOBAL(cur_year);
REQUIRE_GLOBAL(cur_year_tick); REQUIRE_GLOBAL(cur_year_tick);
REQUIRE_GLOBAL(world);
typedef df::unit_personality::T_emotions Emotion; namespace DFHack {
DBG_DECLARE(misery, cycle, DebugCategory::LINFO);
static int factor = 1; DBG_DECLARE(misery, config, DebugCategory::LINFO);
static int tick = 0; }
const int INTERVAL = 1000;
command_result misery(color_ostream& out, vector<string>& parameters); static const string CONFIG_KEY = string(plugin_name) + "/config";
void add_misery(df::unit *unit); static PersistentDataItem config;
void clear_misery(df::unit *unit);
const int FAKE_EMOTION_FLAG = (1 << 30); enum ConfigValues {
const int STRENGTH_MULTIPLIER = 100; CONFIG_IS_ENABLED = 0,
CONFIG_FACTOR = 1,
};
bool is_valid_unit (df::unit *unit) { static int get_config_val(PersistentDataItem &c, int index) {
if (!Units::isOwnRace(unit) || !Units::isOwnCiv(unit)) if (!c.isValid())
return false; return -1;
if (!Units::isActive(unit)) return c.ival(index);
return false; }
return true; 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) { static const int32_t CYCLE_TICKS = 1200; // one day
return e->flags.whole & FAKE_EMOTION_FLAG; static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle
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 add_misery (df::unit *unit) { DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) {
// Add a fake miserable thought if (!Core::getInstance().isWorldLoaded()) {
// Remove any fake ones that already exist out.printerr("Cannot enable %s without a loaded world.\n", plugin_name);
if (!unit || !unit->status.current_soul) return CR_FAILURE;
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);
for (Emotion *e : emotions) { if (enable != is_enabled) {
if (is_fake_emotion(e)) { is_enabled = enable;
e->year = *cur_year; DEBUG(config,out).print("%s from the API; persisting\n",
e->year_tick = *cur_year_tick; is_enabled ? "enabled" : "disabled");
e->strength = STRENGTH_MULTIPLIER * factor; set_config_bool(config, CONFIG_IS_ENABLED, is_enabled);
e->severity = STRENGTH_MULTIPLIER * factor; 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) { DFhackCExport command_result plugin_shutdown (color_ostream &out) {
if (!unit || !unit->status.current_soul) DEBUG(config,out).print("shutting down %s\n", plugin_name);
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) {
factor = 0;
return CR_OK; return CR_OK;
} }
DFhackCExport command_result plugin_onupdate(color_ostream& out) { DFhackCExport command_result plugin_load_data (color_ostream &out) {
static bool wasLoaded = false; cycle_timestamp = 0;
if ( factor == 0 || !world || !world->map.block_index ) { config = World::GetPersistentData(CONFIG_KEY);
if ( wasLoaded ) {
//we just unloaded the game: clear all data
factor = 0;
is_enabled = false;
wasLoaded = false;
}
return CR_OK;
}
if ( !wasLoaded ) { if (!config.isValid()) {
wasLoaded = true; 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 ) { is_enabled = get_config_bool(config, CONFIG_IS_ENABLED);
tick++; DEBUG(config,out).print("loading persisted enabled state: %s\n",
return CR_OK; is_enabled ? "true" : "false");
}
tick = 0;
//TODO: consider units.active return CR_OK;
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; return CR_OK;
} }
DFhackCExport command_result plugin_init(color_ostream& out, vector<PluginCommand> &commands) { DFhackCExport command_result plugin_onupdate(color_ostream &out) {
commands.push_back(PluginCommand( if (is_enabled && world->frame_counter - cycle_timestamp >= CYCLE_TICKS)
"misery", do_cycle(out);
"Increase the intensity of negative dwarven thoughts.",
misery));
return CR_OK; return CR_OK;
} }
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) static bool call_misery_lua(color_ostream *out, const char *fn_name,
{ int nargs = 0, int nres = 0,
if (enable != is_enabled) Lua::LuaLambda && args_lambda = Lua::DEFAULT_LUA_LAMBDA,
{ Lua::LuaLambda && res_lambda = Lua::DEFAULT_LUA_LAMBDA) {
is_enabled = enable; DEBUG(config).print("calling misery lua function: '%s'\n", fn_name);
factor = enable ? 1 : 0;
tick = INTERVAL;
}
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) { static command_result do_command(color_ostream &out, vector<string> &parameters) {
if ( !world || !world->map.block_index ) { CoreSuspender suspend;
out.printerr("misery can only be enabled in fortress mode with a fully-loaded game.\n");
if (!Core::getInstance().isWorldLoaded()) {
out.printerr("Cannot run %s without a loaded world.\n", plugin_name);
return CR_FAILURE; return CR_FAILURE;
} }
if ( parameters.size() < 1 || parameters.size() > 2 ) { bool show_help = false;
return CR_WRONG_USAGE; 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" ) { return show_help ? CR_WRONG_USAGE : CR_OK;
if ( parameters.size() > 1 ) { }
return CR_WRONG_USAGE;
} /////////////////////////////////////////////////////
factor = 0; // cycle logic
is_enabled = false; //
return CR_OK;
} else if ( parameters[0] == "enable" ) { const int FAKE_EMOTION_FLAG = (1 << 30);
is_enabled = true; const int STRENGTH_MULTIPLIER = 100;
factor = 1;
if ( parameters.size() == 2 ) { typedef df::unit_personality::T_emotions Emotion;
int a = atoi(parameters[1].c_str());
if ( a < 1 ) { static bool is_fake_emotion(Emotion *e) {
out.printerr("Second argument must be a positive integer.\n"); return e->flags.whole & FAKE_EMOTION_FLAG;
return CR_WRONG_USAGE; }
}
factor = a; static void clear_misery(df::unit *unit) {
} if (!unit || !unit->status.current_soul)
tick = INTERVAL; return;
} else if ( parameters[0] == "clear" ) { auto &emotions = unit->status.current_soul->personality.emotions;
for (df::unit *unit : world->units.all) { auto it = std::remove_if(emotions.begin(), emotions.end(), [](Emotion *e) {
if (is_valid_unit(unit)) { if (is_fake_emotion(e)) {
clear_misery(unit); delete e;
} return true;
}
} else {
int a = atoi(parameters[0].c_str());
if ( a < 0 ) {
return CR_WRONG_USAGE;
} }
factor = a; return false;
is_enabled = factor > 0; });
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);
} }
}
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
};