From 20996fa3a71e6a5ea7cd7633da1bf407ce3c741c Mon Sep 17 00:00:00 2001 From: DazedAnon Date: Wed, 15 Jul 2026 20:39:11 -0500 Subject: [PATCH] Mutation --- IdleSpectator/AgentHarness.cs | 8 +- IdleSpectator/HarnessScenarios.cs | 12 + IdleSpectator/MutationDiscoveryHarness.cs | 801 ++++++++++++++++++++++ IdleSpectator/mod.json | 2 +- 4 files changed, 821 insertions(+), 2 deletions(-) create mode 100644 IdleSpectator/MutationDiscoveryHarness.cs diff --git a/IdleSpectator/AgentHarness.cs b/IdleSpectator/AgentHarness.cs index 765436c..c0334b4 100644 --- a/IdleSpectator/AgentHarness.cs +++ b/IdleSpectator/AgentHarness.cs @@ -4056,12 +4056,18 @@ public static class AgentHarness case "disaster_war_audit": { DisasterWarAuditResult audit = DisasterWarHarness.RunAudit(HarnessDir()); - // Keep inventory dump for debugging alongside audit. WorldLogEventHarness.DumpDisasterWarInventory(HarnessDir()); pass = audit.Passed; detail = audit.Detail; break; } + case "mutation_discovery": + { + MutationDiscoveryResult discovery = MutationDiscoveryHarness.Run(HarnessDir()); + pass = discovery.Passed; + detail = discovery.Detail; + break; + } case "activity_happiness_count": { int want = ParseCountExpect(cmd, defaultValue: 1); diff --git a/IdleSpectator/HarnessScenarios.cs b/IdleSpectator/HarnessScenarios.cs index 30ed302..4fd71a8 100644 --- a/IdleSpectator/HarnessScenarios.cs +++ b/IdleSpectator/HarnessScenarios.cs @@ -55,6 +55,8 @@ internal static class HarnessScenarios return DisasterWarAudit(); case "event_coverage": return EventCoverage(); + case "mutation_discovery": + return MutationDiscovery(); case "chronicle": case "chronicle_smoke": return ChronicleSmoke(); @@ -1127,6 +1129,16 @@ internal static class HarnessScenarios }; } + private static List MutationDiscovery() + { + return new List + { + Step("md0", "wait_world"), + Step("md1", "assert", expect: "mutation_discovery"), + Step("md2", "snapshot") + }; + } + private static List ActivityRateSample() { return new List diff --git a/IdleSpectator/MutationDiscoveryHarness.cs b/IdleSpectator/MutationDiscoveryHarness.cs new file mode 100644 index 0000000..da5ca62 --- /dev/null +++ b/IdleSpectator/MutationDiscoveryHarness.cs @@ -0,0 +1,801 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; +using UnityEngine; + +namespace IdleSpectator; + +public sealed class MutationDiscoveryResult +{ + public bool Passed; + public string Detail = ""; + public int Managers; + public int ManagerMethods; + public int ActorMethods; + public int BehTypes; + public int Libraries; + public int LibraryAssets; + public int Gaps; +} + +/// +/// Discovers event-relevant mutation paths from live game assemblies and AssetManager. +/// Writes TSVs under .harness/ for the idle-camera coverage plan. +/// +public static class MutationDiscoveryHarness +{ + private static readonly string[] ManagerNameNeedles = + { + "Manager", "Keeper" + }; + + private static readonly string[] MethodPrefixNeedles = + { + "new", "add", "remove", "set", "become", "start", "finish", "cancel", + "create", "destroy", "die", "kill", "birth", "clear", "leave", "join", + "make", "end", "load", "spawn", "generate", "burn", "write" + }; + + private static readonly string[] ActorMethodNeedles = + { + "lover", "friend", "family", "child", "parent", "baby", "born", "birth", + "mate", "breed", "social", "clan", "king", "leader", "trait", "status", + "happiness", "plot", "army", "war", "alliance", "culture", "religion", + "language", "book", "item", "boat", "home", "house", "city", "kingdom", + "die", "kill", "task", "job" + }; + + private static readonly string[] BehNeedles = + { + "Lover", "Friend", "Family", "Social", "Child", "Baby", "Birth", "Plot", + "Clan", "Army", "War", "Book", "Build", "Boat", "Trade", "Era", "Age", + "Trait", "Religion", "Culture", "Language", "Alliance", "King", "Marry" + }; + + private static readonly string[] LibraryFields = + { + "actor_library", "status", "happiness_library", "world_log_library", + "disasters", "war_types_library", "traits", "plots_library", "plot_category_library", + "era_library", "buildings", "architecture_library", "city_build_orders", + "culture_traits", "religion_traits", "clan_traits", "language_traits", + "subspecies_traits", "kingdoms_traits", "book_types", "items", "items_modifiers", + "spells", "powers", "combat_action_library", "decisions_library", + "loyalty_library", "opinion_library", "knowledge_library", "gene_library", + "phenotype_library", "communication_library", "story_library", + "world_laws_library", "biome_library", "citizen_job_library", "professions", + "personalities", "tasks_actor" + }; + + private static readonly HashSet AlreadyWired = + new HashSet(StringComparer.OrdinalIgnoreCase) + { + "Actor.changeHappiness", + "Actor.checkHappinessChangeFromDeathEvent", + "Actor.birthEvent", + "Actor.becomeLoversWith", + "Actor.setBestFriend", + "Actor.newKillAction", + "Actor.die", + "Actor.setTask", + "WorldLogMessageExtensions.add", + "BaseSimObject.addNewStatusEffect", + "Status.finish", + "SubspeciesManager.newSpecies", + "BehPollinate.execute", + "BehUnloadResources.execute", + "BehAttackActorHuntingTarget.execute", + "BehCityActorRemoveFire.execute", + "BehTryToSocialize.execute", + "BehPlantCrops.execute", + "BehCityActorFertilizeCrop.execute", + "BehThrowResources.execute", + "BehBuildTarget.execute", + "BehPoopOutside.execute", + "BehPoopInside.execute", + "BehBoatFishing.execute", + "BehBoatCollectFish.execute", + "BehClaimZoneForCityActorBorder.execute", + "InterestFeeds.OnWorldLogMessage", + "InterestFeeds.OnStatusChange", + "InterestFeeds.OnActivityNote", + "InterestFeeds.OnChronicleMilestone", + "WarInterestFeed.Tick", + "WorldActivityScanner.FindHottestBattle", + "SpeciesDiscovery" + }; + + public static MutationDiscoveryResult Run(string outputDirectory) + { + Assembly gameAsm = typeof(Actor).Assembly; + var managersTsv = new StringBuilder(); + managersTsv.AppendLine("type\tmethod\tarity\tstatic\tkind_guess\talready_wired\tnotes"); + + var actorTsv = new StringBuilder(); + actorTsv.AppendLine("type\tmethod\tarity\tstatic\tneedle\talready_wired\tnotes"); + + var behTsv = new StringBuilder(); + behTsv.AppendLine("type\tmethod\tarity\tneedle\talready_wired\tnotes"); + + var libTsv = new StringBuilder(); + libTsv.AppendLine("field\tcount\tsample_ids\tnotes"); + + var gapsTsv = new StringBuilder(); + gapsTsv.AppendLine( + "kind\ttype\tmethod_or_field\tevent_worthy\tsuggested_ownership\talready_wired\treason"); + + int managerCount = 0; + int managerMethods = 0; + int actorMethods = 0; + int behTypes = 0; + int libraries = 0; + int libraryAssets = 0; + int gaps = 0; + + Type[] types; + try + { + types = gameAsm.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + types = ex.Types ?? Array.Empty(); + } + + var managerTypes = new List(); + var behTypesList = new List(); + Type actorType = typeof(Actor); + Type baseSimType = typeof(BaseSimObject); + + for (int i = 0; i < types.Length; i++) + { + Type t = types[i]; + if (t == null || string.IsNullOrEmpty(t.Name)) + { + continue; + } + + string name = t.Name; + if (IsManagerish(name)) + { + managerTypes.Add(t); + } + + if (name.StartsWith("Beh", StringComparison.Ordinal) && MatchesAny(name, BehNeedles)) + { + behTypesList.Add(t); + } + } + + managerTypes.Sort((a, b) => string.CompareOrdinal(a.FullName, b.FullName)); + for (int i = 0; i < managerTypes.Count; i++) + { + Type t = managerTypes[i]; + managerCount++; + MethodInfo[] methods = SafeMethods(t); + for (int m = 0; m < methods.Length; m++) + { + MethodInfo method = methods[m]; + if (method == null || method.IsSpecialName) + { + continue; + } + + if (!MethodLooksMutating(method.Name)) + { + continue; + } + + managerMethods++; + string key = t.Name + "." + method.Name; + bool wired = AlreadyWired.Contains(key); + string kind = GuessManagerKind(t.Name, method.Name); + managersTsv.Append(t.Name).Append('\t') + .Append(method.Name).Append('\t') + .Append(method.GetParameters().Length).Append('\t') + .Append(method.IsStatic ? "1" : "0").Append('\t') + .Append(kind).Append('\t') + .Append(wired ? "1" : "0").Append('\t') + .Append(Escape(method.ToString())).AppendLine(); + + if (!wired && IsEventWorthyManager(t.Name, method.Name)) + { + gaps++; + gapsTsv.Append("ManagerGenesis").Append('\t') + .Append(t.Name).Append('\t') + .Append(method.Name).Append('\t') + .Append("Yes").Append('\t') + .Append(SuggestOwnership(t.Name, method.Name)).Append('\t') + .Append("0").Append('\t') + .Append("unwired_manager_mutation").AppendLine(); + } + } + } + + DumpTypeMethods(actorType, ActorMethodNeedles, actorTsv, gapsTsv, ref actorMethods, ref gaps); + DumpTypeMethods(baseSimType, ActorMethodNeedles, actorTsv, gapsTsv, ref actorMethods, ref gaps); + + // Also scan Actor for lover/friend clear-style methods even if needle-filtered narrowly. + DumpExtraActorRelationMethods(actorType, actorTsv, gapsTsv, ref actorMethods, ref gaps); + + behTypesList.Sort((a, b) => string.CompareOrdinal(a.Name, b.Name)); + for (int i = 0; i < behTypesList.Count; i++) + { + Type t = behTypesList[i]; + behTypes++; + string needle = FirstMatch(t.Name, BehNeedles); + MethodInfo exec = t.GetMethod("execute", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + string methodName = exec != null ? "execute" : ""; + string key = t.Name + (string.IsNullOrEmpty(methodName) ? "" : "." + methodName); + bool wired = AlreadyWired.Contains(key); + int arity = exec != null ? exec.GetParameters().Length : 0; + behTsv.Append(t.Name).Append('\t') + .Append(methodName).Append('\t') + .Append(arity).Append('\t') + .Append(needle).Append('\t') + .Append(wired ? "1" : "0").Append('\t') + .Append("BehPulse").AppendLine(); + + if (!wired && IsEventWorthyBeh(t.Name)) + { + gaps++; + gapsTsv.Append("BehPulse").Append('\t') + .Append(t.Name).Append('\t') + .Append(methodName).Append('\t') + .Append(BehSoft(t.Name) ? "Soft" : "Yes").Append('\t') + .Append(BehSoft(t.Name) ? "Ambient" : "Signal").Append('\t') + .Append("0").Append('\t') + .Append("unwired_beh").AppendLine(); + } + } + + for (int i = 0; i < LibraryFields.Length; i++) + { + string field = LibraryFields[i]; + List ids = EnumerateLibraryIds(field); + libraries++; + libraryAssets += ids.Count; + string sample = ""; + int sampleN = Math.Min(5, ids.Count); + for (int s = 0; s < sampleN; s++) + { + if (s > 0) + { + sample += ","; + } + + sample += ids[s]; + } + + bool known = field == "actor_library" + || field == "status" + || field == "happiness_library" + || field == "world_log_library" + || field == "disasters" + || field == "war_types_library" + || field == "traits" + || field == "tasks_actor"; + libTsv.Append(field).Append('\t') + .Append(ids.Count).Append('\t') + .Append(Escape(sample)).Append('\t') + .Append(known ? "partially_or_fully_wired" : "library_unwired_for_events").AppendLine(); + + if (!known && ids.Count > 0) + { + gaps++; + gapsTsv.Append("LibraryAsset").Append('\t') + .Append("AssetManager").Append('\t') + .Append(field).Append('\t') + .Append("Yes").Append('\t') + .Append("Signal").Append('\t') + .Append("0").Append('\t') + .Append("count=" + ids.Count).AppendLine(); + } + } + + // World / MapBox live manager fields + AppendWorldFields(gapsTsv, managersTsv, ref gaps, ref managerMethods); + + try + { + if (!string.IsNullOrEmpty(outputDirectory)) + { + Directory.CreateDirectory(outputDirectory); + File.WriteAllText(Path.Combine(outputDirectory, "mutation_managers.tsv"), managersTsv.ToString()); + File.WriteAllText(Path.Combine(outputDirectory, "mutation_actor.tsv"), actorTsv.ToString()); + File.WriteAllText(Path.Combine(outputDirectory, "mutation_beh.tsv"), behTsv.ToString()); + File.WriteAllText(Path.Combine(outputDirectory, "mutation_libraries.tsv"), libTsv.ToString()); + File.WriteAllText(Path.Combine(outputDirectory, "mutation_gaps.tsv"), gapsTsv.ToString()); + } + } + catch (Exception ex) + { + Debug.LogWarning("[IdleSpectator] mutation discovery write failed: " + ex.Message); + } + + return new MutationDiscoveryResult + { + Passed = managerCount > 0 && libraries > 0, + Managers = managerCount, + ManagerMethods = managerMethods, + ActorMethods = actorMethods, + BehTypes = behTypes, + Libraries = libraries, + LibraryAssets = libraryAssets, + Gaps = gaps, + Detail = + $"managers={managerCount} managerMethods={managerMethods} actorMethods={actorMethods} " + + $"beh={behTypes} libraries={libraries} libraryAssets={libraryAssets} gaps={gaps}" + }; + } + + private static void AppendWorldFields( + StringBuilder gapsTsv, + StringBuilder managersTsv, + ref int gaps, + ref int managerMethods) + { + object world = null; + try + { + world = World.world; + } + catch + { + // ignore + } + + if (world == null) + { + return; + } + + string[] fields = + { + "plots", "wars", "alliances", "clans", "cultures", "religions", "languages", + "families", "armies", "books", "buildings", "items", "subspecies", "cities", + "kingdoms", "diplomacy", "era_manager", "earthquake_manager", "units" + }; + + Type worldType = world.GetType(); + for (int i = 0; i < fields.Length; i++) + { + string name = fields[i]; + object value = null; + try + { + FieldInfo f = worldType.GetField(name); + PropertyInfo p = worldType.GetProperty(name); + value = f != null ? f.GetValue(world) : p?.GetValue(world, null); + } + catch + { + // ignore + } + + string typeName = value != null ? value.GetType().Name : "null"; + managersTsv.Append("World.").Append(name).Append('\t') + .Append("(instance)").Append('\t') + .Append("0").Append('\t') + .Append("0").Append('\t') + .Append("PollState").Append('\t') + .Append(name == "wars" || name == "units" || name == "subspecies" ? "1" : "0").Append('\t') + .Append(typeName).AppendLine(); + managerMethods++; + + bool wired = name == "wars" || name == "units" || name == "subspecies"; + if (!wired && value != null) + { + gaps++; + gapsTsv.Append("PollState").Append('\t') + .Append("World").Append('\t') + .Append(name).Append('\t') + .Append("Yes").Append('\t') + .Append("Signal").Append('\t') + .Append("0").Append('\t') + .Append("world_field=" + typeName).AppendLine(); + } + } + } + + private static void DumpTypeMethods( + Type type, + string[] needles, + StringBuilder actorTsv, + StringBuilder gapsTsv, + ref int actorMethods, + ref int gaps) + { + MethodInfo[] methods = SafeMethods(type); + for (int i = 0; i < methods.Length; i++) + { + MethodInfo method = methods[i]; + if (method == null || method.IsSpecialName) + { + continue; + } + + string needle = FirstMatch(method.Name, needles); + if (string.IsNullOrEmpty(needle) && !MethodLooksMutating(method.Name)) + { + continue; + } + + if (string.IsNullOrEmpty(needle)) + { + needle = "mutate"; + } + + actorMethods++; + string key = type.Name + "." + method.Name; + bool wired = AlreadyWired.Contains(key); + actorTsv.Append(type.Name).Append('\t') + .Append(method.Name).Append('\t') + .Append(method.GetParameters().Length).Append('\t') + .Append(method.IsStatic ? "1" : "0").Append('\t') + .Append(needle).Append('\t') + .Append(wired ? "1" : "0").Append('\t') + .Append(Escape(method.ToString())).AppendLine(); + + if (!wired && IsEventWorthyActor(method.Name)) + { + gaps++; + gapsTsv.Append("ActorMutation").Append('\t') + .Append(type.Name).Append('\t') + .Append(method.Name).Append('\t') + .Append("Yes").Append('\t') + .Append(SuggestOwnership("Actor", method.Name)).Append('\t') + .Append("0").Append('\t') + .Append("unwired_actor_mutation").AppendLine(); + } + } + } + + private static void DumpExtraActorRelationMethods( + Type actorType, + StringBuilder actorTsv, + StringBuilder gapsTsv, + ref int actorMethods, + ref int gaps) + { + string[] extra = + { + "removeLover", "clearLover", "setLover", "getLover", "hasLover", + "removeBestFriend", "clearBestFriend", "getBestFriend", + "addChild", "removeChild", "makeBaby", "haveChild", + "addTrait", "removeTrait", "setPlot", "leavePlot", "finishPlot" + }; + + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + MethodInfo[] found = actorType.GetMethods( + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + for (int m = 0; m < found.Length; m++) + { + MethodInfo method = found[m]; + if (method == null || method.IsSpecialName) + { + continue; + } + + bool match = false; + for (int i = 0; i < extra.Length; i++) + { + if (method.Name.Equals(extra[i], StringComparison.OrdinalIgnoreCase) + || method.Name.StartsWith(extra[i], StringComparison.OrdinalIgnoreCase)) + { + match = true; + break; + } + } + + if (!match || !seen.Add(method.Name)) + { + continue; + } + + actorMethods++; + string key = actorType.Name + "." + method.Name; + bool wired = AlreadyWired.Contains(key); + actorTsv.Append(actorType.Name).Append('\t') + .Append(method.Name).Append('\t') + .Append(method.GetParameters().Length).Append('\t') + .Append(method.IsStatic ? "1" : "0").Append('\t') + .Append("relation").Append('\t') + .Append(wired ? "1" : "0").Append('\t') + .Append("extra_relation_scan").AppendLine(); + + if (!wired && !method.Name.StartsWith("get", StringComparison.OrdinalIgnoreCase) + && !method.Name.StartsWith("has", StringComparison.OrdinalIgnoreCase)) + { + gaps++; + gapsTsv.Append("ActorMutation").Append('\t') + .Append(actorType.Name).Append('\t') + .Append(method.Name).Append('\t') + .Append("Yes").Append('\t') + .Append("Signal").Append('\t') + .Append("0").Append('\t') + .Append("relation_lifecycle").AppendLine(); + } + } + } + + private static List EnumerateLibraryIds(string fieldName) + { + var ids = new List(); + try + { + object lib = typeof(AssetManager).GetField(fieldName)?.GetValue(null); + if (lib == null) + { + return ids; + } + + object listObj = lib.GetType().GetField("list")?.GetValue(lib) + ?? lib.GetType().GetProperty("list")?.GetValue(lib, null); + System.Collections.IList list = listObj as System.Collections.IList; + if (list == null) + { + return ids; + } + + for (int i = 0; i < list.Count; i++) + { + object asset = list[i]; + object idObj = asset?.GetType().GetField("id")?.GetValue(asset) + ?? asset?.GetType().GetProperty("id")?.GetValue(asset, null); + string id = idObj as string; + if (!string.IsNullOrEmpty(id)) + { + ids.Add(id); + } + } + } + catch + { + // Library missing in this build. + } + + return ids; + } + + private static MethodInfo[] SafeMethods(Type type) + { + try + { + return type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly); + } + catch + { + return Array.Empty(); + } + } + + private static bool IsManagerish(string name) + { + for (int i = 0; i < ManagerNameNeedles.Length; i++) + { + if (name.IndexOf(ManagerNameNeedles[i], StringComparison.OrdinalIgnoreCase) >= 0) + { + return true; + } + } + + return false; + } + + private static bool MethodLooksMutating(string name) + { + string n = name ?? ""; + for (int i = 0; i < MethodPrefixNeedles.Length; i++) + { + if (n.StartsWith(MethodPrefixNeedles[i], StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + + private static bool MatchesAny(string value, string[] needles) + { + return !string.IsNullOrEmpty(FirstMatch(value, needles)); + } + + private static string FirstMatch(string value, string[] needles) + { + if (string.IsNullOrEmpty(value) || needles == null) + { + return ""; + } + + for (int i = 0; i < needles.Length; i++) + { + if (value.IndexOf(needles[i], StringComparison.OrdinalIgnoreCase) >= 0) + { + return needles[i]; + } + } + + return ""; + } + + private static string GuessManagerKind(string typeName, string methodName) + { + if (methodName.StartsWith("new", StringComparison.OrdinalIgnoreCase) + || methodName.StartsWith("create", StringComparison.OrdinalIgnoreCase) + || methodName.StartsWith("generate", StringComparison.OrdinalIgnoreCase)) + { + return "ManagerGenesis"; + } + + if (methodName.StartsWith("remove", StringComparison.OrdinalIgnoreCase) + || methodName.StartsWith("destroy", StringComparison.OrdinalIgnoreCase) + || methodName.StartsWith("finish", StringComparison.OrdinalIgnoreCase) + || methodName.StartsWith("cancel", StringComparison.OrdinalIgnoreCase) + || methodName.StartsWith("end", StringComparison.OrdinalIgnoreCase)) + { + return "ManagerEnd"; + } + + return "ManagerMutation"; + } + + private static bool IsEventWorthyManager(string typeName, string methodName) + { + string t = typeName ?? ""; + string m = methodName ?? ""; + if (t.IndexOf("Debug", StringComparison.OrdinalIgnoreCase) >= 0 + || t.IndexOf("UI", StringComparison.OrdinalIgnoreCase) >= 0 + || t.IndexOf("Save", StringComparison.OrdinalIgnoreCase) >= 0 + || t.IndexOf("Upload", StringComparison.OrdinalIgnoreCase) >= 0 + || t.IndexOf("Tooltip", StringComparison.OrdinalIgnoreCase) >= 0 + || t.IndexOf("Nameplate", StringComparison.OrdinalIgnoreCase) >= 0 + || t.IndexOf("Locale", StringComparison.OrdinalIgnoreCase) >= 0 + || t.IndexOf("Asset", StringComparison.OrdinalIgnoreCase) >= 0 && t.IndexOf("Manager", StringComparison.OrdinalIgnoreCase) >= 0 + && t != "AssetManager") + { + // Keep AssetManager out of genesis gaps; libraries handled separately. + if (t.IndexOf("ActorManager", StringComparison.OrdinalIgnoreCase) < 0 + && t.IndexOf("BuildingManager", StringComparison.OrdinalIgnoreCase) < 0 + && t.IndexOf("WarManager", StringComparison.OrdinalIgnoreCase) < 0 + && t.IndexOf("PlotManager", StringComparison.OrdinalIgnoreCase) < 0 + && t.IndexOf("ClanManager", StringComparison.OrdinalIgnoreCase) < 0 + && t.IndexOf("CultureManager", StringComparison.OrdinalIgnoreCase) < 0 + && t.IndexOf("ReligionManager", StringComparison.OrdinalIgnoreCase) < 0 + && t.IndexOf("LanguageManager", StringComparison.OrdinalIgnoreCase) < 0 + && t.IndexOf("FamilyManager", StringComparison.OrdinalIgnoreCase) < 0 + && t.IndexOf("ArmyManager", StringComparison.OrdinalIgnoreCase) < 0 + && t.IndexOf("BookManager", StringComparison.OrdinalIgnoreCase) < 0 + && t.IndexOf("ItemManager", StringComparison.OrdinalIgnoreCase) < 0 + && t.IndexOf("AllianceManager", StringComparison.OrdinalIgnoreCase) < 0 + && t.IndexOf("CityManager", StringComparison.OrdinalIgnoreCase) < 0 + && t.IndexOf("KingdomManager", StringComparison.OrdinalIgnoreCase) < 0 + && t.IndexOf("DiplomacyManager", StringComparison.OrdinalIgnoreCase) < 0 + && t.IndexOf("WorldAgeManager", StringComparison.OrdinalIgnoreCase) < 0 + && t.IndexOf("SubspeciesManager", StringComparison.OrdinalIgnoreCase) < 0 + && t.IndexOf("BattleKeeper", StringComparison.OrdinalIgnoreCase) < 0) + { + return false; + } + } + + // Focus on sim managers + bool simManager = + t.IndexOf("Plot", StringComparison.OrdinalIgnoreCase) >= 0 + || t.IndexOf("War", StringComparison.OrdinalIgnoreCase) >= 0 + || t.IndexOf("Clan", StringComparison.OrdinalIgnoreCase) >= 0 + || t.IndexOf("Culture", StringComparison.OrdinalIgnoreCase) >= 0 + || t.IndexOf("Religion", StringComparison.OrdinalIgnoreCase) >= 0 + || t.IndexOf("Language", StringComparison.OrdinalIgnoreCase) >= 0 + || t.IndexOf("Family", StringComparison.OrdinalIgnoreCase) >= 0 + || t.IndexOf("Army", StringComparison.OrdinalIgnoreCase) >= 0 + || t.IndexOf("Book", StringComparison.OrdinalIgnoreCase) >= 0 + || t.IndexOf("Item", StringComparison.OrdinalIgnoreCase) >= 0 + || t.IndexOf("Building", StringComparison.OrdinalIgnoreCase) >= 0 + || t.IndexOf("Alliance", StringComparison.OrdinalIgnoreCase) >= 0 + || t.IndexOf("City", StringComparison.OrdinalIgnoreCase) >= 0 + || t.IndexOf("Kingdom", StringComparison.OrdinalIgnoreCase) >= 0 + || t.IndexOf("Diplomacy", StringComparison.OrdinalIgnoreCase) >= 0 + || t.IndexOf("WorldAge", StringComparison.OrdinalIgnoreCase) >= 0 + || t.IndexOf("Subspecies", StringComparison.OrdinalIgnoreCase) >= 0 + || t.IndexOf("ActorManager", StringComparison.OrdinalIgnoreCase) >= 0 + || t.IndexOf("BattleKeeper", StringComparison.OrdinalIgnoreCase) >= 0; + + if (!simManager) + { + return false; + } + + return m.StartsWith("new", StringComparison.OrdinalIgnoreCase) + || m.StartsWith("create", StringComparison.OrdinalIgnoreCase) + || m.StartsWith("generate", StringComparison.OrdinalIgnoreCase) + || m.StartsWith("add", StringComparison.OrdinalIgnoreCase) + || m.StartsWith("remove", StringComparison.OrdinalIgnoreCase) + || m.StartsWith("destroy", StringComparison.OrdinalIgnoreCase) + || m.StartsWith("finish", StringComparison.OrdinalIgnoreCase) + || m.StartsWith("cancel", StringComparison.OrdinalIgnoreCase) + || m.StartsWith("start", StringComparison.OrdinalIgnoreCase) + || m.StartsWith("set", StringComparison.OrdinalIgnoreCase) + || m.StartsWith("end", StringComparison.OrdinalIgnoreCase) + || m.StartsWith("burn", StringComparison.OrdinalIgnoreCase) + || m.IndexOf("War", StringComparison.OrdinalIgnoreCase) >= 0 + || m.IndexOf("Age", StringComparison.OrdinalIgnoreCase) >= 0 + || m.IndexOf("Plot", StringComparison.OrdinalIgnoreCase) >= 0 + || m.IndexOf("Friend", StringComparison.OrdinalIgnoreCase) >= 0; + } + + private static bool IsEventWorthyActor(string methodName) + { + string m = methodName ?? ""; + return m.IndexOf("lover", StringComparison.OrdinalIgnoreCase) >= 0 + || m.IndexOf("friend", StringComparison.OrdinalIgnoreCase) >= 0 + || m.IndexOf("child", StringComparison.OrdinalIgnoreCase) >= 0 + || m.IndexOf("birth", StringComparison.OrdinalIgnoreCase) >= 0 + || m.IndexOf("baby", StringComparison.OrdinalIgnoreCase) >= 0 + || m.IndexOf("trait", StringComparison.OrdinalIgnoreCase) >= 0 + || m.IndexOf("plot", StringComparison.OrdinalIgnoreCase) >= 0 + || m.IndexOf("die", StringComparison.OrdinalIgnoreCase) >= 0 + || m.IndexOf("kill", StringComparison.OrdinalIgnoreCase) >= 0 + || m.IndexOf("king", StringComparison.OrdinalIgnoreCase) >= 0 + || m.IndexOf("clan", StringComparison.OrdinalIgnoreCase) >= 0 + || m.IndexOf("army", StringComparison.OrdinalIgnoreCase) >= 0 + || m.IndexOf("book", StringComparison.OrdinalIgnoreCase) >= 0 + || m.IndexOf("home", StringComparison.OrdinalIgnoreCase) >= 0 + || m.IndexOf("house", StringComparison.OrdinalIgnoreCase) >= 0; + } + + private static bool IsEventWorthyBeh(string typeName) + { + string t = typeName ?? ""; + // Skip pure navigation helpers + if (t.IndexOf("FindTile", StringComparison.OrdinalIgnoreCase) >= 0 + || t.IndexOf("GoTo", StringComparison.OrdinalIgnoreCase) >= 0 + || t.IndexOf("LookAt", StringComparison.OrdinalIgnoreCase) >= 0 + || t.IndexOf("CheckExistence", StringComparison.OrdinalIgnoreCase) >= 0 + || t.IndexOf("CheckLimit", StringComparison.OrdinalIgnoreCase) >= 0) + { + return false; + } + + return true; + } + + private static bool BehSoft(string typeName) + { + string t = typeName ?? ""; + return t.IndexOf("Social", StringComparison.OrdinalIgnoreCase) >= 0 + || t.IndexOf("Talk", StringComparison.OrdinalIgnoreCase) >= 0 + || t.IndexOf("Check", StringComparison.OrdinalIgnoreCase) >= 0 + || t.IndexOf("Follow", StringComparison.OrdinalIgnoreCase) >= 0; + } + + private static string SuggestOwnership(string typeName, string methodName) + { + string blob = (typeName ?? "") + "." + (methodName ?? ""); + if (blob.IndexOf("War", StringComparison.OrdinalIgnoreCase) >= 0 + || blob.IndexOf("Plot", StringComparison.OrdinalIgnoreCase) >= 0 + || blob.IndexOf("Age", StringComparison.OrdinalIgnoreCase) >= 0 + || blob.IndexOf("die", StringComparison.OrdinalIgnoreCase) >= 0 + || blob.IndexOf("kill", StringComparison.OrdinalIgnoreCase) >= 0 + || blob.IndexOf("lover", StringComparison.OrdinalIgnoreCase) >= 0 + || blob.IndexOf("destroy", StringComparison.OrdinalIgnoreCase) >= 0 + || blob.IndexOf("new", StringComparison.OrdinalIgnoreCase) >= 0) + { + return "Signal"; + } + + return "Ambient"; + } + + private static string Escape(string value) + { + if (string.IsNullOrEmpty(value)) + { + return ""; + } + + return value.Replace('\t', ' ').Replace('\n', ' ').Replace('\r', ' '); + } +} diff --git a/IdleSpectator/mod.json b/IdleSpectator/mod.json index f2d6d8e..900a41f 100644 --- a/IdleSpectator/mod.json +++ b/IdleSpectator/mod.json @@ -1,7 +1,7 @@ { "name": "IdleSpectator", "author": "dazed", - "version": "0.16.10", + "version": "0.16.11", "description": "AFK Idle Spectator (I) + Lore (L). Event-centered scene director + happiness Activity/Life logging.", "GUID": "com.dazed.idlespectator" }