diff --git a/IdleSpectator/AgentHarness.cs b/IdleSpectator/AgentHarness.cs index 3daf8ad..8a47769 100644 --- a/IdleSpectator/AgentHarness.cs +++ b/IdleSpectator/AgentHarness.cs @@ -453,12 +453,20 @@ public static class AgentHarness case "lore_favorites": { Chronicle.ShowHud(); - ChronicleHud.SetTab(ChronicleHud.LoreTab.Living); string v = (cmd.value ?? "true").Trim().ToLowerInvariant(); bool on = v != "false" && v != "0" && v != "off"; ChronicleHud.SetFavoritesOnly(on); + // Filter is a list view - leave detail if we were in one. + if (ChronicleHud.DetailUnitId != 0) + { + ChronicleHud.ClearUnitDetail(); + } + _cmdOk++; - Emit(cmd, ok: true, detail: $"favoritesOnly={ChronicleHud.FavoritesOnly}"); + Emit( + cmd, + ok: true, + detail: $"favoritesOnly={ChronicleHud.FavoritesOnly} tab={ChronicleHud.ActiveTab}"); break; } @@ -549,34 +557,44 @@ public static class AgentHarness long killerId = 0; Vector3 deathPos = Vector3.zero; string mode = (cmd.tier ?? "").Trim().ToLowerInvariant(); + bool favorite = mode.IndexOf("fav", System.StringComparison.Ordinal) >= 0; Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; - if (mode == "killer" && focus != null && focus.isAlive()) + if (mode.IndexOf("killer", System.StringComparison.Ordinal) >= 0 + && focus != null + && focus.isAlive()) { killerId = focus.getID(); deathPos = focus.current_position; } - else if (mode == "place") + else if (mode.IndexOf("place", System.StringComparison.Ordinal) >= 0 + || favorite + || string.IsNullOrEmpty(mode)) { - if (focus != null) + if (mode.IndexOf("place", System.StringComparison.Ordinal) >= 0 || favorite) { - deathPos = focus.current_position; - } - else if (MoveCamera.instance != null) - { - deathPos = MoveCamera.instance.transform.position; - deathPos.z = 0f; - } + if (focus != null) + { + deathPos = focus.current_position; + } + else if (MoveCamera.instance != null) + { + deathPos = MoveCamera.instance.transform.position; + deathPos.z = 0f; + } - if (deathPos == Vector3.zero) - { - deathPos = new Vector3(48f, 48f, 0f); + if (deathPos == Vector3.zero) + { + deathPos = new Vector3(48f, 48f, 0f); + } } } long id = Chronicle.ForceOrphanHistory( - name, species, n, prefix, attackType, killerId, deathPos); + name, species, n, prefix, attackType, killerId, deathPos, favorite); DeathManner manner = Chronicle.LatestDeathMannerFor(id); - bool ok = id != 0 && Chronicle.HistoryCountFor(id) >= n; + bool ok = id != 0 + && Chronicle.HistoryCountFor(id) >= n + && (!favorite || Chronicle.IsFavoriteSubject(id)); if (ok) { _cmdOk++; @@ -590,7 +608,7 @@ public static class AgentHarness cmd, ok, detail: - $"orphanId={id} hist={Chronicle.HistoryCountFor(id)} want={n} manner={manner} killer={killerId} mode={mode}"); + $"orphanId={id} hist={Chronicle.HistoryCountFor(id)} want={n} manner={manner} killer={killerId} mode={mode} fav={Chronicle.IsFavoriteSubject(id)}"); break; } @@ -1742,13 +1760,36 @@ public static class AgentHarness bool fav = false; try { - fav = unit != null && unit.isFavorite(); + fav = unit != null && unit.isAlive() && unit.isFavorite(); } catch { // ignore } + if (!fav) + { + long id = WatchCaption.CurrentUnitId; + if (id == 0) + { + id = ChronicleHud.DetailUnitId; + } + + if (id == 0 && unit != null) + { + try + { + id = unit.getID(); + } + catch + { + id = 0; + } + } + + fav = Chronicle.IsFavoriteSubject(id); + } + string wantRaw = (cmd.value ?? "true").Trim().ToLowerInvariant(); bool want = wantRaw != "false" && wantRaw != "0" && wantRaw != "off"; pass = fav == want; diff --git a/IdleSpectator/Chronicle.cs b/IdleSpectator/Chronicle.cs index 0072c70..556a542 100644 --- a/IdleSpectator/Chronicle.cs +++ b/IdleSpectator/Chronicle.cs @@ -151,6 +151,7 @@ public static class Chronicle private static readonly HashSet LoverPairKeys = new HashSet(); private static readonly HashSet FriendPairKeys = new HashSet(); private static readonly HashSet DeathLogged = new HashSet(); + private static readonly HashSet FavoriteIds = new HashSet(); private static readonly Dictionary FinalDossiers = new Dictionary(); private static readonly HashSet LandmarkDedupe = new HashSet(); @@ -244,6 +245,82 @@ public static class Chronicle public static int FinalDossierCount => FinalDossiers.Count; + public static bool IsFavoriteSubject(long unitId) + { + return unitId != 0 && FavoriteIds.Contains(unitId); + } + + /// Track favorites across death so Fallen can filter like Living. + public static void SetFavoriteSubject(long unitId, bool favorite) + { + if (unitId == 0) + { + return; + } + + bool had = FavoriteIds.Contains(unitId); + if (favorite == had) + { + if (FinalDossiers.TryGetValue(unitId, out UnitDossier existing) && existing != null) + { + existing.IsFavorite = favorite; + } + + return; + } + + if (favorite) + { + FavoriteIds.Add(unitId); + } + else + { + FavoriteIds.Remove(unitId); + } + + if (FinalDossiers.TryGetValue(unitId, out UnitDossier dossier) && dossier != null) + { + dossier.IsFavorite = favorite; + } + + BumpRevision(); + } + + public static void SyncFavoriteFromActor(Actor actor) + { + if (actor == null) + { + return; + } + + long id = 0; + try + { + id = actor.getID(); + } + catch + { + return; + } + + if (id == 0) + { + return; + } + + bool fav = false; + try + { + fav = actor.isAlive() && actor.isFavorite(); + } + catch + { + fav = false; + } + + SetFavoriteSubject(id, fav); + } + /// Harness / tuning: change subject cap and prune immediately. public static void SetMaxHistorySubjects(int max) { @@ -534,6 +611,7 @@ public static class Chronicle LoverPairKeys.Clear(); FriendPairKeys.Clear(); DeathLogged.Clear(); + FavoriteIds.Clear(); FinalDossiers.Clear(); LandmarkDedupe.Clear(); LandmarksPerAge.Clear(); @@ -587,7 +665,8 @@ public static class Chronicle public static void ShowHud() { EnsureWindow(); - if (ChronicleHud.DetailUnitId == 0) + // Default into Living only when opening from World with no detail pinned. + if (ChronicleHud.DetailUnitId == 0 && ChronicleHud.ActiveTab == ChronicleHud.LoreTab.World) { ChronicleHud.SetTab(ChronicleHud.LoreTab.Living); } @@ -706,6 +785,7 @@ public static class Chronicle } NoteFocusId(id); + SyncFavoriteFromActor(actor); } private static void NoteFocusId(long id) @@ -867,6 +947,15 @@ public static class Chronicle return; } + if (fav) + { + FavoriteIds.Add(id); + } + else + { + FavoriteIds.Remove(id); + } + string name = SafeName(actor); string species = SpeciesOf(actor); if (!string.IsNullOrEmpty(needle)) @@ -982,7 +1071,13 @@ public static class Chronicle if (favoritesOnly) { - continue; + if (!IsFavoriteSubject(id) + && !(FinalDossiers.TryGetValue(id, out UnitDossier favDossier) + && favDossier != null + && favDossier.IsFavorite)) + { + continue; + } } IReadOnlyList snap = SnapshotForSubject(id); @@ -1003,13 +1098,21 @@ public static class Chronicle } } + bool fallenFav = IsFavoriteSubject(id); + if (!fallenFav + && FinalDossiers.TryGetValue(id, out UnitDossier archived) + && archived != null) + { + fallenFav = archived.IsFavorite; + } + byId[id] = new ChronicleCharacterSummary { UnitId = id, Name = name, SpeciesId = species, HistoryCount = snap.Count, - IsFavorite = false, + IsFavorite = fallenFav, IsAlive = false, IsRecent = false, DeathManner = LatestDeathMannerFor(id), @@ -1021,6 +1124,12 @@ public static class Chronicle var list = new List(byId.Values); list.Sort((a, b) => { + // Favorites first on both Living and Fallen. + if (a.IsFavorite != b.IsFavorite) + { + return a.IsFavorite ? -1 : 1; + } + if (wantFallen || (!a.IsAlive && !b.IsAlive)) { // Fallen: most recent event first. @@ -1037,12 +1146,7 @@ public static class Chronicle return string.Compare(a.Title, b.Title, System.StringComparison.OrdinalIgnoreCase); } - // Living: favorites, recently watched, event count, name. - if (a.IsFavorite != b.IsFavorite) - { - return a.IsFavorite ? -1 : 1; - } - + // Living: recently watched, event count, name. int ra = a.IsRecent && a.RecentIndex >= 0 ? a.RecentIndex : int.MaxValue; int rb = b.IsRecent && b.RecentIndex >= 0 ? b.RecentIndex : int.MaxValue; if (ra != rb) @@ -1270,7 +1374,8 @@ public static class Chronicle string labelPrefix = "Fallen", AttackType attackType = AttackType.Age, long killerId = 0, - Vector3 deathPos = default) + Vector3 deathPos = default, + bool favorite = false) { count = Mathf.Clamp(count, 1, MaxHistoryPerUnit); long id; @@ -1362,7 +1467,14 @@ public static class Chronicle BumpRevision(); - FinalDossiers[id] = UnitDossier.FromFallenArchive(id, who, sp, manner); + UnitDossier archived = UnitDossier.FromFallenArchive(id, who, sp, manner); + archived.IsFavorite = favorite; + FinalDossiers[id] = archived; + if (favorite) + { + FavoriteIds.Add(id); + } + PruneFinalDossiers(); return id; } @@ -1615,6 +1727,11 @@ public static class Chronicle } FinalDossiers[id] = dossier; + if (dossier.IsFavorite) + { + FavoriteIds.Add(id); + } + PruneFinalDossiers(); } @@ -1946,6 +2063,7 @@ public static class Chronicle Histories.Remove(id); DeathLogged.Remove(id); FinalDossiers.Remove(id); + FavoriteIds.Remove(id); need--; } } diff --git a/IdleSpectator/ChronicleHud.cs b/IdleSpectator/ChronicleHud.cs index 5e4c651..7783581 100644 --- a/IdleSpectator/ChronicleHud.cs +++ b/IdleSpectator/ChronicleHud.cs @@ -848,20 +848,17 @@ public static class ChronicleHud if (_favFilterBtn != null) { - // Favorites filter is Living-only. - _favFilterBtn.gameObject.SetActive(_tab == LoreTab.Living && !detail); + _favFilterBtn.gameObject.SetActive(chars && !detail); } if (_refreshBtn != null) { _refreshBtn.gameObject.SetActive(chars && !detail); - // On Fallen (no fav), park refresh on the far right. RectTransform refreshRt = _refreshBtn.GetComponent(); if (refreshRt != null) { - refreshRt.anchoredPosition = _tab == LoreTab.Living - ? new Vector2(-19f, 0f) - : Vector2.zero; + // Search | Refresh | Fav on both Living and Fallen. + refreshRt.anchoredPosition = new Vector2(-19f, 0f); } } @@ -1165,7 +1162,7 @@ public static class ChronicleHud Chronicle.CharacterListScope scope = _tab == LoreTab.Fallen ? Chronicle.CharacterListScope.Fallen : Chronicle.CharacterListScope.Living; - bool favOnly = scope == Chronicle.CharacterListScope.Living && _favoritesOnly; + bool favOnly = _favoritesOnly; List list = Chronicle.ListCharacters(_searchText, favOnly, scope: scope); _lastCharFingerprint = CharacterFingerprint(); _builtAtRevision = Chronicle.HistoryRevision; @@ -1175,7 +1172,7 @@ public static class ChronicleHud { if (scope == Chronicle.CharacterListScope.Fallen) { - AddEmpty("No fallen yet"); + AddEmpty(favOnly ? "No favorite fallen" : "No fallen yet"); } else { @@ -1197,9 +1194,7 @@ public static class ChronicleHud continue; } - if (scope == Chronicle.CharacterListScope.Living - && (favOnly || row.IsFavorite) - && !wroteFavoritesHeader) + if ((favOnly || row.IsFavorite) && !wroteFavoritesHeader) { AddSection("Favorites"); wroteFavoritesHeader = true; diff --git a/IdleSpectator/HarnessScenarios.cs b/IdleSpectator/HarnessScenarios.cs index 64a0107..09feb58 100644 --- a/IdleSpectator/HarnessScenarios.cs +++ b/IdleSpectator/HarnessScenarios.cs @@ -689,6 +689,21 @@ internal static class HarnessScenarios Step("ch18ft5f", "lore_refresh"), Step("ch18ft5g", "assert", expect: "lore_list_stale", value: "false"), Step("ch18ft5h", "assert", expect: "fallen_list_top", value: "FreshFallen"), + // Favorite filter on Fallen (same star control as Living). + Step( + "ch18ft5i", + "chronicle_orphan", + label: "FavFallen", + asset: "human", + value: "Fav", + count: 1, + expect: "Age", + tier: "place+fav"), + Step("ch18ft5j", "lore_favorites", value: "true"), + Step("ch18ft5k", "lore_refresh"), + Step("ch18ft5l", "assert", expect: "fallen_list_top", value: "FavFallen"), + Step("ch18ft5m", "lore_favorites", value: "false"), + Step("ch18ft5n", "lore_refresh"), Step("ch18ft6", "screenshot", value: "hud-lore-fallen-tab.png"), Step("ch18ft7", "wait", wait: 0.55f), Step("ch18q12", "lore_tab", value: "world"), diff --git a/IdleSpectator/WatchCaption.cs b/IdleSpectator/WatchCaption.cs index a0fca77..26bd973 100644 --- a/IdleSpectator/WatchCaption.cs +++ b/IdleSpectator/WatchCaption.cs @@ -224,23 +224,45 @@ public static class WatchCaption } } - if (actor == null || !actor.isAlive()) + if (actor != null && actor.isAlive()) + { + try + { + actor.switchFavorite(); + } + catch + { + return; + } + + Chronicle.SyncFavoriteFromActor(actor); + RefreshFavoriteVisual(actor); + LogService.LogInfo( + $"[IdleSpectator] Favorite toggled name={SafeActorName(actor)} favorite={actor.isFavorite()}"); + return; + } + + // Fallen / archive: track in Chronicle so the Fallen filter stays consistent. + long id = _current != null ? _current.UnitId : 0; + if (id == 0) + { + id = Chronicle.CurrentHistorySubjectId(); + } + + if (id == 0) { return; } - try + bool next = !Chronicle.IsFavoriteSubject(id); + Chronicle.SetFavoriteSubject(id, next); + if (_current != null && _current.UnitId == id) { - actor.switchFavorite(); - } - catch - { - return; + _current.IsFavorite = next; } - RefreshFavoriteVisual(actor); - LogService.LogInfo( - $"[IdleSpectator] Favorite toggled name={SafeActorName(actor)} favorite={actor.isFavorite()}"); + RefreshFavoriteVisual(null); + LogService.LogInfo($"[IdleSpectator] Favorite toggled fallen id={id} favorite={next}"); } public static void PinWhilePaused() @@ -1266,13 +1288,34 @@ public static class WatchCaption bool fav = false; try { - fav = actor != null && actor.isAlive() && actor.isFavorite(); + if (actor != null && actor.isAlive() && actor.isFavorite()) + { + fav = true; + } } catch { fav = false; } + if (!fav) + { + long id = _current != null ? _current.UnitId : 0; + if (id == 0 && actor != null) + { + try + { + id = actor.getID(); + } + catch + { + id = 0; + } + } + + fav = Chronicle.IsFavoriteSubject(id) || (_current != null && _current.IsFavorite); + } + HudIcons.Apply(_favoriteIcon, HudIcons.Favorite()); _favoriteIcon.color = fav ? new Color(1f, 0.85f, 0.25f, 1f) diff --git a/IdleSpectator/mod.json b/IdleSpectator/mod.json index 33f10cb..a44b2ff 100644 --- a/IdleSpectator/mod.json +++ b/IdleSpectator/mod.json @@ -1,7 +1,7 @@ { "name": "IdleSpectator", "author": "dazed", - "version": "0.12.22", - "description": "AFK Idle Spectator (I) + Lore (L). Subject soft cap 100k.", + "version": "0.12.24", + "description": "AFK Idle Spectator (I) + Lore (L). Fallen favorites filter.", "GUID": "com.dazed.idlespectator" }