using Archipelago.HollowKnight.IC; using Archipelago.HollowKnight.IC.Modules; using Archipelago.MultiClient.Net; using Archipelago.MultiClient.Net.Enums; using Archipelago.MultiClient.Net.Exceptions; using Archipelago.MultiClient.Net.Models; using ItemChanger; using ItemChanger.Modules; using ItemChanger.Placements; using ItemChanger.Tags; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using UnityEngine.SceneManagement; namespace Archipelago.HollowKnight; public class HintTracker : Module { public static event Action OnArchipelagoHintUpdate; /// /// List of MultiClient.Net Hint's /// public static List Hints; /// /// List of placement hints to send on scene change or when closing out the session /// private List PendingPlacementHints; private ArchipelagoSession session; private void UpdateHints(Hint[] arrayHints) { Hints = arrayHints.ToList(); foreach (Hint hint in Hints) { if (hint.FindingPlayer != ArchipelagoMod.Instance.session.ConnectionInfo.Slot) { continue; } if (!ArchipelagoPlacementTag.PlacementsByLocationId.ContainsKey(hint.LocationId)) { continue; } ArchipelagoMod.Instance.LogDebug($"Hint data received for item {hint.ItemId} at location {hint.LocationId}"); AbstractPlacement placement = ArchipelagoPlacementTag.PlacementsByLocationId[hint.LocationId]; if (placement == null) { continue; } // set the hinted tag for the single item in the placement that was hinted for. foreach (ArchipelagoItemTag tag in placement.Items.Select(item => item.GetTag()) .Where(tag => tag.Location == hint.LocationId)) { ArchipelagoMod.Instance.LogDebug("Setting hinted true for item"); tag.Hinted = true; } // if all items inside a placement have been hinted for then mark the entire placement as hinted. if (placement.Items.TrueForAll(item => item.GetTag().Hinted)) { ArchipelagoMod.Instance.LogDebug("Setting hinted true for placement"); placement.GetTag().Hinted = true; } if (placement is ShopPlacement shop) { List<(string, AbstractItem)> previewText = new(); foreach (AbstractItem item in shop.Items) { if (item.GetTag().Hinted) { previewText.Add((item.GetPreviewWithCost(), item)); } else { previewText.Add((Language.Language.Get("???", "IC"), item)); } } MultiPreviewRecordTag previewRecordTag = shop.GetOrAddTag(); previewRecordTag.previewTexts ??= new string[shop.Items.Count]; foreach ((string, AbstractItem item) p in previewText) { string str = p.Item1; int index = shop.Items.IndexOf(p.item); if (index >= 0) { previewRecordTag.previewTexts[index] = str; } } } else { List previewText = new(); foreach (AbstractItem item in placement.Items) { if (item.WasEverObtained()) { continue; } previewText.Add(item.GetTag().Hinted ? item.GetPreviewWithCost() : Language.Language.Get("???", "IC")); } placement.GetOrAddTag().previewText = string.Join(Language.Language.Get("COMMA_SPACE", "IC"), previewText); } } try { OnArchipelagoHintUpdate?.Invoke(); } catch (Exception ex) { ArchipelagoMod.Instance.LogError($"Error invoking OnArchipelagoHintUpdate:\n {ex}"); } } public override void Initialize() { PendingPlacementHints = []; session = ArchipelagoMod.Instance.session; // do most setup in OnEnterGame so save data can completely load, we need to // populate all the AP placements to sync with server Events.OnEnterGame += OnEnterGame; } private void OnEnterGame() { session.DataStorage.TrackHints(UpdateHints); AbstractItem.AfterGiveGlobal += UpdateHintFoundStatus; Events.OnSceneChange += SendHintsOnSceneChange; } public override async void Unload() { Events.OnEnterGame -= OnEnterGame; AbstractItem.AfterGiveGlobal -= UpdateHintFoundStatus; Events.OnSceneChange -= SendHintsOnSceneChange; await SendPlacementHintsAsync(); } public void HintPlacement(AbstractPlacement pmt) { // todo - accommodate different hinting times (immediate/never) PendingPlacementHints.Add(pmt); } private void UpdateHintFoundStatus(ReadOnlyGiveEventArgs args) { if (Hints != null && args.Orig.GetTag(out ArchipelagoItemTag tag)) { long location = tag.Location; foreach (Hint hint in Hints) { if (hint.LocationId == location) { hint.Found = true; try { OnArchipelagoHintUpdate?.Invoke(); } catch (Exception ex) { ArchipelagoMod.Instance.LogError($"Error invoking OnArchipelagoHintUpdate:\n {ex}"); } break; } } } } private async void SendHintsOnSceneChange(Scene scene) { await SendPlacementHintsAsync(); } private async Task SendPlacementHintsAsync() { if (!PendingPlacementHints.Any()) { return; } HashSet hintedTags = new(); HashSet hintedLocationIDs = new(); ArchipelagoItemTag tag; foreach (AbstractPlacement pmt in PendingPlacementHints) { foreach (AbstractItem item in pmt.Items) { if (item.GetTag(out tag) && !tag.Hinted) { if ((tag.Flags.HasFlag(ItemFlags.Advancement) || tag.Flags.HasFlag(ItemFlags.NeverExclude)) && !item.WasEverObtained() && !item.HasTag()) { hintedTags.Add(tag); hintedLocationIDs.Add(tag.Location); } else { tag.Hinted = true; } } } } PendingPlacementHints.Clear(); if (!hintedLocationIDs.Any()) { return; } ArchipelagoMod.Instance.LogDebug($"Hinting {hintedLocationIDs.Count()} locations."); try { await session.Locations.ScoutLocationsAsync(true, hintedLocationIDs.ToArray()) .ContinueWith(x => { bool result = !x.IsFaulted; foreach (ArchipelagoItemTag tag in hintedTags) { tag.Hinted = result; } }).TimeoutAfter(1000); } catch (Exception ex) when (ex is ArchipelagoSocketClosedException or TimeoutException) { ItemChangerMod.Modules.Get().ReportDisconnect(); } } }