Implements plugin: spectate v1.0a

develop
Josh Cooper 2022-11-09 15:49:24 -08:00
parent ec6cd8d53a
commit 40cbe4fe88
3 changed files with 168 additions and 124 deletions

@ -68,8 +68,10 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
- `orders`: replace shell craft orders with orders for shell leggings. they have a slightly higher trade price, and "shleggings" is hilarious. - `orders`: replace shell craft orders with orders for shell leggings. they have a slightly higher trade price, and "shleggings" is hilarious.
- `spectate`: new ``auto-unpause`` option for auto-dismissal of announcement pause events (e.g. sieges). - `spectate`: new ``auto-unpause`` option for auto-dismissal of announcement pause events (e.g. sieges).
- `spectate`: new ``auto-disengage`` option for auto-disengagement of plugin through player interaction whilst unpaused. - `spectate`: new ``auto-disengage`` option for auto-disengagement of plugin through player interaction whilst unpaused.
- `spectate`: new ``focus-jobs`` option for following a dwarf after their job has finished (when disabled).
- `spectate`: new ``tick-threshold``, option for specifying the change interval (maximum follow time when focus-jobs is enabled) - `spectate`: new ``tick-threshold``, option for specifying the change interval (maximum follow time when focus-jobs is enabled)
- `spectate`: new ``animals``, option for sometimes following animals
- `spectate`: new ``hostiles``, option for sometimes following hostiles
- `spectate`: new ``visiting``, option for sometimes following visiting merchants, diplomats or plain visitors
- `spectate`: added persistent configuration of the plugin settings - `spectate`: added persistent configuration of the plugin settings
- `gui/cp437-table`: new global keybinding for the clickable on-screen keyboard for players with keyboard layouts that prevent them from using certain keys: Ctrl-Shift-K - `gui/cp437-table`: new global keybinding for the clickable on-screen keyboard for players with keyboard layouts that prevent them from using certain keys: Ctrl-Shift-K
- `quickfort-library-guide`: dreamfort blueprint improvements: added a quantum stockpile for training bolts - `quickfort-library-guide`: dreamfort blueprint improvements: added a quantum stockpile for training bolts

@ -38,9 +38,11 @@ Examples
Features Features
-------- --------
:focus-jobs: Toggle whether the plugin should always be following a job. (default: disabled)
:auto-unpause: Toggle auto-dismissal of game pause events. (default: disabled) :auto-unpause: Toggle auto-dismissal of game pause events. (default: disabled)
:auto-disengage: Toggle auto-disengagement of plugin through player intervention while unpaused. (default: disabled) :auto-disengage: Toggle auto-disengagement of plugin through player intervention while unpaused. (default: disabled)
:animals: Toggle whether to sometimes follow animals. (default: disabled)
:hostiles: Toggle whether to sometimes follow hostiles (eg. undead, titan, invader, etc.) (default: disabled)
:visiting: Toggle whether to sometimes follow visiting units (eg. diplomat)
Settings Settings
-------- --------

@ -10,9 +10,10 @@
#include <Export.h> #include <Export.h>
#include <PluginManager.h> #include <PluginManager.h>
#include <modules/Gui.h>
#include <modules/World.h>
#include <modules/EventManager.h> #include <modules/EventManager.h>
#include <modules/World.h>
#include <modules/Maps.h>
#include <modules/Gui.h>
#include <modules/Job.h> #include <modules/Job.h>
#include <modules/Units.h> #include <modules/Units.h>
#include <df/job.h> #include <df/job.h>
@ -21,11 +22,13 @@
#include <df/global_objects.h> #include <df/global_objects.h>
#include <df/world.h> #include <df/world.h>
#include <df/viewscreen.h> #include <df/viewscreen.h>
#include <df/creature_raw.h>
#include <map> #include <map>
#include <set> #include <set>
#include <random> #include <random>
#include <cinttypes> #include <cinttypes>
#include <functional>
// Debugging // Debugging
namespace DFHack { namespace DFHack {
@ -45,9 +48,12 @@ using namespace df::enums;
struct Configuration { struct Configuration {
bool debug = false; bool debug = false;
bool jobs_focus = false;
bool unpause = false; bool unpause = false;
bool disengage = false; bool disengage = false;
bool animals = false;
bool hostiles = true;
bool visitors = false;
int32_t tick_threshold = 1000; int32_t tick_threshold = 1000;
} config; } config;
@ -55,40 +61,43 @@ Pausing::AnnouncementLock* pause_lock = nullptr;
bool lock_collision = false; bool lock_collision = false;
bool announcements_disabled = false; bool announcements_disabled = false;
bool following_dwarf = false;
df::unit* our_dorf = nullptr;
df::job* job_watched = nullptr;
int32_t timestamp = -1;
std::set<int32_t> job_tracker;
std::map<uint16_t,int16_t> freq;
std::default_random_engine RNG;
#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";
enum ConfigData { enum ConfigData {
UNPAUSE, UNPAUSE,
DISENGAGE, DISENGAGE,
JOB_FOCUS, TICK_THRESHOLD,
TICK_THRESHOLD ANIMALS,
HOSTILES,
VISITORS
}; };
static PersistentDataItem pconfig; static PersistentDataItem pconfig;
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable); 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);
#define COORDARGS(id) id.x, (id).y, id.z
namespace SP { namespace SP {
bool following_dwarf = false;
df::unit* our_dorf = nullptr;
int32_t timestamp = -1;
std::default_random_engine RNG;
void PrintStatus(color_ostream &out) { void PrintStatus(color_ostream &out) {
out.print("Spectate is %s\n", enabled ? "ENABLED." : "DISABLED."); out.print("Spectate is %s\n", enabled ? "ENABLED." : "DISABLED.");
out.print(" FEATURES:\n"); 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."); out.print(" %-20s\t%s\n", "auto-unpause: ", config.unpause ? "on." : "off.");
out.print(" %-20s\t%s\n", "auto-disengage: ", config.disengage ? "on." : "off."); out.print(" %-20s\t%s\n", "auto-disengage: ", config.disengage ? "on." : "off.");
out.print(" %-20s\t%s\n", "animals: ", config.animals ? "on." : "off.");
out.print(" %-20s\t%s\n", "hostiles: ", config.hostiles ? "on." : "off.");
out.print(" %-20s\t%s\n", "visiting: ", config.visitors ? "on." : "off.");
out.print(" SETTINGS:\n"); out.print(" SETTINGS:\n");
out.print(" %-20s\t%" PRIi32 "\n", "tick-threshold: ", config.tick_threshold); out.print(" %-20s\t%" PRIi32 "\n", "tick-threshold: ", config.tick_threshold);
if (following_dwarf)
out.print(" %-21s\t%s[id: %d]\n","FOLLOWING:", our_dorf ? our_dorf->name.first_name.c_str() : "nullptr", df::global::ui->follow_unit);
} }
void SetUnpauseState(bool state) { void SetUnpauseState(bool state) {
@ -147,8 +156,10 @@ namespace SP {
if (pconfig.isValid()) { if (pconfig.isValid()) {
pconfig.ival(UNPAUSE) = config.unpause; pconfig.ival(UNPAUSE) = config.unpause;
pconfig.ival(DISENGAGE) = config.disengage; pconfig.ival(DISENGAGE) = config.disengage;
pconfig.ival(JOB_FOCUS) = config.jobs_focus;
pconfig.ival(TICK_THRESHOLD) = config.tick_threshold; pconfig.ival(TICK_THRESHOLD) = config.tick_threshold;
pconfig.ival(ANIMALS) = config.animals;
pconfig.ival(HOSTILES) = config.hostiles;
pconfig.ival(VISITORS) = config.visitors;
} }
} }
@ -161,20 +172,128 @@ namespace SP {
} else { } else {
config.unpause = pconfig.ival(UNPAUSE); config.unpause = pconfig.ival(UNPAUSE);
config.disengage = pconfig.ival(DISENGAGE); config.disengage = pconfig.ival(DISENGAGE);
config.jobs_focus = pconfig.ival(JOB_FOCUS);
config.tick_threshold = pconfig.ival(TICK_THRESHOLD); config.tick_threshold = pconfig.ival(TICK_THRESHOLD);
config.animals = pconfig.ival(ANIMALS);
config.hostiles = pconfig.ival(HOSTILES);
config.visitors = pconfig.ival(VISITORS);
pause_lock->unlock(); pause_lock->unlock();
SetUnpauseState(config.unpause); SetUnpauseState(config.unpause);
} }
} }
void Enable(color_ostream &out, bool enable) { bool FollowADwarf() {
if (enabled && !World::ReadPauseState()) {
df::coord viewMin = Gui::getViewportPos();
df::coord viewMax{viewMin};
const auto &dims = Gui::getDwarfmodeViewDims().map().second;
viewMax.x += dims.x - 1;
viewMax.y += dims.y - 1;
viewMax.z = viewMin.z;
std::vector<df::unit*> units;
static auto add_if = [&](std::function<bool(df::unit*)> check) {
for (auto unit : world->units.active) {
if (check(unit)) {
units.push_back(unit);
}
}
};
static auto valid = [](df::unit* unit) {
if (Units::isAnimal(unit)) {
return config.animals;
}
if (Units::isVisiting(unit)) {
return config.visitors;
}
if (Units::isDanger(unit)) {
return config.hostiles;
}
return true;
};
/// RANGE 1 (in view)
// grab all valid units
add_if(valid);
// keep only those in the box
Units::getUnitsInBox(units, COORDARGS(viewMin), COORDARGS(viewMax));
int32_t inview_idx2 = units.size()-1;
bool range1_exists = inview_idx2 >= 0;
int32_t inview_idx1 = range1_exists ? 0 : -1;
/// RANGE 2 (citizens)
add_if([](df::unit* unit) {
return valid(unit) && Units::isCitizen(unit, true);
});
int32_t cit_idx2 = units.size()-1;
bool range2_exists = cit_idx2 > inview_idx2;
int32_t cit_idx1 = range2_exists ? inview_idx2+1 : cit_idx2;
/// RANGE 3 (any valid)
add_if(valid);
int32_t all_idx2 = units.size()-1;
bool range3_exists = all_idx2 > cit_idx2;
int32_t all_idx1 = range3_exists ? cit_idx2+1 : all_idx2;
if (!units.empty()) {
std::vector<double> i;
std::vector<double> w;
if (!range1_exists && !range2_exists && !range3_exists) {
return false;
}
if (range1_exists) {
if (inview_idx1 == inview_idx2) {
i.push_back(0);
w.push_back(17);
} else {
i.push_back(inview_idx1);
i.push_back(inview_idx2);
w.push_back(inview_idx2 + 1);
w.push_back(inview_idx2 + 1);
}
}
if (range2_exists) {
if (cit_idx1 == cit_idx2) {
i.push_back(cit_idx1);
w.push_back(7);
} else {
i.push_back(cit_idx1);
i.push_back(cit_idx2);
w.push_back(7);
w.push_back(7);
}
}
if (range3_exists) {
if (all_idx1 == all_idx2) {
i.push_back(all_idx1);
w.push_back(1);
} else {
i.push_back(all_idx1);
i.push_back(all_idx2);
w.push_back(1);
w.push_back(1);
}
}
std::piecewise_linear_distribution<> follow_any(i.begin(), i.end(), w.begin());
// if you're looking at a warning about a local address escaping, it means the unit* from units (which aren't local)
size_t idx = follow_any(RNG);
our_dorf = units[idx];
df::global::ui->follow_unit = our_dorf->id;
timestamp = df::global::world->frame_counter;
return true;
} else {
WARN(plugin).print("units vector is empty!\n");
}
}
return false;
} }
void onUpdate(color_ostream &out) { void onUpdate(color_ostream &out) {
if (!World::isFortressMode() || !Maps::IsValid())
return;
// keeps announcement pause settings locked // keeps announcement pause settings locked
World::Update(); // from pause.h World::Update(); // from pause.h
// Plugin Management
if (lock_collision) { if (lock_collision) {
if (config.unpause) { if (config.unpause) {
// player asked for auto-unpause enabled // player asked for auto-unpause enabled
@ -201,94 +320,23 @@ namespace SP {
if (failsafe >= 10) { if (failsafe >= 10) {
out.printerr("spectate encountered a problem dismissing a popup!\n"); 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);
}
}
}
// every tick check whether to decide to follow a dwarf // plugin logic
void TickHandler(color_ostream& out, void* ptr) { static int32_t last_tick = -1;
int32_t tick = df::global::world->frame_counter; int32_t tick = world->frame_counter;
if (our_dorf) { if (!World::ReadPauseState() && tick - last_tick >= 1) {
if (!Units::isAlive(our_dorf)) { last_tick = tick;
// validate follow state
if (!following_dwarf || !our_dorf || df::global::ui->follow_unit < 0) {
// we're not following anyone
following_dwarf = false; following_dwarf = false;
df::global::ui->follow_unit = -1; if (!config.disengage) {
} // try to
} following_dwarf = FollowADwarf();
if (!following_dwarf || (config.jobs_focus && !job_watched) || timestamp == -1 || (tick - timestamp) > config.tick_threshold) { } else if (!World::ReadPauseState()) {
std::vector<df::unit*> dwarves; plugin_enable(out, false);
for (auto unit: df::global::world->units.active) {
if (!Units::isCitizen(unit)) {
continue;
}
dwarves.push_back(unit);
}
std::uniform_int_distribution<uint64_t> follow_any(0, dwarves.size() - 1);
// 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)];
df::global::ui->follow_unit = our_dorf->id;
job_watched = our_dorf->job.current_job;
following_dwarf = true;
if (config.jobs_focus && !job_watched) {
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
void JobStartEvent(color_ostream& out, void* job_ptr) {
// todo: detect mood jobs
int32_t tick = df::global::world->frame_counter;
auto job = (df::job*) job_ptr;
// don't forget about it
int zcount = ++freq[job->pos.z];
job_tracker.emplace(job->id);
// if we're not doing anything~ then let's pick something
if ((config.jobs_focus && !job_watched) || timestamp == -1 || (tick - timestamp) > config.tick_threshold) {
timestamp = tick;
following_dwarf = true;
// todo: allow the user to configure b, and also revise the math
const double b = base;
double p = b * ((double) zcount / job_tracker.size());
std::bernoulli_distribution follow_job(p);
if (!job->flags.bits.special && follow_job(RNG)) {
job_watched = job;
if (df::unit* unit = Job::getWorker(job)) {
our_dorf = unit;
df::global::ui->follow_unit = unit->id;
}
} else {
timestamp = tick;
std::vector<df::unit*> nonworkers;
for (auto unit: df::global::world->units.active) {
if (!Units::isCitizen(unit) || unit->job.current_job) {
continue;
}
nonworkers.push_back(unit);
}
std::uniform_int_distribution<> follow_drunk(0, nonworkers.size() - 1);
df::global::ui->follow_unit = nonworkers[follow_drunk(RNG)]->id;
}
} }
} }
// every job completed can be forgotten about
void JobCompletedEvent(color_ostream &out, void* job_ptr) {
auto job = (df::job*) job_ptr;
// forget about it
freq[job->pos.z]--;
freq[job->pos.z] = freq[job->pos.z] < 0 ? 0 : freq[job->pos.z];
// the job doesn't exist, so we definitely need to get rid of that
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 && job_watched->id == job->id) {
job_watched = nullptr;
} }
} }
}; };
@ -309,34 +357,23 @@ DFhackCExport command_result plugin_shutdown (color_ostream &out) {
DFhackCExport command_result plugin_load_data (color_ostream &out) { DFhackCExport command_result plugin_load_data (color_ostream &out) {
SP::LoadSettings(); SP::LoadSettings();
SP::following_dwarf = SP::FollowADwarf();
SP::PrintStatus(out); SP::PrintStatus(out);
return DFHack::CR_OK; return DFHack::CR_OK;
} }
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) {
namespace EM = EventManager;
if (enable && !enabled) { if (enable && !enabled) {
out.print("Spectate mode enabled!\n"); 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 enabled = true; // enable_auto_unpause won't do anything without this set now
SP::SetUnpauseState(config.unpause); SP::SetUnpauseState(config.unpause);
} else if (!enable && enabled) { } else if (!enable && enabled) {
// warp 8, engage! // warp 8, engage!
out.print("Spectate mode disabled!\n"); 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 // we need to retain whether auto-unpause is enabled, but we also need to disable its effect
bool temp = config.unpause; bool temp = config.unpause;
SP::SetUnpauseState(false); SP::SetUnpauseState(false);
config.unpause = temp; config.unpause = temp;
job_tracker.clear();
freq.clear();
} }
enabled = enable; enabled = enable;
return DFHack::CR_OK; return DFHack::CR_OK;
@ -348,9 +385,8 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan
case SC_MAP_UNLOADED: case SC_MAP_UNLOADED:
case SC_BEGIN_UNLOAD: case SC_BEGIN_UNLOAD:
case SC_WORLD_UNLOADED: case SC_WORLD_UNLOADED:
our_dorf = nullptr; SP::our_dorf = nullptr;
job_watched = nullptr; SP::following_dwarf = false;
following_dwarf = false;
default: default:
break; break;
} }
@ -381,8 +417,12 @@ command_result spectate (color_ostream &out, std::vector <std::string> & paramet
SP::SetUnpauseState(state); SP::SetUnpauseState(state);
} else if (parameters[1] == "auto-disengage") { } else if (parameters[1] == "auto-disengage") {
config.disengage = state; config.disengage = state;
} else if (parameters[1] == "focus-jobs") { } else if (parameters[1] == "animals") {
config.jobs_focus = state; config.animals = state;
} else if (parameters[1] == "hostiles") {
config.hostiles = state;
} else if (parameters[1] == "visiting") {
config.visitors = state;
} else if (parameters[1] == "tick-threshold" && set && parameters.size() == 3) { } else if (parameters[1] == "tick-threshold" && set && parameters.size() == 3) {
try { try {
config.tick_threshold = std::abs(std::stol(parameters[2])); config.tick_threshold = std::abs(std::stol(parameters[2]));