Adds more features to spectate

develop
Josh Cooper 2022-09-03 11:18:46 -07:00
parent d90d0f86af
commit b6c97214ca
2 changed files with 134 additions and 77 deletions

@ -2,39 +2,47 @@ spectate
======== ========
.. dfhack-tool:: .. dfhack-tool::
:summary: Automatically follow exciting dwarves. :summary: Automatically follow productive dwarves.
:tags: fort interface :tags: fort interface
:no-command:
Usage Usage
----- -----
:: ::
spectate enable spectate
spectate <option> disable spectate
spectate <option> <value>
The plugin will automatically switch which dwarf is being followed periodically, When enabled, the plugin will automatically switch which dwarf is being
preferring dwarves on z-levels with the highest job activity. followed periodically, preferring dwarves on z-levels with the highest
job activity.
To set features that toggle between two states, use {0,1} to specify
which state the feature should be in. Anything else will take any positive
value.
Examples Examples
-------- --------
``spectate`` ``spectate``
See the status of spectate, what is enabled or disabled. The plugin reports its feature status.
``spectate enable``
Enable spectate plugin to pseudo-randomly follow dwarves around.
``spectate auto-unpause`` ``spectate auto-unpause 1``
Enable the spectate plugin to automatically dismiss pause events caused Enable the spectate plugin to automatically dismiss pause events caused
by the game. Siege events are one example of such a game event. by the game. Siege events are one example of such a game event.
``spectate tick-interval 50``
Set the tick interval the followed dwarf can be changed at back to its
default value.
Options Options
------- -------
:no option: Show plugin status. :no option: Show plugin status.
:enable: Enable plugin. :focus-jobs: Toggle whether the plugin should always be following a job.
:disable: Disable plugin. :auto-unpause: Toggle auto-dismissal of game pause events.
:auto-unpause: Toggle auto-dismissal of game pause events. :auto-disengage: Toggle auto-disengagement of plugin through player intervention.
:tick-interval: Set the plugin's tick interval for changing the followed dwarf.

@ -33,21 +33,21 @@ using namespace df::enums;
DFHACK_PLUGIN("spectate"); DFHACK_PLUGIN("spectate");
DFHACK_PLUGIN_IS_ENABLED(enabled); DFHACK_PLUGIN_IS_ENABLED(enabled);
bool dismiss_pause_events = false; Pausing::AnnouncementLock* pause_lock = nullptr;
bool lock_collision = false;
bool unpause_enabled = false;
bool disengage_enabled = false;
bool focus_jobs_enabled = false;
bool following_dwarf = false; bool following_dwarf = false;
df::unit* our_dorf = nullptr; df::unit* our_dorf = nullptr;
void* job_watched = nullptr; df::job* job_watched = nullptr;
int32_t timestamp = -1; int32_t timestamp = -1;
uint64_t tick_span = 50;
REQUIRE_GLOBAL(world); REQUIRE_GLOBAL(world);
REQUIRE_GLOBAL(ui); REQUIRE_GLOBAL(ui);
REQUIRE_GLOBAL(pause_state); REQUIRE_GLOBAL(pause_state);
REQUIRE_GLOBAL(d_init); REQUIRE_GLOBAL(d_init);
// todo: implement as user configurable variables
#define tick_span 50
#define base 0.99 #define base 0.99
Pausing::AnnouncementLock* pause_lock = nullptr;
bool lock_collision = false;
command_result spectate (color_ostream &out, std::vector <std::string> & parameters); command_result spectate (color_ostream &out, std::vector <std::string> & parameters);
@ -81,33 +81,50 @@ DFhackCExport command_result plugin_shutdown (color_ostream &out) {
return CR_OK; return CR_OK;
} }
DFhackCExport command_result plugin_onupdate(color_ostream &out) { void onTick(color_ostream& out, void* tick);
if (lock_collision) { void onJobStart(color_ostream &out, void* job);
if (dismiss_pause_events) { void onJobCompletion(color_ostream &out, void* job);
// player asked for auto-unpause enabled
World::SaveAnnouncementSettings(); void enable_auto_unpause(color_ostream &out, bool state){
if (World::DisableAnnouncementPausing()){ if(unpause_enabled != state && lock_collision) {
// now that we've got what we want, we can lock it down // when enabled, lock collision means announcements haven't been disabled
lock_collision = false; // when disabled, lock collision means announcement are still disabled
pause_lock->lock(); // the only state left to consider here is what the lock should be set to
} lock_collision = false;
unpause_enabled = state;
if (unpause_enabled) {
pause_lock->lock();
} else { } else {
if (World::RestoreAnnouncementSettings()) { // this one should be redundant, the lock should already be unlocked right now
lock_collision = false; pause_lock->unlock();
}
} }
out.print(unpause_enabled ? "auto-unpause: on\n" : "auto-unpause: off\n");
return;
} }
while (dismiss_pause_events && !world->status.popups.empty()) { unpause_enabled = state;
// dismiss announcement popup(s) // update the announcement settings if we can
Gui::getCurViewscreen(true)->feed_key(interface_key::CLOSE_MEGA_ANNOUNCEMENT); if (unpause_enabled) {
if (World::SaveAnnouncementSettings()) {
World::DisableAnnouncementPausing();
pause_lock->lock();
} else {
lock_collision = true;
}
} else {
pause_lock->unlock();
if (!World::RestoreAnnouncementSettings()) {
// this in theory shouldn't happen, if others use the lock like we do in spectate
lock_collision = true;
}
}
// report to the user how things went
if (!lock_collision){
out.print(unpause_enabled ? "auto-unpause: on\n" : "auto-unpause: off\n");
} else {
out.print("auto-unpause: must wait for another Pausing::AnnouncementLock to be lifted. This setting will complete when the lock lifts.\n");
} }
return DFHack::CR_OK;
} }
void onTick(color_ostream& out, void* tick);
void onJobStart(color_ostream &out, void* job);
void onJobCompletion(color_ostream &out, void* job);
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) {
namespace EM = EventManager; namespace EM = EventManager;
if (enable && !enabled) { if (enable && !enabled) {
@ -130,48 +147,78 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) {
return DFHack::CR_OK; return DFHack::CR_OK;
} }
DFhackCExport command_result plugin_onupdate(color_ostream &out) {
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;
pause_lock->lock();
}
} else {
if (World::RestoreAnnouncementSettings()) {
lock_collision = false;
}
}
}
while (unpause_enabled && !world->status.popups.empty()) {
// dismiss announcement popup(s)
Gui::getCurViewscreen(true)->feed_key(interface_key::CLOSE_MEGA_ANNOUNCEMENT);
}
if (disengage_enabled) {
if (our_dorf && our_dorf->id != df::global::ui->follow_unit){
plugin_enable(out, false);
}
}
return DFHack::CR_OK;
}
command_result spectate (color_ostream &out, std::vector <std::string> & parameters) { command_result spectate (color_ostream &out, std::vector <std::string> & parameters) {
if(!parameters.empty()) { if(!parameters.empty()) {
if (parameters[0] == "auto-unpause") { if (parameters.size() % 2 != 0) {
dismiss_pause_events = !dismiss_pause_events; return DFHack::CR_WRONG_USAGE;
}
// update the announcement settings if we can for (int i = 0; i+1 < parameters.size(); i += 2) {
if (dismiss_pause_events) { if (parameters[i] == "auto-unpause") {
if (World::SaveAnnouncementSettings()) { if (parameters[i+1] == "0") {
World::DisableAnnouncementPausing(); enable_auto_unpause(out, false);
pause_lock->lock(); } else if (parameters[i+1] == "1") {
enable_auto_unpause(out, true);
} else { } else {
lock_collision = true; return DFHack::CR_WRONG_USAGE;
} }
} else { } else if (parameters[i] == "auto-disengage") {
pause_lock->unlock(); if (parameters[i+1] == "0") {
if (!World::RestoreAnnouncementSettings()) { disengage_enabled = false;
// this in theory shouldn't happen, if others use the lock like we do in spectate } else if (parameters[i+1] == "1") {
lock_collision = true; disengage_enabled = true;
} else {
return DFHack::CR_WRONG_USAGE;
}
} else if (parameters[i] == "focus-jobs") {
if (parameters[i+1] == "0") {
focus_jobs_enabled = false;
} else if (parameters[i+1] == "1") {
focus_jobs_enabled = true;
} else {
return DFHack::CR_WRONG_USAGE;
}
} else if (parameters[i] == "tick-interval") {
try {
tick_span = std::stol(parameters[i + 1]);
} catch (const std::exception &e) {
out.printerr("%s\n", e.what());
} }
}
// report to the user how things went
if (!lock_collision){
out.print(dismiss_pause_events ? "auto-unpause: on\n" : "auto-unpause: off\n");
} else { } else {
out.print("auto-unpause: must wait for another Pausing::AnnouncementLock to be lifted. This setting will complete when the lock lifts.\n");
}
// probably a typo
if (parameters.size() == 2) {
out.print("If you want additional options open an issue on github, or mention it on discord.\n\n");
return DFHack::CR_WRONG_USAGE; return DFHack::CR_WRONG_USAGE;
} }
} else if (parameters[0] == "disengage") {
//todo: cannibalize follow
} else {
return DFHack::CR_WRONG_USAGE;
} }
} else { } else {
out.print(enabled ? "Spectate is enabled.\n" : "Spectate is disabled.\n"); out.print(enabled ? "Spectate is enabled.\n" : "Spectate is disabled.\n");
if(enabled) { if(enabled) {
out.print(dismiss_pause_events ? "auto-unpause: on.\n" : "auto-unpause: off.\n"); out.print(unpause_enabled ? "auto-unpause: on.\n" : "auto-unpause: off.\n");
} }
} }
return DFHack::CR_OK; return DFHack::CR_OK;
@ -186,7 +233,7 @@ void onTick(color_ostream& out, void* ptr) {
df::global::ui->follow_unit = -1; df::global::ui->follow_unit = -1;
} }
} }
if (!following_dwarf || (tick - timestamp) > tick_span || job_watched == nullptr) { if (!following_dwarf || (focus_jobs_enabled && !job_watched) || (tick - timestamp) > tick_span) {
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)) {
@ -196,6 +243,7 @@ void onTick(color_ostream& out, void* ptr) {
} }
std::uniform_int_distribution<uint64_t> follow_any(0, dwarves.size() - 1); std::uniform_int_distribution<uint64_t> follow_any(0, dwarves.size() - 1);
if (df::global::ui) { if (df::global::ui) {
// if you're looking at a warning about a local address escaping, it means the unit* from dwarves (which aren't local)
our_dorf = dwarves[follow_any(RNG)]; our_dorf = dwarves[follow_any(RNG)];
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;
@ -216,14 +264,14 @@ 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 (job_watched == nullptr || (tick - timestamp) > tick_span) { if ((focus_jobs_enabled && !job_watched) || (tick - timestamp) > tick_span) {
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;
double p = b * ((double) zcount / job_tracker.size()); double p = b * ((double) zcount / job_tracker.size());
std::bernoulli_distribution follow_job(p); std::bernoulli_distribution follow_job(p);
if (!job->flags.bits.special && follow_job(RNG)) { if (!job->flags.bits.special && follow_job(RNG)) {
job_watched = job_ptr; job_watched = job;
df::unit* unit = Job::getWorker(job); df::unit* unit = Job::getWorker(job);
if (df::global::ui && unit) { if (df::global::ui && unit) {
our_dorf = unit; our_dorf = unit;
@ -252,9 +300,10 @@ void onJobCompletion(color_ostream &out, void* job_ptr) {
// forget about it // forget about it
freq[job->pos.z]--; freq[job->pos.z]--;
freq[job->pos.z] = freq[job->pos.z] < 0 ? 0 : freq[job->pos.z]; freq[job->pos.z] = freq[job->pos.z] < 0 ? 0 : freq[job->pos.z];
job_tracker.erase(job->id);
// the job doesn't exist, so we definitely need to get rid of that // the job doesn't exist, so we definitely need to get rid of that
if (job_watched == job_ptr) { job_tracker.erase(job->id);
// the event manager clones jobs and returns those clones for completed jobs. So the pointers won't match without a refactor of EM passing clones to both events
if (job_watched->id == job->id) {
job_watched = nullptr; job_watched = nullptr;
} }
} }