From ec6cd8d53a7da8ca2c4035575d4356bd34429942 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Tue, 8 Nov 2022 11:42:12 -0800 Subject: [PATCH 1/5] 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 --- docs/plugins/spectate.rst | 2 +- plugins/spectate/spectate.cpp | 465 ++++++++++++++++++---------------- 2 files changed, 251 insertions(+), 216 deletions(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index 1ca3efeb8..7e287c8be 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -45,4 +45,4 @@ Features Settings -------- :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) diff --git a/plugins/spectate/spectate.cpp b/plugins/spectate/spectate.cpp index 5f720806c..2e056cf9c 100644 --- a/plugins/spectate/spectate.cpp +++ b/plugins/spectate/spectate.cpp @@ -4,11 +4,13 @@ #include "pause.h" -#include "Core.h" -#include +#include +#include #include #include #include + +#include #include #include #include @@ -25,6 +27,11 @@ #include #include +// Debugging +namespace DFHack { + DBG_DECLARE(spectate, plugin, DebugCategory::LINFO); +} + DFHACK_PLUGIN("spectate"); DFHACK_PLUGIN_IS_ENABLED(enabled); REQUIRE_GLOBAL(world); @@ -36,14 +43,14 @@ using namespace DFHack; using namespace Pausing; using namespace df::enums; -void onTick(color_ostream& out, void* tick); -void onJobStart(color_ostream &out, void* job); -void onJobCompletion(color_ostream &out, void* job); +struct Configuration { + bool debug = false; + 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; bool lock_collision = false; bool announcements_disabled = false; @@ -57,9 +64,6 @@ std::set job_tracker; std::map freq; std::default_random_engine RNG; -void enable_auto_unpause(color_ostream &out, bool state); - - #define base 0.99 static const std::string CONFIG_KEY = std::string(plugin_name) + "/config"; @@ -70,18 +74,225 @@ enum ConfigData { TICK_THRESHOLD }; -static PersistentDataItem config; -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; - } -} +static PersistentDataItem pconfig; +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable); command_result spectate (color_ostream &out, std::vector & parameters); +namespace SP { + + 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(" SETTINGS:\n"); + out.print(" %-20s\t%" PRIi32 "\n", "tick-threshold: ", config.tick_threshold); + } + + void SetUnpauseState(bool state) { + // we don't need to do any of this yet if the plugin isn't enabled + if (enabled) { + // 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 + // The onupdate function above ensure the procedure properly completes, thus we only care about + // state reversal here ergo `enabled != state` + if (lock_collision && config.unpause != state) { + 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, + // therefore nothing to revert and the lock won't even be engaged (nothing to unlock) + lock_collision = false; + config.unpause = state; + if (config.unpause) { + // a collision means we couldn't restore the pause settings, therefore we only need re-engage the lock + pause_lock->lock(); + } + return; + } + // update the announcement settings if we can + if (state) { + if (World::SaveAnnouncementSettings()) { + World::DisableAnnouncementPausing(); + announcements_disabled = true; + pause_lock->lock(); + } else { + 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; + } + } else { + pause_lock->unlock(); + if (announcements_disabled) { + if (!World::RestoreAnnouncementSettings()) { + // this in theory shouldn't happen, if others use the lock like we do in spectate + 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; + } else { + announcements_disabled = false; + } + } + } + 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"); + pause_lock->reportLocks(Core::getInstance().getConsole()); + } + } + config.unpause = state; + } + + void SaveSettings() { + 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; + } + } + + void LoadSettings() { + pconfig = World::GetPersistentData(CONFIG_KEY); + + if (!pconfig.isValid()) { + pconfig = World::AddPersistentData(CONFIG_KEY); + SaveSettings(); + } 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); + 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 { + if (World::RestoreAnnouncementSettings()) { + lock_collision = false; + } + } + } + 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); + } + } + } + + // 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)) { + 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; + } + 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; + } + } +}; + DFhackCExport command_result plugin_init (color_ostream &out, std::vector &commands) { commands.push_back(PluginCommand("spectate", "Automated spectator mode.", @@ -97,19 +308,8 @@ DFhackCExport command_result plugin_shutdown (color_ostream &out) { } DFhackCExport command_result plugin_load_data (color_ostream &out) { - config = World::GetPersistentData(CONFIG_KEY); - - 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); - } + SP::LoadSettings(); + SP::PrintStatus(out); return DFHack::CR_OK; } @@ -118,22 +318,23 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { 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::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 - enable_auto_unpause(out, unpause_enabled); + 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 = unpause_enabled; - enable_auto_unpause(out, false); - unpause_enabled = temp; + bool temp = config.unpause; + SP::SetUnpauseState(false); + config.unpause = temp; job_tracker.clear(); freq.clear(); } @@ -158,93 +359,10 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan } 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); - } - } + SP::onUpdate(out); return DFHack::CR_OK; } -void enable_auto_unpause(color_ostream &out, bool state) { - // we don't need to do any of this yet if the plugin isn't enabled - if (enabled) { - // 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 - // The onupdate function above ensure the procedure properly completes, thus we only care about - // state reversal here ergo `enabled != state` - if (lock_collision && unpause_enabled != state) { - out.print("handling collision\n"); - // 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) - lock_collision = false; - unpause_enabled = state; - if (unpause_enabled) { - // a collision means we couldn't restore the pause settings, therefore we only need re-engage the lock - pause_lock->lock(); - } - return; - } - // update the announcement settings if we can - if (state) { - if (World::SaveAnnouncementSettings()) { - World::DisableAnnouncementPausing(); - announcements_disabled = true; - pause_lock->lock(); - } else { - out.printerr("lock collision enabling auto-unpause\n"); - lock_collision = true; - } - } else { - pause_lock->unlock(); - if (announcements_disabled) { - if (!World::RestoreAnnouncementSettings()) { - // this in theory shouldn't happen, if others use the lock like we do in spectate - out.printerr("lock collision disabling auto-unpause\n"); - lock_collision = true; - } else { - announcements_disabled = false; - } - } - } - if (lock_collision) { - out.printerr( - "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(out); - } - } - unpause_enabled = state; -} - command_result spectate (color_ostream &out, std::vector & parameters) { if (!parameters.empty()) { if (parameters.size() >= 2 && parameters.size() <= 3) { @@ -260,14 +378,14 @@ command_result spectate (color_ostream &out, std::vector & paramet return DFHack::CR_WRONG_USAGE; } if(parameters[1] == "auto-unpause"){ - enable_auto_unpause(out, state); + SP::SetUnpauseState(state); } else if (parameters[1] == "auto-disengage") { - disengage_enabled = state; + config.disengage = state; } else if (parameters[1] == "focus-jobs") { - focus_jobs_enabled = state; + config.jobs_focus = state; } else if (parameters[1] == "tick-threshold" && set && parameters.size() == 3) { try { - tick_threshold = std::abs(std::stol(parameters[2])); + config.tick_threshold = std::abs(std::stol(parameters[2])); } catch (const std::exception &e) { out.printerr("%s\n", e.what()); } @@ -276,91 +394,8 @@ command_result spectate (color_ostream &out, std::vector & paramet } } } else { - out.print("Spectate is %s\n", enabled ? "ENABLED." : "DISABLED."); - out.print("tick-threshold: %" PRIu64 "\n", tick_threshold); - 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."); + SP::PrintStatus(out); } - saveConfig(); + SP::SaveSettings(); return DFHack::CR_OK; } - -// every tick check whether to decide to follow a dwarf -void onTick(color_ostream& out, void* ptr) { - int32_t tick = df::global::world->frame_counter; - if (our_dorf) { - if (!Units::isAlive(our_dorf)) { - following_dwarf = false; - df::global::ui->follow_unit = -1; - } - } - if (!following_dwarf || (focus_jobs_enabled && !job_watched) || (tick - timestamp) > (int32_t) tick_threshold) { - std::vector dwarves; - for (auto unit: df::global::world->units.active) { - if (!Units::isCitizen(unit)) { - continue; - } - 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 (!job_watched) { - timestamp = tick; - } - } -} - -// every new worked job needs to be considered -void onJobStart(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 ((focus_jobs_enabled && !job_watched) || (tick - timestamp) > (int32_t) tick_threshold) { - 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 onJobCompletion(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; - } -} From 40cbe4fe88c8430ab446d740613c73d255239295 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Wed, 9 Nov 2022 15:49:24 -0800 Subject: [PATCH 2/5] 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])); From b99e948b8aac74772f6963261d63e4cdafabf651 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Wed, 9 Nov 2022 21:10:18 -0800 Subject: [PATCH 3/5] Implements plugin: spectate v1.0.1a --- docs/plugins/spectate.rst | 3 +- plugins/spectate/spectate.cpp | 122 ++++++++++++++++++++-------------- 2 files changed, 74 insertions(+), 51 deletions(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index 8bfff4f03..f54d68142 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -46,5 +46,4 @@ Features Settings -------- -: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: 1000) +:tick-threshold: Set the plugin's tick interval for changing the followed dwarf. (default: 1000) diff --git a/plugins/spectate/spectate.cpp b/plugins/spectate/spectate.cpp index e308ddd19..9ad4a1f4e 100644 --- a/plugins/spectate/spectate.cpp +++ b/plugins/spectate/spectate.cpp @@ -26,6 +26,7 @@ #include #include +#include #include #include #include @@ -78,7 +79,7 @@ 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 +#define COORDARGS(id) id.x, id.y, id.z namespace SP { bool following_dwarf = false; @@ -209,68 +210,91 @@ namespace SP { } return true; }; + static auto calc_extra_weight = [](size_t idx, double r1, double r2) { + switch(idx) { + case 0: + return r2; + case 1: + return (r2-r1)/1.3; + case 2: + return (r2-r1)/2; + default: + return 0.0; + } + }; + /// Collecting our choice pool + /////////////////////////////// + std::array ranges{}; + std::array range_exists{}; + static auto build_range = [&](size_t idx){ + size_t first = idx * 2; + size_t second = idx * 2 + 1; + size_t previous = first - 1; + // first we get the end of the range + ranges[second] = units.size() - 1; + // then we calculate whether this indicates there is a range or not + if (first != 0) { + range_exists[idx] = ranges[second] > ranges[previous]; + } else { + range_exists[idx] = ranges[second] >= 0; + } + // lastly we set the start of the range + ranges[first] = ranges[previous] + (range_exists[idx] ? 1 : 0); + }; + + /// RANGE 0 (in view + working) + // grab valid working units + add_if([](df::unit* unit) { + return valid(unit) && Units::isCitizen(unit, true) && unit->job.current_job; + }); + // keep only those in the box + Units::getUnitsInBox(units, COORDARGS(viewMin), COORDARGS(viewMax)); + build_range(0); + /// 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; + build_range(1); - /// RANGE 2 (citizens) + /// RANGE 2 (working citizens) + add_if([](df::unit* unit) { + return valid(unit) && Units::isCitizen(unit, true) && unit->job.current_job; + }); + build_range(2); + + /// RANGE 3 (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; + build_range(3); - /// RANGE 3 (any valid) + /// RANGE 4 (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; - + build_range(4); + // selecting from our choice pool if (!units.empty()) { + std::array bw{23,17,13,7,1}; // probability weights for each range 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); + bool at_least_one = false; + // in one word, elegance + for(size_t idx = 0; idx < range_exists.size(); ++idx) { + if (range_exists[idx]) { + at_least_one = true; + const auto &r1 = ranges[idx*2]; + const auto &r2 = ranges[idx*2+1]; + double extra = calc_extra_weight(idx, r1, r2); + i.push_back(r1); + w.push_back(bw[idx] + extra); + if (r1 != r2) { + i.push_back(r2); + w.push_back(bw[idx] + extra); + } } } - 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); - } + if (!at_least_one) { + return false; } 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) @@ -327,7 +351,7 @@ namespace SP { if (!World::ReadPauseState() && tick - last_tick >= 1) { last_tick = tick; // validate follow state - if (!following_dwarf || !our_dorf || df::global::ui->follow_unit < 0) { + if (!following_dwarf || !our_dorf || df::global::ui->follow_unit < 0 || tick - timestamp >= config.tick_threshold) { // we're not following anyone following_dwarf = false; if (!config.disengage) { From 4a0abd19151ffa52885c19f7b41b54c732d6bb3c Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Thu, 17 Nov 2022 11:17:13 -0800 Subject: [PATCH 4/5] Implements plugin: spectate v1.0.2b --- plugins/spectate/spectate.cpp | 41 ++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/plugins/spectate/spectate.cpp b/plugins/spectate/spectate.cpp index 9ad4a1f4e..411316775 100644 --- a/plugins/spectate/spectate.cpp +++ b/plugins/spectate/spectate.cpp @@ -1,12 +1,12 @@ // // Created by josh on 7/28/21. +// Last updated: 11//10/22 // #include "pause.h" #include #include -#include #include #include @@ -24,8 +24,6 @@ #include #include -#include -#include #include #include #include @@ -87,6 +85,16 @@ namespace SP { int32_t timestamp = -1; std::default_random_engine RNG; + void DebugUnitVector(std::vector units) { + for (auto unit : units) { + INFO(plugin).print("[id: %d]\n animal: %d\n hostile: %d\n visiting: %d\n", + unit->id, + Units::isAnimal(unit), + Units::isDanger(unit), + Units::isVisiting(unit)); + } + } + void PrintStatus(color_ostream &out) { out.print("Spectate is %s\n", enabled ? "ENABLED." : "DISABLED."); out.print(" FEATURES:\n"); @@ -232,28 +240,30 @@ namespace SP { size_t previous = first - 1; // first we get the end of the range ranges[second] = units.size() - 1; - // then we calculate whether this indicates there is a range or not - if (first != 0) { - range_exists[idx] = ranges[second] > ranges[previous]; - } else { + // then we calculate whether the range exists, and set the first index appropriately + if (idx == 0) { range_exists[idx] = ranges[second] >= 0; + ranges[first] = 0; + } else { + range_exists[idx] = ranges[second] > ranges[previous]; + ranges[first] = ranges[previous] + (range_exists[idx] ? 1 : 0); } - // lastly we set the start of the range - ranges[first] = ranges[previous] + (range_exists[idx] ? 1 : 0); }; /// RANGE 0 (in view + working) // grab valid working units - add_if([](df::unit* unit) { - return valid(unit) && Units::isCitizen(unit, true) && unit->job.current_job; + add_if([&](df::unit* unit) { + return valid(unit) && + Units::isUnitInBox(unit, COORDARGS(viewMin), COORDARGS(viewMax)) && + Units::isCitizen(unit, true) && + unit->job.current_job; }); - // keep only those in the box - Units::getUnitsInBox(units, COORDARGS(viewMin), COORDARGS(viewMax)); build_range(0); /// RANGE 1 (in view) - add_if(valid); - Units::getUnitsInBox(units, COORDARGS(viewMin), COORDARGS(viewMax)); + add_if([&](df::unit* unit) { + return valid(unit) && Units::isUnitInBox(unit, COORDARGS(viewMin), COORDARGS(viewMax)); + }); build_range(1); /// RANGE 2 (working citizens) @@ -296,6 +306,7 @@ namespace SP { if (!at_least_one) { return false; } + //DebugUnitVector(units); 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); From 5352649b884b42538f3b290c762884f982a43b4c Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Mon, 21 Nov 2022 12:13:11 -0800 Subject: [PATCH 5/5] Implements plugin: spectate v1.0.3b test --- plugins/spectate/spectate.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/plugins/spectate/spectate.cpp b/plugins/spectate/spectate.cpp index 411316775..f6a5ad8af 100644 --- a/plugins/spectate/spectate.cpp +++ b/plugins/spectate/spectate.cpp @@ -46,8 +46,6 @@ using namespace Pausing; using namespace df::enums; struct Configuration { - bool debug = false; - bool unpause = false; bool disengage = false; bool animals = false; @@ -86,12 +84,14 @@ namespace SP { std::default_random_engine RNG; void DebugUnitVector(std::vector units) { - for (auto unit : units) { - INFO(plugin).print("[id: %d]\n animal: %d\n hostile: %d\n visiting: %d\n", - unit->id, - Units::isAnimal(unit), - Units::isDanger(unit), - Units::isVisiting(unit)); + if (debug_plugin.isEnabled(DFHack::DebugCategory::LDEBUG)) { + for (auto unit: units) { + DEBUG(plugin).print("[id: %d]\n animal: %d\n hostile: %d\n visiting: %d\n", + unit->id, + Units::isAnimal(unit), + Units::isDanger(unit), + Units::isVisiting(unit)); + } } } @@ -306,7 +306,7 @@ namespace SP { if (!at_least_one) { return false; } - //DebugUnitVector(units); + DebugUnitVector(units); 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);