From 09af894acf437188f586a550c33539e1e38d449c Mon Sep 17 00:00:00 2001 From: KeT <42127943+ketpain@users.noreply.github.com> Date: Fri, 17 Apr 2026 13:03:02 -0400 Subject: [PATCH 1/2] spectate continues after unit disappears from map --- plugins/spectate.cpp | 113 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 89 insertions(+), 24 deletions(-) diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp index 072ad813c7..01fc6cccca 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate.cpp @@ -39,6 +39,7 @@ namespace DFHack { static std::default_random_engine rng; static uint32_t next_cycle_unpaused_ms = 0; // threshold for the next cycle +static bool pending_follow_recovery = false; static const size_t MAX_HISTORY = 200; @@ -166,10 +167,52 @@ static class AnnouncementSettings { static void follow_a_dwarf(color_ostream &out); +static bool is_followable_unit(df::unit *unit) { + return unit && + !Units::isDead(unit) && + Units::isActive(unit) && + !unit->flags1.bits.caged && + !unit->flags1.bits.chained && + !Units::isHidden(unit); +} + +static bool has_valid_follow_target() { + return is_followable_unit(df::unit::find(plotinfo->follow_unit)); +} + +static void focus_on_unit(df::unit *unit) { + Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); + plotinfo->follow_item = -1; + plotinfo->follow_unit = unit->id; +} + static class UnitHistory { std::deque history; size_t offset = 0; + bool follow_history_unit(color_ostream &out, size_t new_offset, const char *direction) { + int32_t unit_id = history[history.size() - (1 + new_offset)]; + auto unit = df::unit::find(unit_id); + if (!is_followable_unit(unit)) { + DEBUG(cycle,out).print("skipping {} history unit {} at offset {}; no longer followable\n", + direction, unit_id, new_offset); + return false; + } + + offset = new_offset; + DEBUG(cycle,out).print("scanning {} to unit {} at offset {}\n", direction, unit_id, offset); + focus_on_unit(unit); + return true; + } + + void recover_if_target_lost(color_ostream &out) { + if (has_valid_follow_target()) + return; + + DEBUG(cycle,out).print("current history target is gone; following another unit\n"); + follow_a_dwarf(out); + } + public: void reset() { history.clear(); @@ -196,31 +239,35 @@ static class UnitHistory { void add_and_follow(color_ostream &out, df::unit *unit) { // if we're currently following a unit, add it to the history if it's not already there - if (plotinfo->follow_unit > -1 && plotinfo->follow_unit != get_cur_unit_id()) { + auto followed_unit = df::unit::find(plotinfo->follow_unit); + if (plotinfo->follow_unit > -1 && plotinfo->follow_unit != get_cur_unit_id() && is_followable_unit(followed_unit)) { DEBUG(cycle,out).print("currently following unit {} that is not in history; adding\n", plotinfo->follow_unit); add_to_history(out, plotinfo->follow_unit); + } else if (plotinfo->follow_unit > -1 && plotinfo->follow_unit != get_cur_unit_id()) { + DEBUG(cycle,out).print("currently followed unit {} is no longer followable; not adding to history\n", + plotinfo->follow_unit); } int32_t id = unit->id; add_to_history(out, id); DEBUG(cycle,out).print("now following unit {}: {}\n", id, DF2CONSOLE(Units::getReadableName(unit))); - Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); - plotinfo->follow_item = -1; - plotinfo->follow_unit = id; + focus_on_unit(unit); } void scan_back(color_ostream &out) { - if (history.empty() || offset >= history.size()-1) { + if (history.empty() || offset >= history.size() - 1) { DEBUG(cycle,out).print("already at beginning of history\n"); + recover_if_target_lost(out); return; } - ++offset; - int unit_id = get_cur_unit_id(); - DEBUG(cycle,out).print("scanning back to unit {} at offset {}\n", unit_id, offset); - if (auto unit = df::unit::find(unit_id)) - Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); - plotinfo->follow_item = -1; - plotinfo->follow_unit = unit_id; + + for (size_t new_offset = offset + 1; new_offset < history.size(); ++new_offset) { + if (follow_history_unit(out, new_offset, "back")) + return; + } + + DEBUG(cycle,out).print("no earlier valid history units found\n"); + recover_if_target_lost(out); } void scan_forward(color_ostream &out) { @@ -230,13 +277,14 @@ static class UnitHistory { return; } - --offset; - int unit_id = get_cur_unit_id(); - DEBUG(cycle,out).print("scanning forward to unit {} at offset {}\n", unit_id, offset); - if (auto unit = df::unit::find(unit_id)) - Gui::revealInDwarfmodeMap(Units::getPosition(unit), false, World::ReadPauseState()); - plotinfo->follow_item = -1; - plotinfo->follow_unit = unit_id; + for (size_t new_offset = offset; new_offset > 0;) { + --new_offset; + if (follow_history_unit(out, new_offset, "forward")) + return; + } + + DEBUG(cycle,out).print("no more recent valid history units found; following new unit\n"); + follow_a_dwarf(out); } int32_t get_cur_unit_id() { @@ -307,6 +355,7 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector follow_unit > -1) + if (has_valid_follow_target()) set_next_cycle_unpaused_ms(out); else follow_a_dwarf(out); @@ -375,6 +425,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan switch (event) { case SC_WORLD_LOADED: next_cycle_unpaused_ms = 0; + pending_follow_recovery = false; break; case SC_WORLD_UNLOADED: if (is_enabled) { @@ -395,7 +446,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan DFhackCExport command_result plugin_onupdate(color_ostream &out) { announcement_settings.on_update(out); - if (plotinfo->follow_unit < 0 || plotinfo->follow_item > -1 || is_squads_open()) { + if (plotinfo->follow_item > -1 || is_squads_open()) { DEBUG(cycle,out).print("auto-disengage triggered\n"); is_enabled = false; plotinfo->follow_unit = -1; @@ -403,6 +454,19 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { return CR_OK; } + if (!has_valid_follow_target()) { + uint32_t unpaused_ms = Core::getInstance().getUnpausedMs(); + if (!pending_follow_recovery || unpaused_ms >= next_cycle_unpaused_ms) { + DEBUG(cycle,out).print("current follow target is no longer available; finding another unit\n"); + recent_units.trim(); + follow_a_dwarf(out); + pending_follow_recovery = !has_valid_follow_target(); + } + return CR_OK; + } + + pending_follow_recovery = false; + if (Core::getInstance().getUnpausedMs() >= next_cycle_unpaused_ms) { recent_units.trim(); follow_a_dwarf(out); @@ -444,7 +508,7 @@ static void get_dwarf_buckets(color_ostream &out, vector &other_units) { for (auto unit : world->units.active) { - if (Units::isDead(unit) || !Units::isActive(unit) || unit->flags1.bits.caged || unit->flags1.bits.chained || Units::isHidden(unit)) + if (!is_followable_unit(unit)) continue; if (!config.include_animals && Units::isAnimal(unit)) continue; @@ -611,8 +675,9 @@ static void spectate_followNext(color_ostream &out) { static void spectate_addToHistory(color_ostream &out, int32_t unit_id) { DEBUG(control,out).print("entering spectate_addToHistory; unit_id={}\n", unit_id); - if (!df::unit::find(unit_id)) { - WARN(control,out).print("unit with id {} not found; not adding to history\n", unit_id); + auto unit = df::unit::find(unit_id); + if (!is_followable_unit(unit)) { + WARN(control,out).print("unit with id {} is not followable; not adding to history\n", unit_id); return; } unit_history.add_to_history(out, unit_id); From cc8c79a559157f708e714ccf86338a472d8c74f4 Mon Sep 17 00:00:00 2001 From: KeT <42127943+ketpain@users.noreply.github.com> Date: Fri, 17 Apr 2026 13:06:59 -0400 Subject: [PATCH 2/2] Updated changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index cfff16e5a4..681a296ed4 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -61,6 +61,7 @@ Template for new versions: ## Fixes ## Misc Improvements +- Spectate will now continue cycling units if a unit leaves the fortress map. ## Documentation