1183 lines
37 KiB
C#
1183 lines
37 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);
|
|
RunBook(unit, fails);
|
|
RunPlot(unit, fails);
|
|
RunBuildingDestroy(unit, fails);
|
|
RunBoatSmoke(unit, fails);
|
|
RunCombatSmoke(unit, fails);
|
|
MarkUnsupportedLibraries(fails);
|
|
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)
|
|
{
|
|
// Need two kingdoms - often unavailable. One smoke if possible.
|
|
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
|
|
}
|
|
|
|
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)
|
|
{
|
|
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 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 MarkUnsupportedLibraries(List<string> fails)
|
|
{
|
|
void MarkAll(string domain, IEnumerable<string> ids, Func<string, DiscreteEventEntry> get)
|
|
{
|
|
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", "", "no_deterministic_live_fire_api");
|
|
}
|
|
}
|
|
|
|
MarkAll("spell", EventCatalog.Spell.AuthoredIds, id => EventCatalog.Spell.GetOrFallback(id));
|
|
MarkAll("item", EventCatalog.Item.AuthoredIds, id => EventCatalog.Item.GetOrFallback(id));
|
|
MarkAll("power", EventCatalog.Power.AuthoredIds, id => EventCatalog.Power.GetOrFallback(id));
|
|
MarkAll("gene", EventCatalog.Gene.AuthoredIds, id => EventCatalog.Gene.GetOrFallback(id));
|
|
MarkAll("phenotype", EventCatalog.Phenotype.AuthoredIds, id => EventCatalog.Phenotype.GetOrFallback(id));
|
|
MarkAll("worldLaw", EventCatalog.WorldLaw.AuthoredIds, id => EventCatalog.WorldLaw.GetOrFallback(id));
|
|
MarkAll(
|
|
"subspecies_trait",
|
|
EventCatalog.SubspeciesTrait.AuthoredIds,
|
|
id => EventCatalog.SubspeciesTrait.GetOrFallback(id));
|
|
MarkAll("biome", EventCatalog.Biome.AuthoredIds, id => EventCatalog.Biome.GetOrFallback(id));
|
|
}
|
|
|
|
private static void MarkUnsupportedMisc()
|
|
{
|
|
// Domains without AuthoredIds rows in ledger seed - combat/boat/building claimed in Run*Smoke.
|
|
}
|
|
|
|
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);
|
|
if (InterestRegistry.CountKeysContaining("building:destroy") > 0
|
|
|| InterestRegistry.CountKeysContaining("building:") > 0)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"building",
|
|
"building_destroy",
|
|
"id",
|
|
"Building.startDestroyBuilding",
|
|
"busy_bypass=ForceLiveEventFeeds");
|
|
}
|
|
else
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"building",
|
|
"building_destroy",
|
|
"unsupported",
|
|
"Building.startDestroyBuilding",
|
|
"invoked_but_no_interest_key");
|
|
}
|
|
}
|
|
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 = b.current_position;
|
|
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);
|
|
try
|
|
{
|
|
Actor boat = FindBoatActor(unit);
|
|
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/trade Beh need transport state - mark pipeline only if key appears.
|
|
try
|
|
{
|
|
new ai.behaviours.BehBoatTransportUnloadUnits().execute(boat);
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
if (InterestRegistry.CountKeysContaining("boat:boat_unload") > 0)
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"boat",
|
|
"boat_unload",
|
|
"id",
|
|
"BehBoatTransportUnloadUnits",
|
|
"busy_bypass=ForceLiveEventFeeds");
|
|
}
|
|
else
|
|
{
|
|
EventLiveCoverageLedger.Claim(
|
|
"boat",
|
|
"boat_unload",
|
|
"unsupported",
|
|
"BehBoatTransportUnloadUnits",
|
|
"beh_stop_or_no_passengers");
|
|
}
|
|
|
|
try
|
|
{
|
|
new ai.behaviours.BehBoatMakeTrade().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);
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|