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 1ca3efeb8..f54d68142 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -38,11 +38,12 @@ 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 -------- -: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) +: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 5f720806c..f6a5ad8af 100644 --- a/plugins/spectate/spectate.cpp +++ b/plugins/spectate/spectate.cpp @@ -1,16 +1,19 @@ // // Created by josh on 7/28/21. +// Last updated: 11//10/22 // #include "pause.h" -#include "Core.h" -#include -#include +#include +#include #include #include -#include + #include +#include +#include +#include #include #include #include @@ -19,11 +22,17 @@ #include #include #include +#include -#include -#include +#include #include #include +#include + +// Debugging +namespace DFHack { + DBG_DECLARE(spectate, plugin, DebugCategory::LINFO); +} DFHACK_PLUGIN("spectate"); DFHACK_PLUGIN_IS_ENABLED(enabled); @@ -36,51 +45,336 @@ 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 unpause = false; + bool disengage = false; + bool animals = false; + bool hostiles = true; + bool visitors = 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; -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; - -void enable_auto_unpause(color_ostream &out, bool state); - - #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 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); +#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 DebugUnitVector(std::vector units) { + 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)); + } + } + } + + void PrintStatus(color_ostream &out) { + out.print("Spectate is %s\n", enabled ? "ENABLED." : "DISABLED."); + out.print(" FEATURES:\n"); + 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) { + // 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(TICK_THRESHOLD) = config.tick_threshold; + pconfig.ival(ANIMALS) = config.animals; + pconfig.ival(HOSTILES) = config.hostiles; + pconfig.ival(VISITORS) = config.visitors; + } + } + + 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.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); + } + } + + 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; + }; + 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 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); + } + }; + + /// RANGE 0 (in view + working) + // grab valid working units + add_if([&](df::unit* unit) { + return valid(unit) && + Units::isUnitInBox(unit, COORDARGS(viewMin), COORDARGS(viewMax)) && + Units::isCitizen(unit, true) && + unit->job.current_job; + }); + build_range(0); + + /// RANGE 1 (in view) + add_if([&](df::unit* unit) { + return valid(unit) && Units::isUnitInBox(unit, COORDARGS(viewMin), COORDARGS(viewMax)); + }); + build_range(1); + + /// 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); + }); + build_range(3); + + /// RANGE 4 (any valid) + add_if(valid); + 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; + 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 (!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); + 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 + 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"); + } + + // 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 || tick - timestamp >= config.tick_threshold) { + // we're not following anyone + following_dwarf = false; + if (!config.disengage) { + // try to + following_dwarf = FollowADwarf(); + } else if (!World::ReadPauseState()) { + plugin_enable(out, false); + } + } + } + } +}; DFhackCExport command_result plugin_init (color_ostream &out, std::vector &commands) { commands.push_back(PluginCommand("spectate", @@ -97,45 +391,24 @@ 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::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(onTick, 15); - EM::EventHandler start(onJobStart, 0); - EM::EventHandler complete(onJobCompletion, 0); - EM::registerListener(EventType::TICK, ticking, plugin_self); - EM::registerListener(EventType::JOB_STARTED, start, plugin_self); - EM::registerListener(EventType::JOB_COMPLETED, complete, plugin_self); enabled = true; // enable_auto_unpause won't do anything without this set now - enable_auto_unpause(out, unpause_enabled); + 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; - job_tracker.clear(); - freq.clear(); + bool temp = config.unpause; + SP::SetUnpauseState(false); + config.unpause = temp; } enabled = enable; return DFHack::CR_OK; @@ -147,9 +420,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; } @@ -158,93 +430,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 +449,18 @@ 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; - } else if (parameters[1] == "focus-jobs") { - focus_jobs_enabled = state; + config.disengage = 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 { - 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 +469,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; - } -}