diff --git a/S1API.Tests/Internal/Patches/ContactsAppWaitTests.cs b/S1API.Tests/Internal/Patches/ContactsAppWaitTests.cs new file mode 100644 index 00000000..b346d2e9 --- /dev/null +++ b/S1API.Tests/Internal/Patches/ContactsAppWaitTests.cs @@ -0,0 +1,31 @@ +using S1API.Internal.Patches; + +namespace S1API.Tests.Internal.Patches; + +public sealed class ContactsAppWaitTests +{ + private static Func Id(string id) => () => id; + private static Func Throws() => () => throw new NullReferenceException("NPC data is gone"); + + [Fact] + public void AnNpcWhoseIdThrowsDoesNotHideTheOthers() + { + Assert.True(ContactsAppPatches.AllIdsPresent( + new[] { Id("big_willy"), Throws(), Id("disco_davey"), Throws() }, + new[] { "big_willy", "disco_davey" })); + } + + [Fact] + public void AMissingCustomNpcIsStillWaitedFor() + { + Assert.False(ContactsAppPatches.AllIdsPresent( + new[] { Id("big_willy"), Throws() }, + new[] { "big_willy", "disco_davey" })); + } + + [Fact] + public void NothingReadableMeansNothingPresent() + { + Assert.False(ContactsAppPatches.AllIdsPresent(new[] { Throws() }, new[] { "big_willy" })); + } +} diff --git a/S1API/Internal/Patches/ContactsAppPatches.cs b/S1API/Internal/Patches/ContactsAppPatches.cs index 1ef7e88f..89f22a1e 100644 --- a/S1API/Internal/Patches/ContactsAppPatches.cs +++ b/S1API/Internal/Patches/ContactsAppPatches.cs @@ -169,11 +169,9 @@ private static IEnumerator WaitForNPCs(S1ContactsApp.ContactsApp contactsApp) if (customNPCs.Count == 0) yield break; - yield return new WaitUntil((Func)(() => - { - var allSceneNPCs = Object.FindObjectsOfType(true); - return customNPCs.All(npc => allSceneNPCs.Any(sn => sn.ID == npc.ID)); - })); + // A few checks a second, not every frame: this searches the whole scene, inactive objects included. + while (!AllPresentInScene(customNPCs)) + yield return new WaitForSeconds(0.25f); yield return new WaitUntil((Func)(() => { @@ -201,6 +199,30 @@ private static IEnumerator WaitForNPCs(S1ContactsApp.ContactsApp contactsApp) AddRelationCircles(contactsApp); } + private static bool AllPresentInScene(System.Collections.Generic.List customNPCs) => + AllIdsPresent( + Object.FindObjectsOfType(true).Select(sceneNpc => (System.Func)(() => sceneNpc.ID)), + customNPCs.Select(npc => npc.ID)); + + /// + /// Are all ids among the scene's NPC ids? Some of the game's own NPCs throw from + /// ID (the pooled special customers on IL2CPP); they are never waited for, so they are skipped rather + /// than failing the whole check. + /// + internal static bool AllIdsPresent( + System.Collections.Generic.IEnumerable> sceneIds, + System.Collections.Generic.IEnumerable wanted) + { + var present = new System.Collections.Generic.HashSet(); + foreach (System.Func readId in sceneIds) + { + try { present.Add(readId()); } + catch (System.Exception) { } + } + + return wanted.All(present.Contains); + } + /// /// Creates and positions relation circles for all custom NPCs. ///