Implements plugin: spectate v0.5

Fixes spectate not starting with the first job

Updates spectate.cpp

- refactors features/settings to under a `Configuration` struct with a global variable `config`
- refactors existing `config` => `pconfig`
- moves plugin logic, mostly, to namespace SP (spectate plugin)
- utilizes debugging log macros
- updates status format
- refactors status print code into a separate function
develop
Josh Cooper 2022-11-08 11:42:12 -08:00 committed by Josh Cooper
parent 086ce64787
commit ec6cd8d53a
2 changed files with 251 additions and 216 deletions

@ -45,4 +45,4 @@ Features
Settings Settings
-------- --------
:tick-threshold: Set the plugin's tick interval for changing the followed dwarf. :tick-threshold: Set the plugin's tick interval for changing the followed dwarf.
Acts as a maximum follow time when used with focus-jobs enabled. (default: 50) Acts as a maximum follow time when used with focus-jobs enabled. (default: 1000)

@ -4,11 +4,13 @@
#include "pause.h" #include "pause.h"
#include "Core.h" #include <Debug.h>
#include <modules/Gui.h> #include <Core.h>
#include <Console.h> #include <Console.h>
#include <Export.h> #include <Export.h>
#include <PluginManager.h> #include <PluginManager.h>
#include <modules/Gui.h>
#include <modules/World.h> #include <modules/World.h>
#include <modules/EventManager.h> #include <modules/EventManager.h>
#include <modules/Job.h> #include <modules/Job.h>
@ -25,6 +27,11 @@
#include <random> #include <random>
#include <cinttypes> #include <cinttypes>
// Debugging
namespace DFHack {
DBG_DECLARE(spectate, plugin, DebugCategory::LINFO);
}
DFHACK_PLUGIN("spectate"); DFHACK_PLUGIN("spectate");
DFHACK_PLUGIN_IS_ENABLED(enabled); DFHACK_PLUGIN_IS_ENABLED(enabled);
REQUIRE_GLOBAL(world); REQUIRE_GLOBAL(world);
@ -36,14 +43,14 @@ using namespace DFHack;
using namespace Pausing; using namespace Pausing;
using namespace df::enums; using namespace df::enums;
void onTick(color_ostream& out, void* tick); struct Configuration {
void onJobStart(color_ostream &out, void* job); bool debug = false;
void onJobCompletion(color_ostream &out, void* job); bool jobs_focus = false;
bool unpause = false;
bool disengage = false;
int32_t tick_threshold = 1000;
} config;
uint64_t tick_threshold = 1000;
bool focus_jobs_enabled = false;
bool disengage_enabled = false;
bool unpause_enabled = false;
Pausing::AnnouncementLock* pause_lock = nullptr; Pausing::AnnouncementLock* pause_lock = nullptr;
bool lock_collision = false; bool lock_collision = false;
bool announcements_disabled = false; bool announcements_disabled = false;
@ -57,9 +64,6 @@ std::set<int32_t> job_tracker;
std::map<uint16_t,int16_t> freq; std::map<uint16_t,int16_t> freq;
std::default_random_engine RNG; std::default_random_engine RNG;
void enable_auto_unpause(color_ostream &out, bool state);
#define base 0.99 #define base 0.99
static const std::string CONFIG_KEY = std::string(plugin_name) + "/config"; static const std::string CONFIG_KEY = std::string(plugin_name) + "/config";
@ -70,144 +74,37 @@ enum ConfigData {
TICK_THRESHOLD TICK_THRESHOLD
}; };
static PersistentDataItem config; static PersistentDataItem pconfig;
inline void saveConfig() {
if (config.isValid()) {
config.ival(UNPAUSE) = unpause_enabled;
config.ival(DISENGAGE) = disengage_enabled;
config.ival(JOB_FOCUS) = focus_jobs_enabled;
config.ival(TICK_THRESHOLD) = tick_threshold;
}
}
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable);
command_result spectate (color_ostream &out, std::vector <std::string> & parameters); command_result spectate (color_ostream &out, std::vector <std::string> & parameters);
DFhackCExport command_result plugin_init (color_ostream &out, std::vector <PluginCommand> &commands) { namespace SP {
commands.push_back(PluginCommand("spectate",
"Automated spectator mode.",
spectate,
false));
pause_lock = new AnnouncementLock("spectate");
return CR_OK;
}
DFhackCExport command_result plugin_shutdown (color_ostream &out) { void PrintStatus(color_ostream &out) {
delete pause_lock; out.print("Spectate is %s\n", enabled ? "ENABLED." : "DISABLED.");
return CR_OK; out.print(" FEATURES:\n");
} out.print(" %-20s\t%s\n", "focus-jobs: ", config.jobs_focus ? "on." : "off.");
out.print(" %-20s\t%s\n", "auto-unpause: ", config.unpause ? "on." : "off.");
DFhackCExport command_result plugin_load_data (color_ostream &out) { out.print(" %-20s\t%s\n", "auto-disengage: ", config.disengage ? "on." : "off.");
config = World::GetPersistentData(CONFIG_KEY); out.print(" SETTINGS:\n");
out.print(" %-20s\t%" PRIi32 "\n", "tick-threshold: ", config.tick_threshold);
if (!config.isValid()) {
config = World::AddPersistentData(CONFIG_KEY);
saveConfig();
} else {
unpause_enabled = config.ival(UNPAUSE);
disengage_enabled = config.ival(DISENGAGE);
focus_jobs_enabled = config.ival(JOB_FOCUS);
tick_threshold = config.ival(TICK_THRESHOLD);
pause_lock->unlock();
enable_auto_unpause(out, unpause_enabled);
}
return DFHack::CR_OK;
}
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) {
namespace EM = EventManager;
if (enable && !enabled) {
out.print("Spectate mode enabled!\n");
using namespace EM::EventType;
EM::EventHandler ticking(onTick, 15);
EM::EventHandler start(onJobStart, 0);
EM::EventHandler complete(onJobCompletion, 0);
EM::registerListener(EventType::TICK, ticking, plugin_self);
EM::registerListener(EventType::JOB_STARTED, start, plugin_self);
EM::registerListener(EventType::JOB_COMPLETED, complete, plugin_self);
enabled = true; // enable_auto_unpause won't do anything without this set now
enable_auto_unpause(out, unpause_enabled);
} else if (!enable && enabled) {
// warp 8, engage!
out.print("Spectate mode disabled!\n");
EM::unregisterAll(plugin_self);
// we need to retain whether auto-unpause is enabled, but we also need to disable its effect
bool temp = unpause_enabled;
enable_auto_unpause(out, false);
unpause_enabled = temp;
job_tracker.clear();
freq.clear();
}
enabled = enable;
return DFHack::CR_OK;
}
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) {
if (enabled) {
switch (event) {
case SC_MAP_UNLOADED:
case SC_BEGIN_UNLOAD:
case SC_WORLD_UNLOADED:
our_dorf = nullptr;
job_watched = nullptr;
following_dwarf = false;
default:
break;
}
}
return CR_OK;
}
DFhackCExport command_result plugin_onupdate(color_ostream &out) {
// keeps announcement pause settings locked
World::Update(); // from pause.h
if (lock_collision) {
if (unpause_enabled) {
// player asked for auto-unpause enabled
World::SaveAnnouncementSettings();
if (World::DisableAnnouncementPausing()) {
// now that we've got what we want, we can lock it down
lock_collision = false;
}
} else {
if (World::RestoreAnnouncementSettings()) {
lock_collision = false;
}
}
}
int failsafe = 0;
while (unpause_enabled && !world->status.popups.empty() && ++failsafe <= 10) {
// dismiss announcement popup(s)
Gui::getCurViewscreen(true)->feed_key(interface_key::CLOSE_MEGA_ANNOUNCEMENT);
if (World::ReadPauseState()) {
// WARNING: This has a possibility of conflicting with `reveal hell` - if Hermes himself runs `reveal hell` on precisely the right moment that is
World::SetPauseState(false);
}
}
if (failsafe >= 10) {
out.printerr("spectate encountered a problem dismissing a popup!\n");
}
if (disengage_enabled && !World::ReadPauseState()) {
if (our_dorf && our_dorf->id != df::global::ui->follow_unit) {
plugin_enable(out, false);
}
} }
return DFHack::CR_OK;
}
void enable_auto_unpause(color_ostream &out, bool state) { void SetUnpauseState(bool state) {
// we don't need to do any of this yet if the plugin isn't enabled // we don't need to do any of this yet if the plugin isn't enabled
if (enabled) { if (enabled) {
// todo: R.E. UNDEAD_ATTACK event [still pausing regardless of announcement settings] // todo: R.E. UNDEAD_ATTACK event [still pausing regardless of announcement settings]
// lock_collision == true means: enable_auto_unpause() was already invoked and didn't complete // lock_collision == true means: enable_auto_unpause() was already invoked and didn't complete
// The onupdate function above ensure the procedure properly completes, thus we only care about // The onupdate function above ensure the procedure properly completes, thus we only care about
// state reversal here ergo `enabled != state` // state reversal here ergo `enabled != state`
if (lock_collision && unpause_enabled != state) { if (lock_collision && config.unpause != state) {
out.print("handling collision\n"); WARN(plugin).print("Spectate auto-unpause: Not enabled yet, there was a lock collision. When the other lock holder releases, auto-unpause will engage on its own.\n");
// if unpaused_enabled is true, then a lock collision means: we couldn't save/disable the pause settings, // if unpaused_enabled is true, then a lock collision means: we couldn't save/disable the pause settings,
// therefore nothing to revert and the lock won't even be engaged (nothing to unlock) // therefore nothing to revert and the lock won't even be engaged (nothing to unlock)
lock_collision = false; lock_collision = false;
unpause_enabled = state; config.unpause = state;
if (unpause_enabled) { if (config.unpause) {
// a collision means we couldn't restore the pause settings, therefore we only need re-engage the lock // a collision means we couldn't restore the pause settings, therefore we only need re-engage the lock
pause_lock->lock(); pause_lock->lock();
} }
@ -220,7 +117,7 @@ void enable_auto_unpause(color_ostream &out, bool state) {
announcements_disabled = true; announcements_disabled = true;
pause_lock->lock(); pause_lock->lock();
} else { } else {
out.printerr("lock collision enabling auto-unpause\n"); WARN(plugin).print("Spectate auto-unpause: Could not fully enable. There was a lock collision, when the other lock holder releases, auto-unpause will engage on its own.\n");
lock_collision = true; lock_collision = true;
} }
} else { } else {
@ -228,7 +125,7 @@ void enable_auto_unpause(color_ostream &out, bool state) {
if (announcements_disabled) { if (announcements_disabled) {
if (!World::RestoreAnnouncementSettings()) { if (!World::RestoreAnnouncementSettings()) {
// this in theory shouldn't happen, if others use the lock like we do in spectate // this in theory shouldn't happen, if others use the lock like we do in spectate
out.printerr("lock collision disabling auto-unpause\n"); WARN(plugin).print("Spectate auto-unpause: Could not fully disable. There was a lock collision, when the other lock holder releases, auto-unpause will disengage on its own.\n");
lock_collision = true; lock_collision = true;
} else { } else {
announcements_disabled = false; announcements_disabled = false;
@ -236,58 +133,83 @@ void enable_auto_unpause(color_ostream &out, bool state) {
} }
} }
if (lock_collision) { if (lock_collision) {
out.printerr( ERR(plugin).print("Spectate auto-unpause: Could not fully enable. There was a lock collision, when the other lock holder releases, auto-unpause will engage on its own.\n");
"auto-unpause: must wait for another Pausing::AnnouncementLock to be lifted.\n" WARN(plugin).print(
"The action you were attempting will complete when the following lock or locks lift.\n"); " auto-unpause: must wait for another Pausing::AnnouncementLock to be lifted.\n"
pause_lock->reportLocks(out); " The action you were attempting will complete when the following lock or locks lift.\n");
pause_lock->reportLocks(Core::getInstance().getConsole());
} }
} }
unpause_enabled = state; config.unpause = state;
} }
command_result spectate (color_ostream &out, std::vector <std::string> & parameters) { void SaveSettings() {
if (!parameters.empty()) { if (pconfig.isValid()) {
if (parameters.size() >= 2 && parameters.size() <= 3) { pconfig.ival(UNPAUSE) = config.unpause;
bool state =false; pconfig.ival(DISENGAGE) = config.disengage;
bool set = false; pconfig.ival(JOB_FOCUS) = config.jobs_focus;
if (parameters[0] == "enable") { pconfig.ival(TICK_THRESHOLD) = config.tick_threshold;
state = true;
} else if (parameters[0] == "disable") {
state = false;
} else if (parameters[0] == "set") {
set = true;
} else {
return DFHack::CR_WRONG_USAGE;
} }
if(parameters[1] == "auto-unpause"){
enable_auto_unpause(out, state);
} else if (parameters[1] == "auto-disengage") {
disengage_enabled = state;
} else if (parameters[1] == "focus-jobs") {
focus_jobs_enabled = state;
} else if (parameters[1] == "tick-threshold" && set && parameters.size() == 3) {
try {
tick_threshold = std::abs(std::stol(parameters[2]));
} catch (const std::exception &e) {
out.printerr("%s\n", e.what());
} }
void LoadSettings() {
pconfig = World::GetPersistentData(CONFIG_KEY);
if (!pconfig.isValid()) {
pconfig = World::AddPersistentData(CONFIG_KEY);
SaveSettings();
} else { } else {
return DFHack::CR_WRONG_USAGE; config.unpause = pconfig.ival(UNPAUSE);
config.disengage = pconfig.ival(DISENGAGE);
config.jobs_focus = pconfig.ival(JOB_FOCUS);
config.tick_threshold = pconfig.ival(TICK_THRESHOLD);
pause_lock->unlock();
SetUnpauseState(config.unpause);
} }
} }
void Enable(color_ostream &out, bool enable) {
}
void onUpdate(color_ostream &out) {
// keeps announcement pause settings locked
World::Update(); // from pause.h
if (lock_collision) {
if (config.unpause) {
// player asked for auto-unpause enabled
World::SaveAnnouncementSettings();
if (World::DisableAnnouncementPausing()) {
// now that we've got what we want, we can lock it down
lock_collision = false;
}
} else { } else {
out.print("Spectate is %s\n", enabled ? "ENABLED." : "DISABLED."); if (World::RestoreAnnouncementSettings()) {
out.print("tick-threshold: %" PRIu64 "\n", tick_threshold); lock_collision = false;
out.print("focus-jobs: %s\n", focus_jobs_enabled ? "on." : "off."); }
out.print("auto-unpause: %s\n", unpause_enabled ? "on." : "off."); }
out.print("auto-disengage: %s\n", disengage_enabled ? "on." : "off."); }
int failsafe = 0;
while (config.unpause && !world->status.popups.empty() && ++failsafe <= 10) {
// dismiss announcement popup(s)
Gui::getCurViewscreen(true)->feed_key(interface_key::CLOSE_MEGA_ANNOUNCEMENT);
if (World::ReadPauseState()) {
// WARNING: This has a possibility of conflicting with `reveal hell` - if Hermes himself runs `reveal hell` on precisely the right moment that is
World::SetPauseState(false);
}
}
if (failsafe >= 10) {
out.printerr("spectate encountered a problem dismissing a popup!\n");
}
if (config.disengage && !World::ReadPauseState()) {
if (our_dorf && our_dorf->id != df::global::ui->follow_unit) {
plugin_enable(out, false);
}
}
} }
saveConfig();
return DFHack::CR_OK;
}
// every tick check whether to decide to follow a dwarf // every tick check whether to decide to follow a dwarf
void onTick(color_ostream& out, void* ptr) { void TickHandler(color_ostream& out, void* ptr) {
int32_t tick = df::global::world->frame_counter; int32_t tick = df::global::world->frame_counter;
if (our_dorf) { if (our_dorf) {
if (!Units::isAlive(our_dorf)) { if (!Units::isAlive(our_dorf)) {
@ -295,7 +217,7 @@ void onTick(color_ostream& out, void* ptr) {
df::global::ui->follow_unit = -1; df::global::ui->follow_unit = -1;
} }
} }
if (!following_dwarf || (focus_jobs_enabled && !job_watched) || (tick - timestamp) > (int32_t) tick_threshold) { if (!following_dwarf || (config.jobs_focus && !job_watched) || timestamp == -1 || (tick - timestamp) > config.tick_threshold) {
std::vector<df::unit*> dwarves; std::vector<df::unit*> dwarves;
for (auto unit: df::global::world->units.active) { for (auto unit: df::global::world->units.active) {
if (!Units::isCitizen(unit)) { if (!Units::isCitizen(unit)) {
@ -309,14 +231,18 @@ void onTick(color_ostream& out, void* ptr) {
df::global::ui->follow_unit = our_dorf->id; df::global::ui->follow_unit = our_dorf->id;
job_watched = our_dorf->job.current_job; job_watched = our_dorf->job.current_job;
following_dwarf = true; following_dwarf = true;
if (!job_watched) { if (config.jobs_focus && !job_watched) {
timestamp = tick; timestamp = tick;
} }
} }
} // todo: refactor event manager to respect tick listeners
namespace EM = EventManager;
EM::EventHandler ticking(TickHandler, config.tick_threshold);
EM::registerTick(ticking, config.tick_threshold, plugin_self);
}
// every new worked job needs to be considered // every new worked job needs to be considered
void onJobStart(color_ostream& out, void* job_ptr) { void JobStartEvent(color_ostream& out, void* job_ptr) {
// todo: detect mood jobs // todo: detect mood jobs
int32_t tick = df::global::world->frame_counter; int32_t tick = df::global::world->frame_counter;
auto job = (df::job*) job_ptr; auto job = (df::job*) job_ptr;
@ -324,7 +250,8 @@ void onJobStart(color_ostream& out, void* job_ptr) {
int zcount = ++freq[job->pos.z]; int zcount = ++freq[job->pos.z];
job_tracker.emplace(job->id); job_tracker.emplace(job->id);
// if we're not doing anything~ then let's pick something // if we're not doing anything~ then let's pick something
if ((focus_jobs_enabled && !job_watched) || (tick - timestamp) > (int32_t) tick_threshold) { if ((config.jobs_focus && !job_watched) || timestamp == -1 || (tick - timestamp) > config.tick_threshold) {
timestamp = tick;
following_dwarf = true; following_dwarf = true;
// todo: allow the user to configure b, and also revise the math // todo: allow the user to configure b, and also revise the math
const double b = base; const double b = base;
@ -349,10 +276,10 @@ void onJobStart(color_ostream& out, void* job_ptr) {
df::global::ui->follow_unit = nonworkers[follow_drunk(RNG)]->id; df::global::ui->follow_unit = nonworkers[follow_drunk(RNG)]->id;
} }
} }
} }
// every job completed can be forgotten about // every job completed can be forgotten about
void onJobCompletion(color_ostream &out, void* job_ptr) { void JobCompletedEvent(color_ostream &out, void* job_ptr) {
auto job = (df::job*) job_ptr; auto job = (df::job*) job_ptr;
// forget about it // forget about it
freq[job->pos.z]--; freq[job->pos.z]--;
@ -363,4 +290,112 @@ void onJobCompletion(color_ostream &out, void* job_ptr) {
if (job_watched && job_watched->id == job->id) { if (job_watched && job_watched->id == job->id) {
job_watched = nullptr; job_watched = nullptr;
} }
}
};
DFhackCExport command_result plugin_init (color_ostream &out, std::vector <PluginCommand> &commands) {
commands.push_back(PluginCommand("spectate",
"Automated spectator mode.",
spectate,
false));
pause_lock = new AnnouncementLock("spectate");
return CR_OK;
}
DFhackCExport command_result plugin_shutdown (color_ostream &out) {
delete pause_lock;
return CR_OK;
}
DFhackCExport command_result plugin_load_data (color_ostream &out) {
SP::LoadSettings();
SP::PrintStatus(out);
return DFHack::CR_OK;
}
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) {
namespace EM = EventManager;
if (enable && !enabled) {
out.print("Spectate mode enabled!\n");
using namespace EM::EventType;
EM::EventHandler ticking(SP::TickHandler, config.tick_threshold);
EM::EventHandler start(SP::JobStartEvent, 0);
EM::EventHandler complete(SP::JobCompletedEvent, 0);
//EM::registerListener(EventType::TICK, ticking, plugin_self);
EM::registerTick(ticking, config.tick_threshold, plugin_self);
EM::registerListener(EventType::JOB_STARTED, start, plugin_self);
EM::registerListener(EventType::JOB_COMPLETED, complete, plugin_self);
enabled = true; // enable_auto_unpause won't do anything without this set now
SP::SetUnpauseState(config.unpause);
} else if (!enable && enabled) {
// warp 8, engage!
out.print("Spectate mode disabled!\n");
EM::unregisterAll(plugin_self);
// we need to retain whether auto-unpause is enabled, but we also need to disable its effect
bool temp = config.unpause;
SP::SetUnpauseState(false);
config.unpause = temp;
job_tracker.clear();
freq.clear();
}
enabled = enable;
return DFHack::CR_OK;
}
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) {
if (enabled) {
switch (event) {
case SC_MAP_UNLOADED:
case SC_BEGIN_UNLOAD:
case SC_WORLD_UNLOADED:
our_dorf = nullptr;
job_watched = nullptr;
following_dwarf = false;
default:
break;
}
}
return CR_OK;
}
DFhackCExport command_result plugin_onupdate(color_ostream &out) {
SP::onUpdate(out);
return DFHack::CR_OK;
}
command_result spectate (color_ostream &out, std::vector <std::string> & parameters) {
if (!parameters.empty()) {
if (parameters.size() >= 2 && parameters.size() <= 3) {
bool state =false;
bool set = false;
if (parameters[0] == "enable") {
state = true;
} else if (parameters[0] == "disable") {
state = false;
} else if (parameters[0] == "set") {
set = true;
} else {
return DFHack::CR_WRONG_USAGE;
}
if(parameters[1] == "auto-unpause"){
SP::SetUnpauseState(state);
} else if (parameters[1] == "auto-disengage") {
config.disengage = state;
} else if (parameters[1] == "focus-jobs") {
config.jobs_focus = state;
} else if (parameters[1] == "tick-threshold" && set && parameters.size() == 3) {
try {
config.tick_threshold = std::abs(std::stol(parameters[2]));
} catch (const std::exception &e) {
out.printerr("%s\n", e.what());
}
} else {
return DFHack::CR_WRONG_USAGE;
}
}
} else {
SP::PrintStatus(out);
}
SP::SaveSettings();
return DFHack::CR_OK;
} }