worldbox-observer-mod/IdleSpectator/EventLivePipelinesHarness.cs
DazedAnon 03729664be Implement building completion events and enhance earthquake handling.
- 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
2026-07-17 17:13:43 -05:00

2440 lines
77 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();
InterestRegistry.ForceExpireContaining("building:complete", Time.unscaledTime);
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);
if (InterestRegistry.CountKeysContaining("building:complete") > 0)
{
EventLiveCoverageLedger.Claim(
"building",
"building_complete",
"ok",
"Building.completeConstruction",
"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
}
}
}