From 40cbe4fe88c8430ab446d740613c73d255239295 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Wed, 9 Nov 2022 15:49:24 -0800 Subject: [PATCH] Implements plugin: spectate v1.0a --- docs/changelog.txt | 4 +- docs/plugins/spectate.rst | 4 +- plugins/spectate/spectate.cpp | 284 +++++++++++++++++++--------------- 3 files changed, 168 insertions(+), 124 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 206d64879..1546c6c14 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -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. - `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 ``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 ``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 - `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 diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index 7e287c8be..8bfff4f03 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -38,9 +38,11 @@ Examples 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-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 -------- diff --git a/plugins/spectate/spectate.cpp b/plugins/spectate/spectate.cpp index 2e056cf9c..e308ddd19 100644 --- a/plugins/spectate/spectate.cpp +++ b/plugins/spectate/spectate.cpp @@ -10,9 +10,10 @@ #include #include -#include -#include #include +#include +#include +#include #include #include #include @@ -21,11 +22,13 @@ #include #include #include +#include #include #include #include #include +#include // Debugging namespace DFHack { @@ -45,9 +48,12 @@ using namespace df::enums; struct Configuration { bool debug = false; - bool jobs_focus = false; + bool unpause = false; bool disengage = false; + bool animals = false; + bool hostiles = true; + bool visitors = false; int32_t tick_threshold = 1000; } config; @@ -55,40 +61,43 @@ Pausing::AnnouncementLock* pause_lock = nullptr; bool lock_collision = 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 job_tracker; -std::map freq; -std::default_random_engine RNG; - #define base 0.99 static const std::string CONFIG_KEY = std::string(plugin_name) + "/config"; enum ConfigData { UNPAUSE, DISENGAGE, - JOB_FOCUS, - TICK_THRESHOLD + TICK_THRESHOLD, + ANIMALS, + HOSTILES, + VISITORS + }; static PersistentDataItem pconfig; DFhackCExport command_result plugin_enable(color_ostream &out, bool enable); command_result spectate (color_ostream &out, std::vector & parameters); +#define COORDARGS(id) id.x, (id).y, id.z 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) { out.print("Spectate is %s\n", enabled ? "ENABLED." : "DISABLED."); 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-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(" %-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) { @@ -135,8 +144,8 @@ namespace SP { if (lock_collision) { 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"); WARN(plugin).print( - " auto-unpause: must wait for another Pausing::AnnouncementLock to be lifted.\n" - " 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" + " The action you were attempting will complete when the following lock or locks lift.\n"); pause_lock->reportLocks(Core::getInstance().getConsole()); } } @@ -147,8 +156,10 @@ namespace SP { if (pconfig.isValid()) { pconfig.ival(UNPAUSE) = config.unpause; pconfig.ival(DISENGAGE) = config.disengage; - pconfig.ival(JOB_FOCUS) = config.jobs_focus; 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 { config.unpause = pconfig.ival(UNPAUSE); config.disengage = pconfig.ival(DISENGAGE); - config.jobs_focus = pconfig.ival(JOB_FOCUS); 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(); 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 units; + static auto add_if = [&](std::function 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 i; + std::vector 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) { + if (!World::isFortressMode() || !Maps::IsValid()) + return; + // keeps announcement pause settings locked World::Update(); // from pause.h + + // Plugin Management if (lock_collision) { if (config.unpause) { // player asked for auto-unpause enabled @@ -201,95 +320,24 @@ namespace SP { 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); - } - } - } - // every tick check whether to decide to follow a dwarf - void TickHandler(color_ostream& out, void* ptr) { - int32_t tick = df::global::world->frame_counter; - if (our_dorf) { - if (!Units::isAlive(our_dorf)) { + // plugin logic + static int32_t last_tick = -1; + int32_t tick = world->frame_counter; + if (!World::ReadPauseState() && tick - last_tick >= 1) { + 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; - df::global::ui->follow_unit = -1; - } - } - if (!following_dwarf || (config.jobs_focus && !job_watched) || timestamp == -1 || (tick - timestamp) > config.tick_threshold) { - std::vector dwarves; - for (auto unit: df::global::world->units.active) { - if (!Units::isCitizen(unit)) { - continue; + if (!config.disengage) { + // try to + following_dwarf = FollowADwarf(); + } else if (!World::ReadPauseState()) { + plugin_enable(out, false); } - dwarves.push_back(unit); - } - std::uniform_int_distribution 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 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) { SP::LoadSettings(); + SP::following_dwarf = SP::FollowADwarf(); 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; @@ -348,9 +385,8 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan case SC_MAP_UNLOADED: case SC_BEGIN_UNLOAD: case SC_WORLD_UNLOADED: - our_dorf = nullptr; - job_watched = nullptr; - following_dwarf = false; + SP::our_dorf = nullptr; + SP::following_dwarf = false; default: break; } @@ -381,8 +417,12 @@ command_result spectate (color_ostream &out, std::vector & paramet 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] == "animals") { + 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) { try { config.tick_threshold = std::abs(std::stol(parameters[2]));