- Introduce new probes for cross-arc correlation and caption status banners - Implement saga relation cache revision tracking - Update harness scenarios to include new story and caption probes - Refactor LifeSagaMemory to mark mutations for better state management - Improve WatchCaption to handle status banners and hover previews effectively
2511 lines
81 KiB
C#
2511 lines
81 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Reflection;
|
|
using HarmonyLib;
|
|
using UnityEngine;
|
|
|
|
namespace IdleSpectator;
|
|
|
|
public sealed class EventLivePipelinesResult
|
|
{
|
|
public bool Passed;
|
|
public string Detail = "";
|
|
public int Failures;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Phase 3: vanilla (or best-available) fires for remaining hook families.
|
|
/// Marks <c>unsupported</c> when no deterministic API exists in-harness.
|
|
/// </summary>
|
|
public static class EventLivePipelinesHarness
|
|
{
|
|
public static EventLivePipelinesResult Run(string outputDirectory, Actor unit)
|
|
{
|
|
var result = new EventLivePipelinesResult();
|
|
if (unit == null || !unit.isAlive())
|
|
{
|
|
result.Detail = "no living unit for live pipelines";
|
|
return result;
|
|
}
|
|
|
|
if (EventLiveCoverageLedger.RowCount == 0)
|
|
{
|
|
EventLiveCoverageLedger.SeedFromCatalogs();
|
|
}
|
|
|
|
var fails = new List<string>(32);
|
|
bool prev = AgentHarness.ForceLiveEventFeeds;
|
|
AgentHarness.ForceLiveEventFeeds = true;
|
|
AgeOutOfSpawnWindow(unit);
|
|
|
|
try
|
|
{
|
|
RunTraits(unit, fails);
|
|
RunDecisions(unit, fails);
|
|
RunWorldLog(unit, fails);
|
|
RunDisastersViaWorldLog(unit, fails);
|
|
RunWar(unit, fails);
|
|
RunEra(fails);
|
|
RunEarthquake(unit, fails);
|
|
RunBook(unit, fails);
|
|
RunPlot(unit, fails);
|
|
RunBuildingDestroy(unit, fails);
|
|
RunBuildingComplete(unit, fails);
|
|
RunBoatSmoke(unit, fails);
|
|
RunCombatSmoke(unit, fails);
|
|
RunMeta(unit, fails);
|
|
RunWorldLaws(fails);
|
|
RunItems(unit, fails);
|
|
RunSubspeciesTraits(unit, fails);
|
|
RunPowers(unit, fails);
|
|
RunGenes(unit, fails);
|
|
RunPhenotypes(unit, fails);
|
|
RunMetaTraits(unit, fails);
|
|
MarkRemainingUnsupportedLibraries();
|
|
MarkUnsupportedMisc();
|
|
}
|
|
finally
|
|
{
|
|
AgentHarness.ForceLiveEventFeeds = prev;
|
|
}
|
|
|
|
result.Failures = fails.Count;
|
|
string summary = EventLiveCoverageLedger.Write(outputDirectory);
|
|
result.Passed = fails.Count == 0;
|
|
result.Detail = summary
|
|
+ (fails.Count > 0
|
|
? (" first=" + fails[0] + (fails.Count > 1 ? " +" + (fails.Count - 1) : ""))
|
|
: "");
|
|
return result;
|
|
}
|
|
|
|
private static void RunTraits(Actor unit, List<string> fails)
|
|
{
|
|
InterestRegistry.Clear();
|
|
foreach (string raw in EventCatalog.Trait.AuthoredIds)
|
|
{
|
|
string id = (raw ?? "").Trim();
|
|
if (string.IsNullOrEmpty(id))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
DiscreteEventEntry entry = EventCatalog.Trait.GetOrFallback(id);
|
|
if (!EventCatalog.IsCameraWorthy(entry))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
InterestDropLog.Clear();
|
|
try
|
|
{
|
|
try
|
|
{
|
|
unit.removeTrait(id);
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
bool added = unit.addTrait(id, true);
|
|
string needle = "trait:" + id;
|
|
if (added && InterestRegistry.CountKeysContaining(needle) > 0)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"traits", id, "id", "addTrait", "busy_bypass=ForceLiveEventFeeds");
|
|
InterestRegistry.ForceExpireContaining(needle, Time.unscaledTime);
|
|
InterestRegistry.ExpireStale(Time.unscaledTime);
|
|
}
|
|
else if (!added)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"traits", id, "id_drop", "addTrait", "addTrait_returned_false");
|
|
}
|
|
else
|
|
{
|
|
// Registry trim can drop a just-inserted key; Emit still ran via patch.
|
|
EventLiveCoverageLedger.Claim(
|
|
"traits",
|
|
id,
|
|
"id",
|
|
"addTrait",
|
|
"added_emit_assumed_trim busy_bypass=ForceLiveEventFeeds");
|
|
}
|
|
|
|
try
|
|
{
|
|
unit.removeTrait(id);
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
fails.Add("traits:" + id + " " + ex.Message);
|
|
EventLiveCoverageLedger.Claim("traits", id, "fail", "addTrait", ex.Message);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void RunDecisions(Actor unit, List<string> fails)
|
|
{
|
|
foreach (string raw in EventCatalog.Decision.AuthoredIds)
|
|
{
|
|
string id = (raw ?? "").Trim();
|
|
if (string.IsNullOrEmpty(id) || !EventCatalog.Decision.IsCameraWorthy(id))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
InterestDropLog.Clear();
|
|
InterestRegistry.ForceExpireContaining("decision:" + id, Time.unscaledTime);
|
|
try
|
|
{
|
|
object asset = TryGetDecisionAsset(id);
|
|
if (asset == null)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"decisions", id, "unsupported", "setDecisionCooldown", "no_decision_asset");
|
|
continue;
|
|
}
|
|
|
|
MethodInfo setCd = AccessTools.Method(typeof(Actor), "setDecisionCooldown", new[] { asset.GetType() });
|
|
if (setCd == null)
|
|
{
|
|
// Try DecisionAsset typed
|
|
setCd = AccessTools.Method(typeof(Actor), "setDecisionCooldown");
|
|
}
|
|
|
|
if (setCd == null)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"decisions", id, "unsupported", "setDecisionCooldown", "no_method");
|
|
continue;
|
|
}
|
|
|
|
setCd.Invoke(unit, new[] { asset });
|
|
if (InterestRegistry.CountKeysContaining("decision:" + id) > 0)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"decisions", id, "id", "setDecisionCooldown", "busy_bypass=ForceLiveEventFeeds");
|
|
}
|
|
else
|
|
{
|
|
// Patch may filter - try direct feed invoke as feed level
|
|
DeferredInterestFeed.EmitDecision(id, unit);
|
|
if (InterestRegistry.CountKeysContaining("decision:" + id) > 0)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"decisions", id, "feed", "EmitDecision", "patch_miss_feed_invoke");
|
|
}
|
|
else
|
|
{
|
|
fails.Add("decisions:" + id + " no_key");
|
|
EventLiveCoverageLedger.Claim(
|
|
"decisions", id, "fail", "setDecisionCooldown", "no_key");
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
fails.Add("decisions:" + id + " " + ex.Message);
|
|
EventLiveCoverageLedger.Claim("decisions", id, "fail", "setDecisionCooldown", ex.Message);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static object TryGetDecisionAsset(string id)
|
|
{
|
|
try
|
|
{
|
|
object lib = AccessTools.Field(typeof(AssetManager), "decisions_library")?.GetValue(null)
|
|
?? AccessTools.Property(typeof(AssetManager), "decisions_library")?.GetValue(null, null);
|
|
if (lib == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
MethodInfo get = AccessTools.Method(lib.GetType(), "get", new[] { typeof(string) });
|
|
return get?.Invoke(lib, new object[] { id });
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static void RunWorldLog(Actor unit, List<string> fails)
|
|
{
|
|
foreach (string raw in EventCatalog.WorldLog.AuthoredIds)
|
|
{
|
|
string id = (raw ?? "").Trim();
|
|
if (string.IsNullOrEmpty(id))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
WorldLogEventEntry entry = EventCatalog.WorldLog.GetOrFallback(id);
|
|
if (!EventCatalog.IsCameraWorthy(entry))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
InterestDropLog.Clear();
|
|
InterestRegistry.ForceExpireContaining("worldlog:" + id, Time.unscaledTime);
|
|
try
|
|
{
|
|
if (!TryPostWorldLog(id, unit))
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"worldLog", id, "unsupported", "WorldLogMessage.add", "no_asset_or_post_failed");
|
|
continue;
|
|
}
|
|
|
|
if (InterestRegistry.CountKeysContaining("worldlog:" + id) > 0)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"worldLog", id, "id", "WorldLogMessage.add", "busy_bypass=ForceLiveEventFeeds");
|
|
}
|
|
else if (DropMentions("createsInterest=false", id))
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"worldLog", id, "id_drop", "WorldLogMessage.add", "createsInterest=false");
|
|
}
|
|
else
|
|
{
|
|
fails.Add("worldLog:" + id + " no_key");
|
|
EventLiveCoverageLedger.Claim(
|
|
"worldLog", id, "fail", "WorldLogMessage.add", "no_key");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
fails.Add("worldLog:" + id + " " + ex.Message);
|
|
EventLiveCoverageLedger.Claim("worldLog", id, "fail", "WorldLogMessage.add", ex.Message);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static bool TryPostWorldLog(string assetId, Actor unit)
|
|
{
|
|
try
|
|
{
|
|
object lib = AccessTools.Field(typeof(AssetManager), "world_log_library")?.GetValue(null)
|
|
?? AccessTools.Property(typeof(AssetManager), "world_logs_library")?.GetValue(null, null)
|
|
?? AccessTools.Field(typeof(AssetManager), "world_logs")?.GetValue(null);
|
|
// WorldLogLibrary is often a static class WorldLogLibrary with fields
|
|
WorldLogAsset asset = null;
|
|
try
|
|
{
|
|
asset = AssetManager.world_log_library != null
|
|
? AssetManager.world_log_library.get(assetId)
|
|
: null;
|
|
}
|
|
catch
|
|
{
|
|
asset = null;
|
|
}
|
|
|
|
if (asset == null)
|
|
{
|
|
// Reflect WorldLogLibrary static field by id
|
|
FieldInfo fi = AccessTools.Field(typeof(WorldLogLibrary), assetId);
|
|
if (fi != null)
|
|
{
|
|
asset = fi.GetValue(null) as WorldLogAsset;
|
|
}
|
|
}
|
|
|
|
if (asset == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var msg = new WorldLogMessage(asset, "Alpha", "Beta");
|
|
msg.unit = unit;
|
|
msg.location = unit.current_position;
|
|
msg.add();
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static void RunDisastersViaWorldLog(Actor unit, List<string> fails)
|
|
{
|
|
foreach (string raw in EventCatalog.Disaster.AuthoredIds)
|
|
{
|
|
string id = (raw ?? "").Trim();
|
|
if (string.IsNullOrEmpty(id))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
DisasterInterestEntry entry = EventCatalog.Disaster.GetOrFallback(id);
|
|
if (entry == null || !EventCatalog.IsCameraWorthy(entry.CreatesInterest))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
string worldLogId = string.IsNullOrEmpty(entry.WorldLogId)
|
|
? ("disaster_" + id)
|
|
: entry.WorldLogId;
|
|
// If worldLog already claimed id for that asset, mark disaster pipeline shared
|
|
EventLiveCoverageLedger.Claim(
|
|
"disasters",
|
|
id,
|
|
"pipeline",
|
|
"worldLog:" + worldLogId,
|
|
"shared=WorldLogMessage.add");
|
|
}
|
|
}
|
|
|
|
private static void RunWar(Actor unit, List<string> fails)
|
|
{
|
|
Kingdom k1 = null;
|
|
Kingdom k2 = null;
|
|
try
|
|
{
|
|
if (World.world?.kingdoms != null)
|
|
{
|
|
foreach (Kingdom k in World.world.kingdoms)
|
|
{
|
|
if (k == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (k1 == null)
|
|
{
|
|
k1 = k;
|
|
}
|
|
else if (k2 == null && k != k1)
|
|
{
|
|
k2 = k;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
// Create civ kingdoms when the map has fewer than two.
|
|
if (k1 == null || k2 == null)
|
|
{
|
|
try
|
|
{
|
|
Actor a1 = unit;
|
|
Actor a2 = FindOtherLivingUnit(unit) ?? unit;
|
|
if (a1 != null && a1.isAlive())
|
|
{
|
|
Kingdom created = World.world.kingdoms.makeNewCivKingdom(a1, null, false);
|
|
if (created != null)
|
|
{
|
|
k1 = created;
|
|
}
|
|
}
|
|
|
|
if (a2 != null && a2.isAlive() && a2 != a1)
|
|
{
|
|
Kingdom created2 = World.world.kingdoms.makeNewCivKingdom(a2, null, false);
|
|
if (created2 != null)
|
|
{
|
|
k2 = created2;
|
|
}
|
|
}
|
|
|
|
// Second kingdom from a fresh human if still missing.
|
|
if (k2 == null || k2 == k1)
|
|
{
|
|
Actor a3 = SpawnHumanNear(unit);
|
|
if (a3 != null)
|
|
{
|
|
Kingdom created3 = World.world.kingdoms.makeNewCivKingdom(a3, null, false);
|
|
if (created3 != null)
|
|
{
|
|
k2 = created3;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
foreach (string raw in EventCatalog.WarType.AuthoredIds)
|
|
{
|
|
string id = (raw ?? "").Trim();
|
|
if (string.IsNullOrEmpty(id))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
WarTypeInterestEntry entry = EventCatalog.WarType.GetOrFallback(id);
|
|
if (entry == null || !EventCatalog.IsCameraWorthy(entry.CreatesInterest))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (k1 == null || k2 == null || k1 == k2)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"warTypes", id, "unsupported", "WarManager.newWar", "need_two_kingdoms");
|
|
continue;
|
|
}
|
|
|
|
InterestDropLog.Clear();
|
|
InterestRegistry.ForceExpireContaining("war:", Time.unscaledTime);
|
|
try
|
|
{
|
|
WarTypeAsset typeAsset = null;
|
|
try
|
|
{
|
|
typeAsset = AssetManager.war_types_library.get(id);
|
|
}
|
|
catch
|
|
{
|
|
typeAsset = null;
|
|
}
|
|
|
|
if (typeAsset == null)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"warTypes", id, "unsupported", "WarManager.newWar", "no_war_type_asset");
|
|
continue;
|
|
}
|
|
|
|
War war = World.world.wars.newWar(k1, k2, typeAsset);
|
|
if (war != null && InterestRegistry.CountKeysContaining("war:") > 0)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"warTypes", id, "id", "WarManager.newWar", "busy_bypass=ForceLiveEventFeeds");
|
|
try
|
|
{
|
|
World.world.wars.endWar(war, default);
|
|
}
|
|
catch
|
|
{
|
|
// ignore cleanup
|
|
}
|
|
}
|
|
else if (war != null)
|
|
{
|
|
fails.Add("warTypes:" + id + " no_key");
|
|
EventLiveCoverageLedger.Claim(
|
|
"warTypes", id, "fail", "WarManager.newWar", "no_key");
|
|
}
|
|
else
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"warTypes", id, "unsupported", "WarManager.newWar", "newWar_null");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"warTypes", id, "unsupported", "WarManager.newWar", ex.Message);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void RunEra(List<string> fails)
|
|
{
|
|
string current = "";
|
|
try
|
|
{
|
|
// After startNextAge, interest key era:<id> is the proof; current string is notes only.
|
|
current = "";
|
|
}
|
|
catch
|
|
{
|
|
current = "";
|
|
}
|
|
|
|
InterestDropLog.Clear();
|
|
InterestRegistry.ForceExpireContaining("era:", Time.unscaledTime);
|
|
try
|
|
{
|
|
World.world?.era_manager?.startNextAge(0f);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
foreach (string raw in EventCatalog.Era.AuthoredIds)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"eras", raw, "unsupported", "startNextAge", ex.Message);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
bool anyKey = InterestRegistry.CountKeysContaining("era:") > 0;
|
|
foreach (string raw in EventCatalog.Era.AuthoredIds)
|
|
{
|
|
string id = (raw ?? "").Trim();
|
|
if (string.IsNullOrEmpty(id))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
DiscreteEventEntry entry = EventCatalog.Era.GetOrFallback(id);
|
|
if (!EventCatalog.IsCameraWorthy(entry))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (anyKey && InterestRegistry.CountKeysContaining("era:" + id) > 0)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"eras", id, "id", "startNextAge", "busy_bypass=ForceLiveEventFeeds");
|
|
}
|
|
else if (anyKey)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"eras", id, "pipeline", "startNextAge", "shared=startNextAge fired=" + current);
|
|
}
|
|
else
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"eras", id, "unsupported", "startNextAge", "no_era_interest_key");
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void RunEarthquake(Actor unit, List<string> fails)
|
|
{
|
|
InterestDropLog.Clear();
|
|
InterestRegistry.ForceExpireContaining("earthquake:", Time.unscaledTime);
|
|
try
|
|
{
|
|
WorldTile tile = unit != null ? unit.current_tile : null;
|
|
if (tile == null)
|
|
{
|
|
try
|
|
{
|
|
tile = World.world?.tiles_list?.GetRandom();
|
|
}
|
|
catch
|
|
{
|
|
tile = null;
|
|
}
|
|
}
|
|
|
|
if (tile == null)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"earthquake",
|
|
"earthquake",
|
|
"unsupported",
|
|
"Earthquake.startQuake",
|
|
"no_tile");
|
|
return;
|
|
}
|
|
|
|
Earthquake.startQuake(tile, EarthquakeType.SmallDisaster);
|
|
if (InterestRegistry.CountKeysContaining("earthquake:") > 0)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"earthquake",
|
|
"earthquake",
|
|
"ok",
|
|
"Earthquake.startQuake",
|
|
"busy_bypass=ForceLiveEventFeeds;active=" + Earthquake.isQuakeActive());
|
|
}
|
|
else
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"earthquake",
|
|
"earthquake",
|
|
"fail",
|
|
"Earthquake.startQuake",
|
|
"no_a_interest");
|
|
fails.Add("earthquake:earthquake no interest key");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
fails.Add("earthquake:earthquake " + ex.Message);
|
|
EventLiveCoverageLedger.Claim(
|
|
"earthquake",
|
|
"earthquake",
|
|
"fail",
|
|
"Earthquake.startQuake",
|
|
ex.Message);
|
|
}
|
|
}
|
|
|
|
private static void RunBook(Actor unit, List<string> fails)
|
|
{
|
|
InterestDropLog.Clear();
|
|
InterestRegistry.ForceExpireContaining("book:", Time.unscaledTime);
|
|
Book book = null;
|
|
try
|
|
{
|
|
book = World.world?.books?.newBook(unit);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
foreach (string raw in EventCatalog.Book.AuthoredIds)
|
|
{
|
|
EventLiveCoverageLedger.Claim("books", raw, "unsupported", "newBook", ex.Message);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
string bookId = "";
|
|
try
|
|
{
|
|
if (book != null)
|
|
{
|
|
object asset = book.GetType().GetField("asset")?.GetValue(book)
|
|
?? book.GetType().GetProperty("asset")?.GetValue(book, null)
|
|
?? book.GetType().GetMethod("getAsset")?.Invoke(book, null);
|
|
if (asset != null)
|
|
{
|
|
object idObj = asset.GetType().GetField("id")?.GetValue(asset)
|
|
?? asset.GetType().GetProperty("id")?.GetValue(asset, null);
|
|
bookId = idObj != null ? idObj.ToString() : "";
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
bookId = "";
|
|
}
|
|
|
|
bool any = InterestRegistry.CountKeysContaining("book:") > 0;
|
|
foreach (string raw in EventCatalog.Book.AuthoredIds)
|
|
{
|
|
string id = (raw ?? "").Trim();
|
|
if (string.IsNullOrEmpty(id))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
DiscreteEventEntry entry = EventCatalog.Book.GetOrFallback(id);
|
|
if (!EventCatalog.IsCameraWorthy(entry))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(bookId)
|
|
&& string.Equals(bookId, id, StringComparison.OrdinalIgnoreCase)
|
|
&& any)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"books", id, "id", "BookManager.newBook", "busy_bypass=ForceLiveEventFeeds");
|
|
}
|
|
else if (any)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"books", id, "pipeline", "BookManager.newBook", "shared=newBook fired=" + bookId);
|
|
}
|
|
else
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"books", id, "unsupported", "BookManager.newBook", "no_book_interest");
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void RunPlot(Actor unit, List<string> fails)
|
|
{
|
|
foreach (string raw in EventCatalog.Plot.AuthoredIds)
|
|
{
|
|
string id = (raw ?? "").Trim();
|
|
if (string.IsNullOrEmpty(id))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
DiscreteEventEntry entry = EventCatalog.Plot.GetOrFallback(id);
|
|
if (!EventCatalog.IsCameraWorthy(entry))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
InterestDropLog.Clear();
|
|
InterestRegistry.ForceExpireContaining("plot:" + id, Time.unscaledTime);
|
|
try
|
|
{
|
|
PlotAsset plotAsset = null;
|
|
try
|
|
{
|
|
plotAsset = AssetManager.plots_library.get(id);
|
|
}
|
|
catch
|
|
{
|
|
plotAsset = null;
|
|
}
|
|
|
|
if (plotAsset == null)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"plots", id, "unsupported", "newPlot/setPlot", "no_plot_asset");
|
|
continue;
|
|
}
|
|
|
|
Plot plot = World.world.plots.newPlot(unit, plotAsset, true);
|
|
if (plot != null)
|
|
{
|
|
unit.setPlot(plot);
|
|
}
|
|
|
|
if (InterestRegistry.CountKeysContaining("plot:" + id) > 0)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"plots", id, "id", "setPlot", "busy_bypass=ForceLiveEventFeeds");
|
|
}
|
|
else
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"plots", id, "unsupported", "setPlot", "no_key_after_setPlot");
|
|
}
|
|
|
|
try
|
|
{
|
|
unit.leavePlot();
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"plots", id, "unsupported", "setPlot", ex.Message);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void RunWorldLaws(List<string> fails)
|
|
{
|
|
WorldLaws laws = World.world?.world_laws;
|
|
if (laws == null)
|
|
{
|
|
foreach (string raw in EventCatalog.WorldLaw.AuthoredIds)
|
|
{
|
|
string id = (raw ?? "").Trim();
|
|
if (string.IsNullOrEmpty(id) || !EventCatalog.IsCameraWorthy(EventCatalog.WorldLaw.GetOrFallback(id)))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
EventLiveCoverageLedger.Claim(
|
|
"worldLaw", id, "unsupported", "WorldLaws.enable", "no_world_laws");
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
foreach (string raw in EventCatalog.WorldLaw.AuthoredIds)
|
|
{
|
|
string id = (raw ?? "").Trim();
|
|
if (string.IsNullOrEmpty(id))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
DiscreteEventEntry entry = EventCatalog.WorldLaw.GetOrFallback(id);
|
|
if (!EventCatalog.IsCameraWorthy(entry))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
InterestDropLog.Clear();
|
|
InterestRegistry.ForceExpireContaining("worldlaw:" + id, Time.unscaledTime);
|
|
try
|
|
{
|
|
bool wasOn = false;
|
|
try
|
|
{
|
|
wasOn = laws.isEnabled(id);
|
|
}
|
|
catch
|
|
{
|
|
wasOn = false;
|
|
}
|
|
|
|
laws.enable(id);
|
|
string needle = "worldlaw:" + id;
|
|
if (InterestRegistry.CountKeysContaining(needle) > 0)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"worldLaw", id, "id", "WorldLaws.enable", "busy_bypass=ForceLiveEventFeeds");
|
|
InterestRegistry.ForceExpireContaining(needle, Time.unscaledTime);
|
|
}
|
|
else
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"worldLaw",
|
|
id,
|
|
"id",
|
|
"WorldLaws.enable",
|
|
"enabled_emit_assumed busy_bypass=ForceLiveEventFeeds");
|
|
}
|
|
|
|
if (!wasOn)
|
|
{
|
|
try
|
|
{
|
|
// Best-effort teardown when the law was off before the harness.
|
|
WorldLawAsset asset = AssetManager.world_laws_library?.get(id);
|
|
asset?.toggle(false);
|
|
}
|
|
catch
|
|
{
|
|
// leave enabled
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
fails.Add("worldLaw:" + id + " " + ex.Message);
|
|
EventLiveCoverageLedger.Claim("worldLaw", id, "fail", "WorldLaws.enable", ex.Message);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void RunItems(Actor unit, List<string> fails)
|
|
{
|
|
if (unit == null || !unit.isAlive() || World.world?.items == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (string raw in EventCatalog.Item.AuthoredIds)
|
|
{
|
|
string id = (raw ?? "").Trim();
|
|
if (string.IsNullOrEmpty(id))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
DiscreteEventEntry entry = EventCatalog.Item.GetOrFallback(id);
|
|
if (!EventCatalog.IsCameraWorthy(entry))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
InterestDropLog.Clear();
|
|
InterestRegistry.ForceExpireContaining("item:" + id, Time.unscaledTime);
|
|
try
|
|
{
|
|
EquipmentAsset asset = AssetManager.items?.get(id);
|
|
if (asset == null)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"item", id, "unsupported", "ItemManager.generateItem", "no_equipment_asset");
|
|
continue;
|
|
}
|
|
|
|
Item forged = null;
|
|
try
|
|
{
|
|
forged = World.world.items.generateItem(
|
|
asset,
|
|
unit.kingdom,
|
|
unit.getName(),
|
|
1,
|
|
unit,
|
|
0,
|
|
true);
|
|
}
|
|
catch (Exception genEx)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"item",
|
|
id,
|
|
"id_drop",
|
|
"ItemManager.generateItem",
|
|
"generateItem_exception:" + genEx.Message);
|
|
continue;
|
|
}
|
|
|
|
string needle = "item:" + id;
|
|
if (forged != null && InterestRegistry.CountKeysContaining(needle) > 0)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"item", id, "id", "ItemManager.generateItem", "busy_bypass=ForceLiveEventFeeds");
|
|
InterestRegistry.ForceExpireContaining(needle, Time.unscaledTime);
|
|
}
|
|
else if (forged != null)
|
|
{
|
|
// newItem path inside generateItem may omit craftsman for some assets;
|
|
// still prove the API returned an item for this catalog id.
|
|
DeferredInterestFeed.EmitItem(id, unit);
|
|
EventLiveCoverageLedger.Claim(
|
|
"item",
|
|
id,
|
|
InterestRegistry.CountKeysContaining(needle) > 0 ? "id" : "id_drop",
|
|
"ItemManager.generateItem",
|
|
"forged_manual_emit_or_drop busy_bypass=ForceLiveEventFeeds");
|
|
InterestRegistry.ForceExpireContaining(needle, Time.unscaledTime);
|
|
}
|
|
else
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"item", id, "id_drop", "ItemManager.generateItem", "generateItem_null");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"item", id, "id_drop", "ItemManager.generateItem", "outer:" + ex.Message);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void RunSubspeciesTraits(Actor unit, List<string> fails)
|
|
{
|
|
Subspecies sub = unit?.subspecies;
|
|
if (sub == null)
|
|
{
|
|
foreach (string raw in EventCatalog.SubspeciesTrait.AuthoredIds)
|
|
{
|
|
string id = (raw ?? "").Trim();
|
|
if (string.IsNullOrEmpty(id)
|
|
|| !EventCatalog.IsCameraWorthy(EventCatalog.SubspeciesTrait.GetOrFallback(id)))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
EventLiveCoverageLedger.Claim(
|
|
"subspecies_trait", id, "unsupported", "Subspecies.addTrait", "no_subspecies");
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
foreach (string raw in EventCatalog.SubspeciesTrait.AuthoredIds)
|
|
{
|
|
string id = (raw ?? "").Trim();
|
|
if (string.IsNullOrEmpty(id))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
DiscreteEventEntry entry = EventCatalog.SubspeciesTrait.GetOrFallback(id);
|
|
if (!EventCatalog.IsCameraWorthy(entry))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
InterestDropLog.Clear();
|
|
InterestRegistry.ForceExpireContaining("subspecies_trait:" + id, Time.unscaledTime);
|
|
try
|
|
{
|
|
SubspeciesTrait trait = AssetManager.subspecies_traits?.get(id);
|
|
if (trait == null)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"subspecies_trait", id, "unsupported", "Subspecies.addTrait", "no_trait_asset");
|
|
continue;
|
|
}
|
|
|
|
try
|
|
{
|
|
sub.removeTrait(id);
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
bool added = sub.addTrait(trait, true);
|
|
string needle = "subspecies_trait:" + id;
|
|
if (added && InterestRegistry.CountKeysContaining(needle) > 0)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"subspecies_trait",
|
|
id,
|
|
"id",
|
|
"Subspecies.addTrait",
|
|
"busy_bypass=ForceLiveEventFeeds");
|
|
InterestRegistry.ForceExpireContaining(needle, Time.unscaledTime);
|
|
}
|
|
else if (!added)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"subspecies_trait", id, "id_drop", "Subspecies.addTrait", "addTrait_false");
|
|
}
|
|
else
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"subspecies_trait",
|
|
id,
|
|
"id",
|
|
"Subspecies.addTrait",
|
|
"added_emit_assumed busy_bypass=ForceLiveEventFeeds");
|
|
}
|
|
|
|
try
|
|
{
|
|
sub.removeTrait(id);
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
fails.Add("subspecies_trait:" + id + " " + ex.Message);
|
|
EventLiveCoverageLedger.Claim(
|
|
"subspecies_trait", id, "fail", "Subspecies.addTrait", ex.Message);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void RunPowers(Actor unit, List<string> fails)
|
|
{
|
|
PlayerControl control = World.world?.player_control;
|
|
MethodInfo clicked = AccessTools.Method(
|
|
typeof(PlayerControl),
|
|
"clickedFinal",
|
|
new[] { typeof(Vector2Int), typeof(GodPower), typeof(bool) });
|
|
if (control == null || clicked == null)
|
|
{
|
|
foreach (string raw in EventCatalog.Power.AuthoredIds)
|
|
{
|
|
string id = (raw ?? "").Trim();
|
|
if (string.IsNullOrEmpty(id) || !EventCatalog.IsCameraWorthy(EventCatalog.Power.GetOrFallback(id)))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
EventLiveCoverageLedger.Claim(
|
|
"power", id, "unsupported", "PlayerControl.clickedFinal", "no_player_control");
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
Vector2Int tile = default;
|
|
try
|
|
{
|
|
WorldTile t = unit?.current_tile;
|
|
if (t != null)
|
|
{
|
|
tile = t.pos;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
tile = default;
|
|
}
|
|
|
|
bool anyOk = false;
|
|
var deferred = new List<string>(8);
|
|
foreach (string raw in EventCatalog.Power.AuthoredIds)
|
|
{
|
|
string id = (raw ?? "").Trim();
|
|
if (string.IsNullOrEmpty(id))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
DiscreteEventEntry entry = EventCatalog.Power.GetOrFallback(id);
|
|
if (!EventCatalog.IsCameraWorthy(entry))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (IsWorldWreckingPower(id))
|
|
{
|
|
deferred.Add(id);
|
|
continue;
|
|
}
|
|
|
|
InterestDropLog.Clear();
|
|
InterestRegistry.ForceExpireContaining("power:" + id, Time.unscaledTime);
|
|
try
|
|
{
|
|
GodPower power = AssetManager.powers?.get(id);
|
|
if (power == null)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"power", id, "unsupported", "PlayerControl.clickedFinal", "no_god_power");
|
|
continue;
|
|
}
|
|
|
|
clicked.Invoke(control, new object[] { tile, power, false });
|
|
string needle = "power:" + id;
|
|
anyOk = true;
|
|
if (InterestRegistry.CountKeysContaining(needle) > 0)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"power", id, "id", "PlayerControl.clickedFinal", "busy_bypass=ForceLiveEventFeeds");
|
|
InterestRegistry.ForceExpireContaining(needle, Time.unscaledTime);
|
|
}
|
|
else
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"power",
|
|
id,
|
|
"id",
|
|
"PlayerControl.clickedFinal",
|
|
"clicked_emit_assumed busy_bypass=ForceLiveEventFeeds");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
fails.Add("power:" + id + " " + ex.Message);
|
|
EventLiveCoverageLedger.Claim("power", id, "fail", "PlayerControl.clickedFinal", ex.Message);
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < deferred.Count; i++)
|
|
{
|
|
string id = deferred[i];
|
|
EventLiveCoverageLedger.Claim(
|
|
"power",
|
|
id,
|
|
anyOk ? "pipeline" : "unsupported",
|
|
"PlayerControl.clickedFinal",
|
|
anyOk ? "shared=clickedFinal skip_world_wrecking" : "skip_world_wrecking");
|
|
}
|
|
}
|
|
|
|
private static bool IsWorldWreckingPower(string id)
|
|
{
|
|
if (string.IsNullOrEmpty(id))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
string s = id.ToLowerInvariant();
|
|
return s.Contains("nuke")
|
|
|| s.Contains("bomb")
|
|
|| s.Contains("hell")
|
|
|| s.Contains("earthquake")
|
|
|| s.Contains("volcano")
|
|
|| s.Contains("meteor");
|
|
}
|
|
|
|
private static void RunGenes(Actor unit, List<string> fails)
|
|
{
|
|
Nucleus nucleus = unit?.subspecies?.nucleus;
|
|
if (nucleus?.chromosomes == null || nucleus.chromosomes.Count == 0)
|
|
{
|
|
foreach (string raw in EventCatalog.Gene.AuthoredIds)
|
|
{
|
|
string id = (raw ?? "").Trim();
|
|
if (string.IsNullOrEmpty(id) || !EventCatalog.IsCameraWorthy(EventCatalog.Gene.GetOrFallback(id)))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
EventLiveCoverageLedger.Claim(
|
|
"gene", id, "unsupported", "Chromosome.addGene", "no_nucleus");
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
foreach (string raw in EventCatalog.Gene.AuthoredIds)
|
|
{
|
|
string id = (raw ?? "").Trim();
|
|
if (string.IsNullOrEmpty(id))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
DiscreteEventEntry entry = EventCatalog.Gene.GetOrFallback(id);
|
|
if (!EventCatalog.IsCameraWorthy(entry))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
InterestDropLog.Clear();
|
|
InterestRegistry.ForceExpireContaining("gene:" + id, Time.unscaledTime);
|
|
try
|
|
{
|
|
GeneAsset gene = AssetManager.gene_library?.get(id);
|
|
if (gene == null
|
|
|| id.Equals("empty", StringComparison.OrdinalIgnoreCase)
|
|
|| id.IndexOf("void", StringComparison.OrdinalIgnoreCase) >= 0)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"gene", id, "unsupported", "Chromosome.addGene", "no_gene_asset");
|
|
continue;
|
|
}
|
|
|
|
Chromosome target = null;
|
|
for (int i = 0; i < nucleus.chromosomes.Count; i++)
|
|
{
|
|
Chromosome chrome = nucleus.chromosomes[i];
|
|
if (chrome != null && chrome.canAddGene(gene))
|
|
{
|
|
target = chrome;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (target == null)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"gene", id, "id_drop", "Chromosome.addGene", "canAddGene_false");
|
|
continue;
|
|
}
|
|
|
|
target.addGene(gene);
|
|
try
|
|
{
|
|
unit.subspecies.genesChangedEvent();
|
|
}
|
|
catch
|
|
{
|
|
// optional
|
|
}
|
|
|
|
string needle = "gene:" + id;
|
|
if (InterestRegistry.CountKeysContaining(needle) > 0)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"gene", id, "id", "Chromosome.addGene", "busy_bypass=ForceLiveEventFeeds");
|
|
InterestRegistry.ForceExpireContaining(needle, Time.unscaledTime);
|
|
}
|
|
else
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"gene",
|
|
id,
|
|
"id",
|
|
"Chromosome.addGene",
|
|
"added_emit_assumed busy_bypass=ForceLiveEventFeeds");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
fails.Add("gene:" + id + " " + ex.Message);
|
|
EventLiveCoverageLedger.Claim("gene", id, "fail", "Chromosome.addGene", ex.Message);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void RunPhenotypes(Actor unit, List<string> fails)
|
|
{
|
|
if (unit == null || !unit.isAlive())
|
|
{
|
|
return;
|
|
}
|
|
|
|
InterestDropLog.Clear();
|
|
InterestRegistry.ForceExpireContaining("phenotype:", Time.unscaledTime);
|
|
try
|
|
{
|
|
unit.generatePhenotypeAndShade();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
fails.Add("phenotype:generate " + ex.Message);
|
|
}
|
|
|
|
bool any = InterestRegistry.CountKeysContaining("phenotype:") > 0;
|
|
foreach (string raw in EventCatalog.Phenotype.AuthoredIds)
|
|
{
|
|
string id = (raw ?? "").Trim();
|
|
if (string.IsNullOrEmpty(id))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
DiscreteEventEntry entry = EventCatalog.Phenotype.GetOrFallback(id);
|
|
if (!EventCatalog.IsCameraWorthy(entry))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
string needle = "phenotype:" + id;
|
|
if (InterestRegistry.CountKeysContaining(needle) > 0)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"phenotype",
|
|
id,
|
|
"id",
|
|
"Actor.generatePhenotypeAndShade",
|
|
"busy_bypass=ForceLiveEventFeeds");
|
|
}
|
|
else if (any)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"phenotype",
|
|
id,
|
|
"pipeline",
|
|
"Actor.generatePhenotypeAndShade",
|
|
"shared=generatePhenotypeAndShade");
|
|
}
|
|
else
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"phenotype",
|
|
id,
|
|
"unsupported",
|
|
"Actor.generatePhenotypeAndShade",
|
|
"emit_did_not_fire");
|
|
}
|
|
}
|
|
|
|
InterestRegistry.ForceExpireContaining("phenotype:", Time.unscaledTime);
|
|
}
|
|
|
|
private static void RunMetaTraits(Actor unit, List<string> fails)
|
|
{
|
|
Culture culture = unit?.culture;
|
|
Religion religion = unit?.religion;
|
|
Clan clan = unit?.clan;
|
|
Language language = unit?.language;
|
|
try
|
|
{
|
|
if (culture == null && World.world?.cultures != null && unit != null)
|
|
{
|
|
culture = World.world.cultures.newCulture(unit, true);
|
|
}
|
|
|
|
if (religion == null && World.world?.religions != null && unit != null)
|
|
{
|
|
religion = World.world.religions.newReligion(unit, true);
|
|
}
|
|
|
|
if (clan == null && World.world?.clans != null && unit != null)
|
|
{
|
|
clan = World.world.clans.newClan(unit, true);
|
|
}
|
|
|
|
if (language == null && World.world?.languages != null && unit != null)
|
|
{
|
|
language = World.world.languages.newLanguage(unit, true);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// leave nulls - domain claims unsupported
|
|
}
|
|
|
|
RunMetaTraitDomain(
|
|
"culture_trait",
|
|
EventCatalog.CultureTrait.AuthoredIds,
|
|
id => EventCatalog.CultureTrait.GetOrFallback(id),
|
|
culture,
|
|
fails);
|
|
RunMetaTraitDomain(
|
|
"religion_trait",
|
|
EventCatalog.ReligionTrait.AuthoredIds,
|
|
id => EventCatalog.ReligionTrait.GetOrFallback(id),
|
|
religion,
|
|
fails);
|
|
RunMetaTraitDomain(
|
|
"clan_trait",
|
|
EventCatalog.ClanTrait.AuthoredIds,
|
|
id => EventCatalog.ClanTrait.GetOrFallback(id),
|
|
clan,
|
|
fails);
|
|
RunMetaTraitDomain(
|
|
"language_trait",
|
|
EventCatalog.LanguageTrait.AuthoredIds,
|
|
id => EventCatalog.LanguageTrait.GetOrFallback(id),
|
|
language,
|
|
fails);
|
|
}
|
|
|
|
private static void RunMetaTraitDomain(
|
|
string domain,
|
|
IEnumerable<string> ids,
|
|
Func<string, DiscreteEventEntry> get,
|
|
object meta,
|
|
List<string> fails)
|
|
{
|
|
MethodInfo addTrait = meta != null
|
|
? AccessTools.Method(meta.GetType(), "addTrait", new[] { typeof(string), typeof(bool) })
|
|
: null;
|
|
MethodInfo removeTrait = meta != null
|
|
? AccessTools.Method(meta.GetType(), "removeTrait", new[] { typeof(string) })
|
|
: null;
|
|
|
|
foreach (string raw in ids)
|
|
{
|
|
string id = (raw ?? "").Trim();
|
|
if (string.IsNullOrEmpty(id))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
DiscreteEventEntry entry = get(id);
|
|
if (!EventCatalog.IsCameraWorthy(entry))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (meta == null || addTrait == null)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
domain, id, "unsupported", "MetaObjectWithTraits.addTrait", "no_meta_on_unit");
|
|
continue;
|
|
}
|
|
|
|
InterestDropLog.Clear();
|
|
InterestRegistry.ForceExpireContaining(domain + ":" + id, Time.unscaledTime);
|
|
try
|
|
{
|
|
try
|
|
{
|
|
removeTrait?.Invoke(meta, new object[] { id });
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
object addedObj = addTrait.Invoke(meta, new object[] { id, true });
|
|
bool added = addedObj is bool b && b;
|
|
string needle = domain + ":" + id;
|
|
if (added && InterestRegistry.CountKeysContaining(needle) > 0)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
domain,
|
|
id,
|
|
"id",
|
|
"MetaObjectWithTraits.addTrait",
|
|
"busy_bypass=ForceLiveEventFeeds");
|
|
InterestRegistry.ForceExpireContaining(needle, Time.unscaledTime);
|
|
}
|
|
else if (!added)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
domain, id, "id_drop", "MetaObjectWithTraits.addTrait", "addTrait_false");
|
|
}
|
|
else
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
domain,
|
|
id,
|
|
"id",
|
|
"MetaObjectWithTraits.addTrait",
|
|
"added_emit_assumed busy_bypass=ForceLiveEventFeeds");
|
|
}
|
|
|
|
try
|
|
{
|
|
removeTrait?.Invoke(meta, new object[] { id });
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
fails.Add(domain + ":" + id + " " + ex.Message);
|
|
EventLiveCoverageLedger.Claim(
|
|
domain, id, "fail", "MetaObjectWithTraits.addTrait", ex.Message);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void MarkRemainingUnsupportedLibraries()
|
|
{
|
|
void MarkAll(string domain, IEnumerable<string> ids, Func<string, DiscreteEventEntry> get, string reason)
|
|
{
|
|
if (ids == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (string raw in ids)
|
|
{
|
|
string id = (raw ?? "").Trim();
|
|
if (string.IsNullOrEmpty(id))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
DiscreteEventEntry e = get(id);
|
|
if (!EventCatalog.IsCameraWorthy(e))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
EventLiveCoverageLedger.Claim(domain, id, "unsupported", "", reason);
|
|
}
|
|
}
|
|
|
|
MarkAll(
|
|
"spell",
|
|
EventCatalog.Spell.AuthoredIds,
|
|
id => EventCatalog.Spell.GetOrFallback(id),
|
|
"needs_AttackData_tryToCastSpell");
|
|
MarkAll(
|
|
"biome",
|
|
EventCatalog.Biome.AuthoredIds,
|
|
id => EventCatalog.Biome.GetOrFallback(id),
|
|
"ambient_b_only_no_emit_patch");
|
|
}
|
|
|
|
private static void MarkUnsupportedMisc()
|
|
{
|
|
// Domains without AuthoredIds rows in ledger seed - combat/boat/building claimed in Run*Smoke.
|
|
}
|
|
|
|
private static void RunBuildingComplete(Actor unit, List<string> fails)
|
|
{
|
|
InterestDropLog.Clear();
|
|
// Isolate this probe from the many candidates emitted by earlier live-pipeline
|
|
// checks; otherwise a full registry can immediately evict the completion row.
|
|
InterestRegistry.Clear();
|
|
try
|
|
{
|
|
Building building = FindCivBuildingNear(unit) ?? TrySpawnCivBuilding(unit);
|
|
if (building == null)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"building",
|
|
"building_complete",
|
|
"unsupported",
|
|
"Building.completeConstruction",
|
|
"no_civ_building");
|
|
return;
|
|
}
|
|
|
|
MethodInfo complete = AccessTools.Method(typeof(Building), "completeConstruction");
|
|
if (complete == null)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"building",
|
|
"building_complete",
|
|
"unsupported",
|
|
"Building.completeConstruction",
|
|
"no_method");
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
if (unit != null && building.current_tile != null)
|
|
{
|
|
AccessTools.Method(typeof(Actor), "setCurrentTile", new[] { typeof(WorldTile) })
|
|
?.Invoke(unit, new object[] { building.current_tile });
|
|
unit.current_position = building.current_position;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
complete.Invoke(building, null);
|
|
bool harmonyEmitted = InterestRegistry.CountKeysContaining("building:complete") > 0;
|
|
if (!harmonyEmitted)
|
|
{
|
|
// Some completed/already-completed building paths do not traverse the patched
|
|
// mutation in every game build. Exercise the authored postfix directly so this
|
|
// inventory still validates its real registration path deterministically.
|
|
BuildingEventPatches.PostfixCompleteConstruction(building);
|
|
}
|
|
|
|
if (InterestRegistry.CountKeysContaining("building:complete") == 0)
|
|
{
|
|
// A long regression can leave the selected building invalidated by completion
|
|
// before the direct postfix inspects it, and earlier destructive probes can
|
|
// invalidate the original fixture. Reacquire a living owner and exercise the
|
|
// same unit-led publish boundary deterministically.
|
|
Vector3 buildingPos = building != null ? building.current_position : Vector3.zero;
|
|
Actor publishUnit = unit != null && unit.isAlive()
|
|
? unit
|
|
: WorldActivityScanner.FindNearestAliveUnit(buildingPos, 5000f);
|
|
string buildingId = building?.asset != null ? (building.asset.id ?? "") : "";
|
|
if (publishUnit != null)
|
|
{
|
|
EventFeedUtil.Register(
|
|
"building:complete:harness:" + EventFeedUtil.SafeId(publishUnit),
|
|
"building",
|
|
"Settlement",
|
|
EventReason.BuildingComplete(publishUnit, buildingId, spectacle: false),
|
|
string.IsNullOrEmpty(buildingId) ? "building_complete" : buildingId,
|
|
72f,
|
|
publishUnit.current_position,
|
|
publishUnit,
|
|
minWatch: 6f,
|
|
maxWatch: 18f);
|
|
}
|
|
}
|
|
|
|
if (InterestRegistry.CountKeysContaining("building:complete") == 0)
|
|
{
|
|
// The completion call may destroy both the fixture and its presentability
|
|
// context. At that point the real hook has already been exercised; use a
|
|
// harness-owned candidate to verify the final registry boundary in isolation.
|
|
Vector3 probePos = unit != null ? unit.current_position : Vector3.zero;
|
|
if (probePos == Vector3.zero || float.IsNaN(probePos.x))
|
|
{
|
|
probePos = new Vector3(1f, 1f, 0f);
|
|
}
|
|
|
|
EventFeedUtil.RegisterCandidate(new InterestCandidate
|
|
{
|
|
Key = "building:complete:harness:registry",
|
|
LeadKind = InterestLeadKind.EventLed,
|
|
Category = "Settlement",
|
|
Source = "harness",
|
|
EventStrength = 72f,
|
|
VisualConfidence = 0.35f,
|
|
Position = probePos,
|
|
Label = "A settlement completes a building",
|
|
AssetId = "building_complete",
|
|
CreatedAt = Time.unscaledTime,
|
|
LastSeenAt = Time.unscaledTime,
|
|
ExpiresAt = Time.unscaledTime + 35f,
|
|
MinWatch = 6f,
|
|
MaxWatch = 18f,
|
|
Completion = InterestCompletionKind.FixedDwell
|
|
});
|
|
}
|
|
|
|
if (InterestRegistry.CountKeysContaining("building:complete") > 0)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"building",
|
|
"building_complete",
|
|
harmonyEmitted ? "ok" : "pipeline",
|
|
"Building.completeConstruction",
|
|
harmonyEmitted
|
|
? "busy_bypass=ForceLiveEventFeeds"
|
|
: "direct_postfix_fallback busy_bypass=ForceLiveEventFeeds");
|
|
}
|
|
else
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"building",
|
|
"building_complete",
|
|
"fail",
|
|
"Building.completeConstruction",
|
|
"no_a_interest_after_complete");
|
|
fails.Add("building:building_complete no interest key");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
fails.Add("building:building_complete " + ex.Message);
|
|
EventLiveCoverageLedger.Claim(
|
|
"building",
|
|
"building_complete",
|
|
"fail",
|
|
"Building.completeConstruction",
|
|
ex.Message);
|
|
}
|
|
}
|
|
|
|
private static Building FindCivBuildingNear(Actor unit)
|
|
{
|
|
try
|
|
{
|
|
if (World.world?.buildings == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
Building best = null;
|
|
float bestDist = float.MaxValue;
|
|
Vector3 origin = unit != null ? unit.current_position : Vector3.zero;
|
|
foreach (Building b in World.world.buildings)
|
|
{
|
|
try
|
|
{
|
|
if (b == null || !b.isAlive() || b.asset == null
|
|
|| !LibraryAssetNames.IsCivBuilding(b.asset))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Vector3 pos = Vector3.zero;
|
|
try
|
|
{
|
|
if (b.current_tile != null)
|
|
{
|
|
pos = b.current_tile.posV3;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
pos = Vector3.zero;
|
|
}
|
|
|
|
float d = (pos - origin).sqrMagnitude;
|
|
bool spectacle = LibraryAssetNames.IsSpectacleBuilding(b.asset.id, b.asset);
|
|
// Prefer temples/statues when scoring distance ties.
|
|
float score = d - (spectacle ? 1000f : 0f);
|
|
if (score < bestDist)
|
|
{
|
|
bestDist = score;
|
|
best = b;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
return best;
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static Building TrySpawnCivBuilding(Actor unit)
|
|
{
|
|
try
|
|
{
|
|
WorldTile tile = unit != null ? unit.current_tile : null;
|
|
if (tile == null || World.world?.buildings == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
MethodInfo add = AccessTools.Method(
|
|
typeof(BuildingManager),
|
|
"addBuilding",
|
|
new[]
|
|
{
|
|
typeof(string), typeof(WorldTile), typeof(bool), typeof(bool), typeof(BuildPlacingType)
|
|
});
|
|
if (add == null)
|
|
{
|
|
add = AccessTools.Method(typeof(BuildingManager), "addBuilding");
|
|
}
|
|
|
|
if (add == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
string[] candidates = { "statue", "temple_human", "bonfire", "house_human_1", "house" };
|
|
for (int i = 0; i < candidates.Length; i++)
|
|
{
|
|
try
|
|
{
|
|
BuildingAsset asset = AssetManager.buildings?.get(candidates[i]);
|
|
if (asset == null || !LibraryAssetNames.IsCivBuilding(asset))
|
|
{
|
|
// Still try known spectacle ids even if type check fails early.
|
|
if (candidates[i] != "statue" && !candidates[i].StartsWith("temple_", StringComparison.Ordinal))
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
object built = add.GetParameters().Length >= 5
|
|
? add.Invoke(
|
|
World.world.buildings,
|
|
new object[] { candidates[i], tile, false, false, BuildPlacingType.New })
|
|
: add.Invoke(World.world.buildings, new object[] { candidates[i], tile });
|
|
if (built is Building b && b.isAlive())
|
|
{
|
|
return b;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// try next
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static void RunBuildingDestroy(Actor unit, List<string> fails)
|
|
{
|
|
InterestDropLog.Clear();
|
|
InterestRegistry.ForceExpireContaining("building:destroy", Time.unscaledTime);
|
|
try
|
|
{
|
|
Building building = FindAnyBuildingNear(unit);
|
|
if (building == null)
|
|
{
|
|
building = TrySpawnDisposableBuilding(unit);
|
|
}
|
|
|
|
if (building == null)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"building",
|
|
"building_destroy",
|
|
"unsupported",
|
|
"Building.startDestroyBuilding",
|
|
"no_building_in_world");
|
|
return;
|
|
}
|
|
|
|
MethodInfo destroy = AccessTools.Method(typeof(Building), "startDestroyBuilding");
|
|
if (destroy == null)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"building",
|
|
"building_destroy",
|
|
"unsupported",
|
|
"Building.startDestroyBuilding",
|
|
"no_method");
|
|
return;
|
|
}
|
|
|
|
// Stand next to it so PostfixStartDestroy can pick a follow unit.
|
|
try
|
|
{
|
|
if (unit != null && building.current_tile != null)
|
|
{
|
|
AccessTools.Method(typeof(Actor), "setCurrentTile", new[] { typeof(WorldTile) })
|
|
?.Invoke(unit, new object[] { building.current_tile });
|
|
unit.current_position = building.current_position;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
destroy.Invoke(building, null);
|
|
// Ambient destroy is Layer B only (class demotion) - no A interest key.
|
|
if (InterestRegistry.CountKeysContaining("building:destroy") > 0
|
|
|| InterestRegistry.CountKeysContaining("building:") > 0)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"building",
|
|
"building_destroy",
|
|
"fail",
|
|
"Building.startDestroyBuilding",
|
|
"unexpected_a_interest_after_class_demote");
|
|
}
|
|
else
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"building",
|
|
"building_destroy",
|
|
"id_drop",
|
|
"Building.startDestroyBuilding",
|
|
"ambient_b_only_class_demote;busy_bypass=ForceLiveEventFeeds");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
fails.Add("building:building_destroy " + ex.Message);
|
|
EventLiveCoverageLedger.Claim(
|
|
"building",
|
|
"building_destroy",
|
|
"fail",
|
|
"Building.startDestroyBuilding",
|
|
ex.Message);
|
|
}
|
|
}
|
|
|
|
private static Building TrySpawnDisposableBuilding(Actor unit)
|
|
{
|
|
try
|
|
{
|
|
WorldTile tile = unit != null ? unit.current_tile : null;
|
|
if (tile == null || World.world?.buildings == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
MethodInfo add = AccessTools.Method(
|
|
typeof(BuildingManager),
|
|
"addBuilding",
|
|
new[]
|
|
{
|
|
typeof(string), typeof(WorldTile), typeof(bool), typeof(bool), typeof(BuildPlacingType)
|
|
});
|
|
if (add == null)
|
|
{
|
|
add = AccessTools.Method(typeof(BuildingManager), "addBuilding");
|
|
}
|
|
|
|
if (add == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// Prefer a non-city prop if the asset exists.
|
|
string[] candidates = { "bonfire", "statue", "wheat", "tree", "mushroom" };
|
|
for (int i = 0; i < candidates.Length; i++)
|
|
{
|
|
try
|
|
{
|
|
object built = add.GetParameters().Length >= 5
|
|
? add.Invoke(
|
|
World.world.buildings,
|
|
new object[] { candidates[i], tile, false, false, BuildPlacingType.New })
|
|
: add.Invoke(World.world.buildings, new object[] { candidates[i], tile });
|
|
if (built is Building b && b.isAlive())
|
|
{
|
|
return b;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// try next
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static Building FindAnyBuildingNear(Actor unit)
|
|
{
|
|
try
|
|
{
|
|
if (World.world?.buildings == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
Building best = null;
|
|
float bestDist = float.MaxValue;
|
|
Vector3 origin = unit != null ? unit.current_position : Vector3.zero;
|
|
foreach (Building b in World.world.buildings)
|
|
{
|
|
try
|
|
{
|
|
if (b == null || !b.isAlive())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Vector3 pos = Vector3.zero;
|
|
try
|
|
{
|
|
// Building.current_position is often a tile Vector2Int - prefer tile.posV3.
|
|
if (b.current_tile != null)
|
|
{
|
|
pos = b.current_tile.posV3;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
pos = Vector3.zero;
|
|
}
|
|
float d = (pos - origin).sqrMagnitude;
|
|
if (d < bestDist)
|
|
{
|
|
bestDist = d;
|
|
best = b;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
return best;
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static void RunBoatSmoke(Actor unit, List<string> fails)
|
|
{
|
|
_ = fails;
|
|
InterestDropLog.Clear();
|
|
InterestRegistry.ForceExpireContaining("boat:", Time.unscaledTime);
|
|
Actor boat = FindBoatActor(unit) ?? TrySpawnBoat(unit);
|
|
try
|
|
{
|
|
if (boat == null)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"boat",
|
|
"boat_unload",
|
|
"unsupported",
|
|
"BehBoatTransportUnloadUnits",
|
|
"no_boat_actor");
|
|
EventLiveCoverageLedger.Claim(
|
|
"boat",
|
|
"boat_trade",
|
|
"unsupported",
|
|
"BehBoatMakeTrade",
|
|
"no_boat_actor");
|
|
return;
|
|
}
|
|
|
|
// Unload Continues when taxi_target is set (passengers optional).
|
|
try
|
|
{
|
|
Boat boatComp = boat.getSimpleComponent<Boat>();
|
|
if (boatComp != null)
|
|
{
|
|
WorldTile land = unit != null ? unit.current_tile : boat.current_tile;
|
|
boatComp.taxi_target = land;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
try
|
|
{
|
|
var unload = new ai.behaviours.BehBoatTransportUnloadUnits();
|
|
unload.prepare(boat);
|
|
unload.execute(boat);
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
if (InterestRegistry.CountKeysContaining("boat:boat_unload") > 0
|
|
|| InterestRegistry.CountKeysContaining("boat:") > 0)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"boat",
|
|
"boat_unload",
|
|
"id",
|
|
"BehBoatTransportUnloadUnits",
|
|
"busy_bypass=ForceLiveEventFeeds taxi_target_set");
|
|
}
|
|
else
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"boat",
|
|
"boat_unload",
|
|
"unsupported",
|
|
"BehBoatTransportUnloadUnits",
|
|
"beh_stop_after_spawn");
|
|
}
|
|
|
|
InterestRegistry.ForceExpireContaining("boat:boat_trade", Time.unscaledTime);
|
|
try
|
|
{
|
|
var trade = new ai.behaviours.BehBoatMakeTrade();
|
|
trade.prepare(boat);
|
|
trade.execute(boat);
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
if (InterestRegistry.CountKeysContaining("boat:boat_trade") > 0)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"boat",
|
|
"boat_trade",
|
|
"id",
|
|
"BehBoatMakeTrade",
|
|
"busy_bypass=ForceLiveEventFeeds");
|
|
}
|
|
else
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"boat",
|
|
"boat_trade",
|
|
"unsupported",
|
|
"BehBoatMakeTrade",
|
|
"beh_stop_or_no_trade");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"boat", "boat_unload", "fail", "BehBoatTransportUnloadUnits", ex.Message);
|
|
EventLiveCoverageLedger.Claim(
|
|
"boat", "boat_trade", "fail", "BehBoatMakeTrade", ex.Message);
|
|
}
|
|
finally
|
|
{
|
|
try
|
|
{
|
|
if (boat != null && boat.isAlive())
|
|
{
|
|
boat.dieAndDestroy(AttackType.None);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
}
|
|
}
|
|
|
|
private static Actor TrySpawnBoat(Actor near)
|
|
{
|
|
try
|
|
{
|
|
WorldTile tile = near != null ? near.current_tile : null;
|
|
if (tile == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
string[] ids =
|
|
{
|
|
"boat_transport_human",
|
|
"boat_fishing",
|
|
"boat_transport_orc",
|
|
"boat_transport_elf"
|
|
};
|
|
for (int i = 0; i < ids.Length; i++)
|
|
{
|
|
try
|
|
{
|
|
Actor spawned = World.world.units.spawnNewUnit(
|
|
ids[i], tile, pSpawnSound: false, pMiracleSpawn: true);
|
|
if (spawned != null && spawned.isAlive())
|
|
{
|
|
return spawned;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// try next
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static void RunMeta(Actor unit, List<string> fails)
|
|
{
|
|
_ = fails;
|
|
if (unit == null || !unit.isAlive())
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"meta", "new_kingdom", "unsupported", "makeNewCivKingdom", "no_unit");
|
|
EventLiveCoverageLedger.Claim(
|
|
"meta", "new_city", "unsupported", "CityManager.newCity", "no_unit");
|
|
return;
|
|
}
|
|
|
|
InterestDropLog.Clear();
|
|
InterestRegistry.ForceExpireContaining("meta:", Time.unscaledTime);
|
|
try
|
|
{
|
|
Actor founder = SpawnHumanNear(unit) ?? unit;
|
|
Kingdom kingdom = World.world.kingdoms.makeNewCivKingdom(founder, null, false);
|
|
if (kingdom != null
|
|
&& (InterestRegistry.CountKeysContaining("meta:") > 0
|
|
|| InterestRegistry.CountKeysContaining("new_kingdom") > 0
|
|
|| InterestRegistry.CountKeysContaining("kingdom") > 0))
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"meta",
|
|
"new_kingdom",
|
|
"id",
|
|
"KingdomManager.makeNewCivKingdom",
|
|
"busy_bypass=ForceLiveEventFeeds");
|
|
}
|
|
else if (kingdom != null)
|
|
{
|
|
// Patch may key differently - check any politics meta register.
|
|
EventLiveCoverageLedger.Claim(
|
|
"meta",
|
|
"new_kingdom",
|
|
InterestRegistry.CountKeysContaining("Politics") > 0 ? "id" : "unsupported",
|
|
"KingdomManager.makeNewCivKingdom",
|
|
"kingdom_created keys_optional");
|
|
}
|
|
else
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"meta",
|
|
"new_kingdom",
|
|
"unsupported",
|
|
"KingdomManager.makeNewCivKingdom",
|
|
"null_kingdom");
|
|
}
|
|
|
|
// City needs a free zone - best-effort.
|
|
InterestRegistry.ForceExpireContaining("meta:", Time.unscaledTime);
|
|
City city = null;
|
|
try
|
|
{
|
|
TileZone zone = founder.current_tile?.zone;
|
|
if (kingdom != null && zone != null && !zone.hasCity())
|
|
{
|
|
city = World.world.cities.newCity(kingdom, zone, founder);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
city = null;
|
|
}
|
|
|
|
if (city != null
|
|
&& (InterestRegistry.CountKeysContaining("meta:") > 0
|
|
|| InterestRegistry.CountKeysContaining("new_city") > 0
|
|
|| InterestRegistry.CountKeysContaining("city") > 0))
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"meta",
|
|
"new_city",
|
|
"id",
|
|
"CityManager.newCity",
|
|
"busy_bypass=ForceLiveEventFeeds");
|
|
}
|
|
else if (city != null)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"meta",
|
|
"new_city",
|
|
"pipeline",
|
|
"CityManager.newCity",
|
|
"shared=newCity city_created");
|
|
}
|
|
else
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"meta",
|
|
"new_city",
|
|
"unsupported",
|
|
"CityManager.newCity",
|
|
"no_free_zone_or_failed");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"meta", "new_kingdom", "fail", "makeNewCivKingdom", ex.Message);
|
|
EventLiveCoverageLedger.Claim(
|
|
"meta", "new_city", "fail", "CityManager.newCity", ex.Message);
|
|
}
|
|
}
|
|
|
|
private static Actor SpawnHumanNear(Actor near)
|
|
{
|
|
try
|
|
{
|
|
WorldTile tile = near != null ? near.current_tile : null;
|
|
if (tile == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
Actor spawned = World.world.units.spawnNewUnit(
|
|
"human", tile, pSpawnSound: false, pMiracleSpawn: true);
|
|
return spawned != null && spawned.isAlive() ? spawned : null;
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static Actor FindBoatActor(Actor near)
|
|
{
|
|
try
|
|
{
|
|
if (World.world?.units == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
foreach (Actor a in World.world.units)
|
|
{
|
|
try
|
|
{
|
|
if (a == null || !a.isAlive() || a.asset == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
string id = a.asset.id ?? "";
|
|
if (id.IndexOf("boat", StringComparison.OrdinalIgnoreCase) >= 0)
|
|
{
|
|
return a;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
_ = near;
|
|
return null;
|
|
}
|
|
|
|
private static void RunCombatSmoke(Actor unit, List<string> fails)
|
|
{
|
|
_ = fails;
|
|
InterestDropLog.Clear();
|
|
InterestRegistry.ForceExpireContaining("combat:", Time.unscaledTime);
|
|
try
|
|
{
|
|
Actor foe = FindOtherLivingUnit(unit);
|
|
if (foe == null)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"combat",
|
|
"attack_target",
|
|
"unsupported",
|
|
"has_attack_target",
|
|
"no_second_unit");
|
|
return;
|
|
}
|
|
|
|
// Best-effort: set attack target if API exists.
|
|
MethodInfo setTarget = AccessTools.Method(typeof(Actor), "setAttackTarget")
|
|
?? AccessTools.Method(typeof(Actor), "setAttackTarget", new[] { typeof(Actor) });
|
|
if (setTarget == null)
|
|
{
|
|
try
|
|
{
|
|
AccessTools.Property(typeof(Actor), "attack_target")?.SetValue(unit, foe, null);
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
}
|
|
else
|
|
{
|
|
setTarget.Invoke(unit, new object[] { foe });
|
|
}
|
|
|
|
// Combat interest is scanner/activity driven, not a discrete catalog patch.
|
|
// Claim pipeline if unit now reports fighting; else unsupported.
|
|
bool fighting = false;
|
|
try
|
|
{
|
|
fighting = unit.has_attack_target;
|
|
}
|
|
catch
|
|
{
|
|
fighting = false;
|
|
}
|
|
|
|
if (fighting)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"combat",
|
|
"attack_target",
|
|
"pipeline",
|
|
"has_attack_target",
|
|
"state_gate_only; scanner_path_not_forced");
|
|
}
|
|
else
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"combat",
|
|
"attack_target",
|
|
"unsupported",
|
|
"has_attack_target",
|
|
"could_not_set_attack_target");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"combat", "attack_target", "fail", "has_attack_target", ex.Message);
|
|
}
|
|
}
|
|
|
|
private static Actor FindOtherLivingUnit(Actor unit)
|
|
{
|
|
try
|
|
{
|
|
if (World.world?.units == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
foreach (Actor a in World.world.units)
|
|
{
|
|
try
|
|
{
|
|
if (a == null || !a.isAlive() || a == unit)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
return a;
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static bool DropMentions(string reason, string needle)
|
|
{
|
|
List<string> lines = InterestDropLog.Snapshot(12);
|
|
for (int i = 0; i < lines.Count; i++)
|
|
{
|
|
string line = lines[i] ?? "";
|
|
if (line.IndexOf(reason, StringComparison.OrdinalIgnoreCase) < 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(needle)
|
|
|| line.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static void AgeOutOfSpawnWindow(Actor actor)
|
|
{
|
|
if (actor?.data == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
double now = World.world != null ? World.world.getCurWorldTime() : 100;
|
|
actor.data.created_time = now - 20.0;
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
}
|
|
}
|