- Update building event patches to include completion notifications - Introduce earthquake event handling for ongoing seismic activity - Modify event catalog to reflect changes in ambient interest settings - Add new harness scenarios for library asset label validation - Increment version to 0.28.17 in mod.json
928 lines
37 KiB
C#
928 lines
37 KiB
C#
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Discovers event-relevant mutation paths from live game assemblies and AssetManager.
|
|
/// Writes TSVs under .harness/ for the idle-camera coverage plan.
|
|
/// </summary>
|
|
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<string> AlreadyWired =
|
|
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
"Actor.changeHappiness",
|
|
"Actor.checkHappinessChangeFromDeathEvent",
|
|
"Actor.birthEvent",
|
|
"Actor.becomeLoversWith",
|
|
"Actor.setBestFriend",
|
|
"Actor.newKillAction",
|
|
"Actor.die",
|
|
"Actor.setTask",
|
|
"Actor.setLover",
|
|
"Actor.addChild",
|
|
"Actor.addChildSimple",
|
|
"Actor.leavePlot",
|
|
"Actor.setPlot",
|
|
"Actor.addTrait",
|
|
"Actor.removeTrait",
|
|
"Actor.generatePhenotypeAndShade",
|
|
"Actor.setDecisionCooldown",
|
|
"FamilyManager.newFamily",
|
|
"FamilyManager.removeObject",
|
|
"ActorManager.createBabyActorFromData",
|
|
"PlotManager.newPlot",
|
|
"PlotManager.cancelPlot",
|
|
"WarManager.newWar",
|
|
"WarManager.endWar",
|
|
"BookManager.newBook",
|
|
"BookManager.generateNewBook",
|
|
"BookManager.burnBook",
|
|
"WorldAgeManager.startNextAge",
|
|
"Earthquake.startQuake",
|
|
"Earthquake.spawnQuake",
|
|
"CultureManager.newCulture",
|
|
"ReligionManager.newReligion",
|
|
"LanguageManager.newLanguage",
|
|
"ClanManager.newClan",
|
|
"ArmyManager.newArmy",
|
|
"AllianceManager.newAlliance",
|
|
"CityManager.newCity",
|
|
"KingdomManager.makeNewCivKingdom",
|
|
"Family.setAlpha",
|
|
"Plot.finishPlot",
|
|
"ItemManager.generateItem",
|
|
"ItemManager.newItem",
|
|
"SubspeciesManager.newSpecies",
|
|
"SubspeciesManager.addRandomTraitFromBiomeToSubspecies",
|
|
"SubspeciesManager.addTraitsFromBiomeToSubspecies",
|
|
"Subspecies.mutateFrom",
|
|
"BehDealDamageToTargetBuilding.execute",
|
|
"BehConsumeTargetBuilding.execute",
|
|
"Building.startDestroyBuilding",
|
|
"Building.completeConstruction",
|
|
"BehBoatTransportUnloadUnits.execute",
|
|
"BehBoatMakeTrade.execute",
|
|
"BehBoatTransportDoLoading.execute",
|
|
"BehFindLover.execute",
|
|
"BehFamilyGroupNew.execute",
|
|
"BehFamilyGroupJoin.execute",
|
|
"BehFamilyGroupLeave.execute",
|
|
"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",
|
|
"CombatActionLibrary.tryToCastSpell",
|
|
"PlayerControl.clickedFinal",
|
|
"WorldLaws.enable",
|
|
"WorldLawAsset.toggle",
|
|
"WorldLogMessageExtensions.add",
|
|
"BaseSimObject.addNewStatusEffect",
|
|
"Status.finish",
|
|
"StatusManager.newStatus",
|
|
"InterestFeeds.OnWorldLogMessage",
|
|
"InterestFeeds.OnStatusChange",
|
|
"InterestFeeds.OnActivityNote",
|
|
"InterestFeeds.OnChronicleMilestone",
|
|
"BattleKeeperManager.addUnitKilled",
|
|
"WarInterestFeed.Tick",
|
|
"PlotInterestFeed.Tick",
|
|
"DeferredInterestFeed",
|
|
"WorldActivityScanner.FindHottestBattle",
|
|
"SpeciesDiscovery",
|
|
// Covered by primary owners (birth / die / trait / city / WorldLog).
|
|
"ActorManager.spawnNewUnit",
|
|
"ActorManager.spawnNewUnitByPlayer",
|
|
"Actor.dieSimpleNone",
|
|
"Actor.dieAndDestroy",
|
|
"Actor.addChildren",
|
|
"Actor.removeTraits",
|
|
"Actor.addInjuryTrait",
|
|
"Actor.applyForcedKingdomTrait",
|
|
"Actor.generateRandomSpawnTraits",
|
|
"Actor.traitModifiedEvent",
|
|
"KingdomManager.makeNewCivKingdom"
|
|
};
|
|
|
|
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_creates_interest\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<Type>();
|
|
}
|
|
|
|
var managerTypes = new List<Type>();
|
|
var behTypesList = new List<Type>();
|
|
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(SuggestCreatesInterest(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) ? "false" : "true").Append('\t')
|
|
.Append("0").Append('\t')
|
|
.Append("unwired_beh").AppendLine();
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < LibraryFields.Length; i++)
|
|
{
|
|
string field = LibraryFields[i];
|
|
List<string> 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];
|
|
}
|
|
|
|
// Inventory is complete when a live catalog, activity chip, or WorldLog owns the domain.
|
|
// CreatesInterest dials live in catalogs / dossier - not as open mutation gaps.
|
|
libTsv.Append(field).Append('\t')
|
|
.Append(ids.Count).Append('\t')
|
|
.Append(Escape(sample)).Append('\t')
|
|
.Append(LibraryCoverageNote(field)).AppendLine();
|
|
|
|
// Full id lists for priority event catalogs.
|
|
if (!string.IsNullOrEmpty(outputDirectory)
|
|
&& (field == "plots_library"
|
|
|| field == "plot_category_library"
|
|
|| field == "era_library"
|
|
|| field == "book_types"
|
|
|| field == "traits"
|
|
|| field == "buildings"))
|
|
{
|
|
try
|
|
{
|
|
var full = new StringBuilder();
|
|
full.AppendLine("id");
|
|
for (int s = 0; s < ids.Count; s++)
|
|
{
|
|
full.AppendLine(ids[s]);
|
|
}
|
|
|
|
File.WriteAllText(
|
|
Path.Combine(outputDirectory, "library-" + field + ".tsv"),
|
|
full.ToString());
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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";
|
|
bool wired = name == "wars"
|
|
|| name == "units"
|
|
|| name == "subspecies"
|
|
|| name == "plots"
|
|
// Meta genesis Harmony covers new*; era/quake have dedicated wires.
|
|
|| name == "alliances"
|
|
|| name == "clans"
|
|
|| name == "cultures"
|
|
|| name == "religions"
|
|
|| name == "languages"
|
|
|| name == "families"
|
|
|| name == "armies"
|
|
|| name == "books"
|
|
|| name == "cities"
|
|
|| name == "kingdoms"
|
|
|| name == "items"
|
|
|| name == "buildings"
|
|
|| name == "diplomacy"
|
|
|| name == "era_manager"
|
|
|| name == "earthquake_manager";
|
|
managersTsv.Append("World").Append('\t')
|
|
.Append(name).Append('\t')
|
|
.Append("0").Append('\t')
|
|
.Append("0").Append('\t')
|
|
.Append("PollState").Append('\t')
|
|
.Append(wired ? "1" : "0").Append('\t')
|
|
.Append(typeName).AppendLine();
|
|
managerMethods++;
|
|
|
|
if (!wired && value != null)
|
|
{
|
|
gaps++;
|
|
gapsTsv.Append("PollState").Append('\t')
|
|
.Append("World").Append('\t')
|
|
.Append(name).Append('\t')
|
|
.Append("Yes").Append('\t')
|
|
.Append("true").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(SuggestCreatesInterest("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<string>(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("true").Append('\t')
|
|
.Append("0").Append('\t')
|
|
.Append("relation_lifecycle").AppendLine();
|
|
}
|
|
}
|
|
}
|
|
|
|
private static List<string> EnumerateLibraryIds(string fieldName)
|
|
{
|
|
var ids = new List<string>();
|
|
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<MethodInfo>();
|
|
}
|
|
}
|
|
|
|
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 string LibraryCoverageNote(string field)
|
|
{
|
|
switch (field ?? "")
|
|
{
|
|
case "architecture_library":
|
|
case "city_build_orders":
|
|
return "covered_by_building_activity";
|
|
case "kingdoms_traits":
|
|
return "covered_by_trait_meta_feeds";
|
|
case "items_modifiers":
|
|
return "covered_by_item_feed";
|
|
case "combat_action_library":
|
|
return "covered_by_spell_combat_feeds";
|
|
case "loyalty_library":
|
|
case "opinion_library":
|
|
case "knowledge_library":
|
|
case "communication_library":
|
|
case "story_library":
|
|
return "covered_by_worldlog_happiness";
|
|
case "citizen_job_library":
|
|
case "professions":
|
|
case "personalities":
|
|
return "covered_by_activity_dossier";
|
|
default:
|
|
return "partially_or_fully_wired";
|
|
}
|
|
}
|
|
|
|
private static bool IsEventWorthyManager(string typeName, string methodName)
|
|
{
|
|
string t = typeName ?? "";
|
|
string m = methodName ?? "";
|
|
|
|
// Persistence / teardown / UI / FX / transport bookkeeping - not spectator moments.
|
|
if (m.StartsWith("clear", StringComparison.OrdinalIgnoreCase)
|
|
|| m.StartsWith("load", StringComparison.OrdinalIgnoreCase)
|
|
|| m.Equals("setup", StringComparison.OrdinalIgnoreCase)
|
|
|| m.StartsWith("setDirty", StringComparison.OrdinalIgnoreCase)
|
|
|| m.IndexOf("startCollectHistoryData", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| m.IndexOf("setAges", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| m.IndexOf("setAgeTo", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| m.IndexOf("setCurrentSlot", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| m.IndexOf("setDefaultAges", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| m.Equals("setDirty", StringComparison.OrdinalIgnoreCase)
|
|
|| m.Equals("removeObject", StringComparison.OrdinalIgnoreCase)
|
|
|| m.Equals("createNewUnit", StringComparison.OrdinalIgnoreCase)
|
|
|| m.Equals("ClearAllDisposed", StringComparison.OrdinalIgnoreCase)
|
|
|| m.Equals("clearLastYearStats", StringComparison.OrdinalIgnoreCase)
|
|
|| m.Equals("clearAllCitiesLists", StringComparison.OrdinalIgnoreCase)
|
|
|| m.Equals("setAllLinksDirty", StringComparison.OrdinalIgnoreCase)
|
|
|| m.Equals("clearAll", StringComparison.OrdinalIgnoreCase)
|
|
|| m.Equals("clearAndClose", StringComparison.OrdinalIgnoreCase)
|
|
|| m.IndexOf("addRandomTraitFromBiome", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| m.IndexOf("generateModsFor", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| m.IndexOf("newDiplomacyTick", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| m.IndexOf("removeRelationsFor", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| m.IndexOf("removeDeadUnits", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| m.StartsWith("setLanguage", StringComparison.OrdinalIgnoreCase)
|
|
|| m.IndexOf("TextField", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| m.IndexOf("LocalizedText", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| m.IndexOf("Worldnet", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| m.IndexOf("createDB", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| m.IndexOf("loadDB", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| m.IndexOf("loadAutoTester", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| m.IndexOf("addAction", StringComparison.OrdinalIgnoreCase) >= 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (t.IndexOf("LocalizedText", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| t.IndexOf("UnitText", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| t.IndexOf("MapChunk", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| t.IndexOf("TileManager", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| t.IndexOf("DBManager", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| t.IndexOf("InApp", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| t.IndexOf("DelayedActions", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| t.IndexOf("TaxiManager", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| t.IndexOf("Projectile", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| t.IndexOf("DropManager", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| t.IndexOf("ResourceThrow", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| t.IndexOf("EffectDrag", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| t.IndexOf("SignalManager", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| t.IndexOf("CorruptedTrees", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| t.StartsWith("SystemManager", StringComparison.OrdinalIgnoreCase)
|
|
|| t.StartsWith("SimSystemManager", StringComparison.OrdinalIgnoreCase)
|
|
|| t.StartsWith("CoreSystemManager", StringComparison.OrdinalIgnoreCase)
|
|
|| t.StartsWith("MetaSystemManager", StringComparison.OrdinalIgnoreCase)
|
|
|| t.StartsWith("BaseSystemManager", StringComparison.OrdinalIgnoreCase)
|
|
|| 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"))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool IsEventWorthyActor(string methodName)
|
|
{
|
|
string m = methodName ?? "";
|
|
// Pure queries / counters / trait cache churn - not camera events.
|
|
if (m.StartsWith("has", StringComparison.OrdinalIgnoreCase)
|
|
|| m.StartsWith("is", StringComparison.OrdinalIgnoreCase)
|
|
|| m.StartsWith("get", StringComparison.OrdinalIgnoreCase)
|
|
|| m.StartsWith("calc", StringComparison.OrdinalIgnoreCase)
|
|
|| m.StartsWith("check", StringComparison.OrdinalIgnoreCase)
|
|
|| m.StartsWith("count", StringComparison.OrdinalIgnoreCase)
|
|
|| m.StartsWith("sort", StringComparison.OrdinalIgnoreCase)
|
|
|| m.StartsWith("increase", StringComparison.OrdinalIgnoreCase)
|
|
|| m.StartsWith("decrease", StringComparison.OrdinalIgnoreCase)
|
|
|| m.StartsWith("u2_", StringComparison.OrdinalIgnoreCase)
|
|
|| m.StartsWith("inOwn", StringComparison.OrdinalIgnoreCase)
|
|
|| m.IndexOf("Nutrition", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| m.Equals("clearTraits", StringComparison.OrdinalIgnoreCase)
|
|
|| m.Equals("clearTraitCache", StringComparison.OrdinalIgnoreCase)
|
|
|| m.Equals("clearHomeBuilding", StringComparison.OrdinalIgnoreCase)
|
|
|| m.Equals("setHomeBuilding", StringComparison.OrdinalIgnoreCase)
|
|
|| m.Equals("setClan", StringComparison.OrdinalIgnoreCase)
|
|
|| m.Equals("setArmy", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
private static bool IsEventWorthyBeh(string typeName)
|
|
{
|
|
// Beh pulses are covered by Actor.setTask → OnActivityNote / warm activity scanner.
|
|
// Only leave a gap when a Beh is a distinct narrative feed not on the activity path.
|
|
_ = typeName;
|
|
return false;
|
|
}
|
|
|
|
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 SuggestCreatesInterest(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 "true";
|
|
}
|
|
|
|
return "false";
|
|
}
|
|
|
|
private static string Escape(string value)
|
|
{
|
|
if (string.IsNullOrEmpty(value))
|
|
{
|
|
return "";
|
|
}
|
|
|
|
return value.Replace('\t', ' ').Replace('\n', ' ').Replace('\r', ' ');
|
|
}
|
|
}
|