Enhance interest management in IdleSpectator by introducing new methods for enumerating live plot, era, book type, trait, and building IDs. Update AgentHarness to support domain feeds and add new harness scenarios for domain event auditing and event tracking. Refactor event registration logic in InterestFeeds and WarInterestFeed to streamline interest candidate creation. Increment version in mod.json to 0.16.32 to reflect these changes.
This commit is contained in:
parent
20996fa3a7
commit
eaa431cc3d
30 changed files with 2593 additions and 89 deletions
|
|
@ -274,6 +274,31 @@ public static class ActivityAssetCatalog
|
|||
}
|
||||
}
|
||||
|
||||
public static List<string> EnumerateLivePlotIds()
|
||||
{
|
||||
return EnumerateLibraryIds("plots_library");
|
||||
}
|
||||
|
||||
public static List<string> EnumerateLiveEraIds()
|
||||
{
|
||||
return EnumerateLibraryIds("era_library");
|
||||
}
|
||||
|
||||
public static List<string> EnumerateLiveBookTypeIds()
|
||||
{
|
||||
return EnumerateLibraryIds("book_types");
|
||||
}
|
||||
|
||||
public static List<string> EnumerateLiveTraitIds()
|
||||
{
|
||||
return EnumerateLibraryIds("traits");
|
||||
}
|
||||
|
||||
public static List<string> EnumerateLiveBuildingIds()
|
||||
{
|
||||
return EnumerateLibraryIds("buildings");
|
||||
}
|
||||
|
||||
private static List<string> EnumerateLibraryIds(string assetManagerField)
|
||||
{
|
||||
var ids = new List<string>();
|
||||
|
|
|
|||
|
|
@ -1801,6 +1801,10 @@ public static class AgentHarness
|
|||
DoWorldLogFeed(cmd);
|
||||
break;
|
||||
|
||||
case "domain_feed":
|
||||
DoDomainFeed(cmd);
|
||||
break;
|
||||
|
||||
case "interest_inject":
|
||||
DoInterestInject(cmd);
|
||||
break;
|
||||
|
|
@ -2522,6 +2526,189 @@ public static class AgentHarness
|
|||
Emit(cmd, ok: true, detail: $"fed id={assetId} key={key} evt={entry.EventStrength:0.#}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Injects a Signal from a domain catalog while the harness is Busy
|
||||
/// (domain feeds themselves early-out on Busy, so register here).
|
||||
/// asset = domain, value = event/asset id.
|
||||
/// </summary>
|
||||
private static void DoDomainFeed(HarnessCommand cmd)
|
||||
{
|
||||
if (!WorldReady())
|
||||
{
|
||||
_cmdFail++;
|
||||
Emit(cmd, ok: false, detail: "world_not_ready");
|
||||
return;
|
||||
}
|
||||
|
||||
string domain = (cmd.asset ?? "").Trim().ToLowerInvariant();
|
||||
string id = (cmd.value ?? cmd.expect ?? "").Trim();
|
||||
Actor unit = ResolveUnit(null) ?? WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f);
|
||||
if (unit == null && domain != "era")
|
||||
{
|
||||
_cmdFail++;
|
||||
Emit(cmd, ok: false, detail: "no unit for domain_feed");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
DiscreteEventEntry entry;
|
||||
string key;
|
||||
string label;
|
||||
InterestOwnership owns;
|
||||
float strength;
|
||||
string category;
|
||||
string source;
|
||||
|
||||
switch (domain)
|
||||
{
|
||||
case "relationship":
|
||||
case "rel":
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
id = "set_lover";
|
||||
}
|
||||
|
||||
entry = RelationshipEventCatalog.GetOrFallback(id);
|
||||
key = "rel:" + entry.Id + ":" + EventFeedUtil.SafeId(unit);
|
||||
label = entry.MakeLabel(EventFeedUtil.SafeName(unit), "");
|
||||
owns = entry.OwnsCamera;
|
||||
strength = entry.EventStrength;
|
||||
category = entry.Category;
|
||||
source = "relationship";
|
||||
break;
|
||||
case "plot":
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
id = "new_war";
|
||||
}
|
||||
|
||||
entry = PlotInterestCatalog.GetOrFallback(id);
|
||||
key = "plot:" + entry.Id + ":new:" + EventFeedUtil.SafeId(unit);
|
||||
label = entry.MakeLabel(EventFeedUtil.SafeName(unit), "new");
|
||||
owns = entry.OwnsCamera;
|
||||
strength = entry.EventStrength;
|
||||
category = entry.Category;
|
||||
source = "plot";
|
||||
break;
|
||||
case "era":
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
id = "age_chaos";
|
||||
}
|
||||
|
||||
entry = EraInterestCatalog.GetOrFallback(id);
|
||||
if (unit == null)
|
||||
{
|
||||
unit = EventFeedUtil.AnyAliveUnit();
|
||||
}
|
||||
|
||||
if (unit == null)
|
||||
{
|
||||
_cmdFail++;
|
||||
Emit(cmd, ok: false, detail: "no unit for era feed");
|
||||
return;
|
||||
}
|
||||
|
||||
key = "era:" + entry.Id;
|
||||
label = entry.MakeLabel("", "");
|
||||
owns = entry.OwnsCamera;
|
||||
strength = entry.EventStrength;
|
||||
category = entry.Category;
|
||||
source = "era";
|
||||
break;
|
||||
case "book":
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
id = "history_book";
|
||||
}
|
||||
|
||||
entry = BookInterestCatalog.GetOrFallback(id);
|
||||
key = "book:" + entry.Id + ":new:" + EventFeedUtil.SafeId(unit);
|
||||
label = entry.MakeLabel(EventFeedUtil.SafeName(unit), "new");
|
||||
owns = entry.OwnsCamera;
|
||||
strength = entry.EventStrength;
|
||||
category = entry.Category;
|
||||
source = "book";
|
||||
break;
|
||||
case "trait":
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
id = "immortal";
|
||||
}
|
||||
|
||||
entry = TraitInterestCatalog.GetOrFallback(id);
|
||||
key = "trait:" + entry.Id + ":gains:" + EventFeedUtil.SafeId(unit);
|
||||
label = EventFeedUtil.SafeName(unit) + " gains " + entry.Id;
|
||||
owns = entry.OwnsCamera;
|
||||
strength = entry.EventStrength;
|
||||
category = entry.Category;
|
||||
source = "trait";
|
||||
break;
|
||||
case "meta":
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
id = "new_city";
|
||||
}
|
||||
|
||||
key = "meta:" + id + ":" + EventFeedUtil.SafeId(unit);
|
||||
label = "Meta: " + id + " (" + EventFeedUtil.SafeName(unit) + ")";
|
||||
owns = InterestOwnership.Signal;
|
||||
strength = 74f;
|
||||
category = "Politics";
|
||||
source = "meta";
|
||||
break;
|
||||
case "boat":
|
||||
id = "boat_unload";
|
||||
key = "boat:boat_unload:" + EventFeedUtil.SafeId(unit);
|
||||
label = "Boat unloads: " + EventFeedUtil.SafeName(unit);
|
||||
owns = InterestOwnership.Signal;
|
||||
strength = 72f;
|
||||
category = "Travel";
|
||||
source = "boat";
|
||||
break;
|
||||
case "building":
|
||||
id = "building_damage";
|
||||
key = "building:building_damage:" + EventFeedUtil.SafeId(unit);
|
||||
label = "Damages a building: " + EventFeedUtil.SafeName(unit);
|
||||
owns = InterestOwnership.Signal;
|
||||
strength = 70f;
|
||||
category = "Spectacle";
|
||||
source = "building";
|
||||
break;
|
||||
default:
|
||||
_cmdFail++;
|
||||
Emit(cmd, ok: false, detail: "unknown domain=" + domain);
|
||||
return;
|
||||
}
|
||||
|
||||
InterestCandidate registered = EventFeedUtil.Register(
|
||||
key,
|
||||
source,
|
||||
category,
|
||||
label,
|
||||
id,
|
||||
strength,
|
||||
owns,
|
||||
unit.current_position,
|
||||
unit);
|
||||
if (registered == null)
|
||||
{
|
||||
_cmdFail++;
|
||||
Emit(cmd, ok: false, detail: "register_failed domain=" + domain + " id=" + id);
|
||||
return;
|
||||
}
|
||||
|
||||
_cmdOk++;
|
||||
Emit(cmd, ok: true, detail: $"domain={domain} id={id} key={key}");
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
_cmdFail++;
|
||||
Emit(cmd, ok: false, detail: "domain_feed error: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DoInterestInject(HarnessCommand cmd)
|
||||
{
|
||||
if (!WorldReady())
|
||||
|
|
@ -4061,6 +4248,13 @@ public static class AgentHarness
|
|||
detail = audit.Detail;
|
||||
break;
|
||||
}
|
||||
case "domain_event_audit":
|
||||
{
|
||||
DomainEventAuditResult audit = DomainEventHarness.RunAudit(HarnessDir());
|
||||
pass = audit.Passed;
|
||||
detail = audit.Detail;
|
||||
break;
|
||||
}
|
||||
case "mutation_discovery":
|
||||
{
|
||||
MutationDiscoveryResult discovery = MutationDiscoveryHarness.Run(HarnessDir());
|
||||
|
|
|
|||
72
IdleSpectator/BoatEventPatches.cs
Normal file
72
IdleSpectator/BoatEventPatches.cs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
using ai.behaviours;
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>Boat transport/trade interest patches from mutation discovery.</summary>
|
||||
[HarmonyPatch]
|
||||
public static class BoatEventPatches
|
||||
{
|
||||
[HarmonyPatch(typeof(BehBoatTransportUnloadUnits), nameof(BehBoatTransportUnloadUnits.execute))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixUnload(Actor pActor, BehResult __result)
|
||||
{
|
||||
if (pActor == null || !pActor.isAlive() || __result == BehResult.Stop)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EmitBoat("boat_unload", 72f, InterestOwnership.Signal, pActor, "Boat unloads: {a}");
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(BehBoatMakeTrade), nameof(BehBoatMakeTrade.execute))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixTrade(Actor pActor, BehResult __result)
|
||||
{
|
||||
if (pActor == null || !pActor.isAlive() || __result == BehResult.Stop)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EmitBoat("boat_trade", 68f, InterestOwnership.Signal, pActor, "Boat trade: {a}");
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(BehBoatTransportDoLoading), nameof(BehBoatTransportDoLoading.execute))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixLoad(Actor pActor, BehResult __result)
|
||||
{
|
||||
if (pActor == null || !pActor.isAlive() || __result == BehResult.Stop)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EmitBoat("boat_load", 55f, InterestOwnership.Ambient, pActor, "Boat loads: {a}");
|
||||
}
|
||||
|
||||
private static void EmitBoat(
|
||||
string eventId,
|
||||
float strength,
|
||||
InterestOwnership owns,
|
||||
Actor actor,
|
||||
string labelTemplate)
|
||||
{
|
||||
if (AgentHarness.Busy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string label = (labelTemplate ?? "{a}").Replace("{a}", EventFeedUtil.SafeName(actor));
|
||||
string key = "boat:" + eventId + ":" + EventFeedUtil.SafeId(actor);
|
||||
EventFeedUtil.Register(
|
||||
key,
|
||||
"boat",
|
||||
"Travel",
|
||||
label,
|
||||
eventId,
|
||||
strength,
|
||||
owns,
|
||||
actor.current_position,
|
||||
actor);
|
||||
}
|
||||
}
|
||||
50
IdleSpectator/BookEventPatches.cs
Normal file
50
IdleSpectator/BookEventPatches.cs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
using HarmonyLib;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>Book lifecycle patches from mutation discovery.</summary>
|
||||
[HarmonyPatch]
|
||||
public static class BookEventPatches
|
||||
{
|
||||
[HarmonyPatch(typeof(BookManager), nameof(BookManager.newBook))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixNewBook(Book __result)
|
||||
{
|
||||
if (__result == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BookInterestFeed.EmitFromBook(__result, "new");
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(BookManager), nameof(BookManager.generateNewBook))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixGenerateNewBook(Book __result)
|
||||
{
|
||||
if (__result == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BookInterestFeed.EmitFromBook(__result, "generate");
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(BookManager), nameof(BookManager.burnBook))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixBurnBook(object[] __args)
|
||||
{
|
||||
Book book = null;
|
||||
if (__args != null && __args.Length > 0)
|
||||
{
|
||||
book = __args[0] as Book;
|
||||
}
|
||||
|
||||
if (book == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BookInterestFeed.EmitFromBook(book, "burn");
|
||||
}
|
||||
}
|
||||
69
IdleSpectator/BookInterestCatalog.cs
Normal file
69
IdleSpectator/BookInterestCatalog.cs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>Authored interest overlay for every live book_types asset.</summary>
|
||||
public static class BookInterestCatalog
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
static BookInterestCatalog()
|
||||
{
|
||||
Add("family_story", 52f, "Culture", InterestOwnership.Ambient, "Family story: {a}");
|
||||
Add("love_story", 58f, "Culture", InterestOwnership.Ambient, "Love story: {a}");
|
||||
Add("friendship_story", 50f, "Culture", InterestOwnership.Ambient, "Friendship story: {a}");
|
||||
Add("bad_story_about_king", 62f, "Culture", InterestOwnership.Signal, "Scandalous book: {a}");
|
||||
Add("fable", 48f, "Culture", InterestOwnership.Ambient, "Fable: {a}");
|
||||
Add("warfare_manual", 60f, "Culture", InterestOwnership.Ambient, "Warfare manual: {a}");
|
||||
Add("economy_manual", 48f, "Culture", InterestOwnership.Ambient, "Economy manual: {a}");
|
||||
Add("stewardship_manual", 48f, "Culture", InterestOwnership.Ambient, "Stewardship manual: {a}");
|
||||
Add("diplomacy_manual", 55f, "Culture", InterestOwnership.Ambient, "Diplomacy manual: {a}");
|
||||
Add("mathbook", 45f, "Culture", InterestOwnership.Ambient, "Math book: {a}");
|
||||
Add("biology_book", 45f, "Culture", InterestOwnership.Ambient, "Biology book: {a}");
|
||||
Add("history_book", 55f, "Culture", InterestOwnership.Ambient, "History book: {a}");
|
||||
}
|
||||
|
||||
private static void Add(
|
||||
string id,
|
||||
float strength,
|
||||
string category,
|
||||
InterestOwnership owns,
|
||||
string label)
|
||||
{
|
||||
Entries[id] = new DiscreteEventEntry
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = strength,
|
||||
Category = category,
|
||||
OwnsCamera = owns,
|
||||
CreatesInterest = true,
|
||||
LabelTemplate = label
|
||||
};
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds => Entries.Keys;
|
||||
|
||||
public static bool HasAuthored(string id) =>
|
||||
!string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
|
||||
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
|
||||
return new DiscreteEventEntry
|
||||
{
|
||||
Id = key,
|
||||
EventStrength = 45f,
|
||||
Category = "Culture",
|
||||
OwnsCamera = InterestOwnership.Ambient,
|
||||
LabelTemplate = "Book ({id}): {a}",
|
||||
IsFallback = true
|
||||
};
|
||||
}
|
||||
}
|
||||
88
IdleSpectator/BookInterestFeed.cs
Normal file
88
IdleSpectator/BookInterestFeed.cs
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>Registers book creation/burn interest candidates.</summary>
|
||||
public static class BookInterestFeed
|
||||
{
|
||||
public static void Emit(string bookTypeId, Actor subject, string phase)
|
||||
{
|
||||
if (AgentHarness.Busy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DiscreteEventEntry entry = BookInterestCatalog.GetOrFallback(bookTypeId);
|
||||
if (!entry.CreatesInterest)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Actor follow = subject;
|
||||
if (follow == null || !follow.isAlive())
|
||||
{
|
||||
follow = EventFeedUtil.AnyAliveUnit();
|
||||
}
|
||||
|
||||
if (follow == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string label = entry.MakeLabel(EventFeedUtil.SafeName(follow), phase ?? "");
|
||||
if (!string.IsNullOrEmpty(phase) && phase == "burn")
|
||||
{
|
||||
label = "Book burned: " + EventFeedUtil.SafeName(follow);
|
||||
}
|
||||
|
||||
string key = "book:" + entry.Id + ":" + (phase ?? "new") + ":" + EventFeedUtil.SafeId(follow);
|
||||
InterestOwnership owns = phase == "burn" ? InterestOwnership.Signal : entry.OwnsCamera;
|
||||
float strength = phase == "burn" ? Mathf.Max(entry.EventStrength, 70f) : entry.EventStrength;
|
||||
EventFeedUtil.Register(
|
||||
key,
|
||||
"book",
|
||||
entry.Category,
|
||||
label,
|
||||
entry.Id,
|
||||
strength,
|
||||
owns,
|
||||
follow.current_position,
|
||||
follow);
|
||||
}
|
||||
|
||||
public static void EmitFromBook(object book, string phase)
|
||||
{
|
||||
if (book == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string typeId = "unknown";
|
||||
Actor author = null;
|
||||
try
|
||||
{
|
||||
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 id = asset.GetType().GetField("id")?.GetValue(asset)
|
||||
?? asset.GetType().GetProperty("id")?.GetValue(asset, null);
|
||||
if (id != null)
|
||||
{
|
||||
typeId = id.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
author = book.GetType().GetField("author")?.GetValue(book) as Actor
|
||||
?? book.GetType().GetProperty("author")?.GetValue(book, null) as Actor
|
||||
?? book.GetType().GetMethod("getAuthor")?.Invoke(book, null) as Actor;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
Emit(typeId, author, phase);
|
||||
}
|
||||
}
|
||||
118
IdleSpectator/BuildingEventPatches.cs
Normal file
118
IdleSpectator/BuildingEventPatches.cs
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
using ai.behaviours;
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>Building damage/destroy interest patches from mutation discovery.</summary>
|
||||
[HarmonyPatch]
|
||||
public static class BuildingEventPatches
|
||||
{
|
||||
[HarmonyPatch(typeof(BehDealDamageToTargetBuilding), nameof(BehDealDamageToTargetBuilding.execute))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixDamageBuilding(Actor pActor, BehResult __result)
|
||||
{
|
||||
if (pActor == null || !pActor.isAlive() || __result == BehResult.Stop)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Emit("building_damage", 70f, InterestOwnership.Signal, pActor, "Damages a building: {a}");
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(BehConsumeTargetBuilding), nameof(BehConsumeTargetBuilding.execute))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixConsumeBuilding(Actor pActor, BehResult __result)
|
||||
{
|
||||
if (pActor == null || !pActor.isAlive() || __result == BehResult.Stop)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Emit("building_consume", 78f, InterestOwnership.Signal, pActor, "Consumes a building: {a}");
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Building), "startDestroyBuilding")]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixStartDestroy(Building __instance)
|
||||
{
|
||||
if (__instance == null || AgentHarness.Busy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Vector3 pos = Vector3.zero;
|
||||
object posObj = __instance.GetType().GetField("current_position")?.GetValue(__instance)
|
||||
?? __instance.GetType().GetProperty("current_position")?.GetValue(__instance, null);
|
||||
if (posObj is Vector3 v)
|
||||
{
|
||||
pos = v;
|
||||
}
|
||||
|
||||
string buildingName = "";
|
||||
object asset = __instance.GetType().GetField("asset")?.GetValue(__instance)
|
||||
?? __instance.GetType().GetProperty("asset")?.GetValue(__instance, null);
|
||||
if (asset != null)
|
||||
{
|
||||
object id = asset.GetType().GetField("id")?.GetValue(asset)
|
||||
?? asset.GetType().GetProperty("id")?.GetValue(asset, null);
|
||||
buildingName = id?.ToString() ?? "";
|
||||
}
|
||||
|
||||
Actor near = pos != Vector3.zero
|
||||
? EventFeedUtil.NearestUnit(pos) ?? EventFeedUtil.AnyAliveUnit()
|
||||
: EventFeedUtil.AnyAliveUnit();
|
||||
if (near == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string label = string.IsNullOrEmpty(buildingName)
|
||||
? "Building destroyed near " + EventFeedUtil.SafeName(near)
|
||||
: "Building falls: " + buildingName;
|
||||
string key = "building:destroy:" + buildingName + ":" + EventFeedUtil.SafeId(near);
|
||||
EventFeedUtil.Register(
|
||||
key,
|
||||
"building",
|
||||
"Spectacle",
|
||||
label,
|
||||
string.IsNullOrEmpty(buildingName) ? "building_destroy" : buildingName,
|
||||
88f,
|
||||
InterestOwnership.Signal,
|
||||
near.current_position,
|
||||
near);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore API mismatches
|
||||
}
|
||||
}
|
||||
|
||||
private static void Emit(
|
||||
string eventId,
|
||||
float strength,
|
||||
InterestOwnership owns,
|
||||
Actor actor,
|
||||
string labelTemplate)
|
||||
{
|
||||
if (AgentHarness.Busy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string label = (labelTemplate ?? "{a}").Replace("{a}", EventFeedUtil.SafeName(actor));
|
||||
string key = "building:" + eventId + ":" + EventFeedUtil.SafeId(actor);
|
||||
EventFeedUtil.Register(
|
||||
key,
|
||||
"building",
|
||||
"Spectacle",
|
||||
label,
|
||||
eventId,
|
||||
strength,
|
||||
owns,
|
||||
actor.current_position,
|
||||
actor);
|
||||
}
|
||||
}
|
||||
194
IdleSpectator/CatalogCoverageAudit.cs
Normal file
194
IdleSpectator/CatalogCoverageAudit.cs
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
public sealed class CatalogCoverageDiff
|
||||
{
|
||||
public string Domain = "";
|
||||
public int LiveTotal;
|
||||
public int AuthoredTotal;
|
||||
public int Missing;
|
||||
public int Orphan;
|
||||
public readonly List<string> MissingIds = new List<string>();
|
||||
public readonly List<string> OrphanIds = new List<string>();
|
||||
|
||||
public bool Passed => Missing == 0 && Orphan == 0;
|
||||
|
||||
public string Detail =>
|
||||
$"{Domain}: live={LiveTotal} authored={AuthoredTotal} missing={Missing} orphan={Orphan}";
|
||||
}
|
||||
|
||||
/// <summary>Shared live-vs-authored coverage audit used by domain event catalogs.</summary>
|
||||
public static class CatalogCoverageAudit
|
||||
{
|
||||
public static CatalogCoverageDiff Diff(
|
||||
string domain,
|
||||
IEnumerable<string> liveIds,
|
||||
IEnumerable<string> authoredIds)
|
||||
{
|
||||
var live = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var authored = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var diff = new CatalogCoverageDiff { Domain = domain ?? "domain" };
|
||||
|
||||
if (liveIds != null)
|
||||
{
|
||||
foreach (string id in liveIds)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
live.Add(id.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
if (authoredIds != null)
|
||||
{
|
||||
foreach (string id in authoredIds)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
authored.Add(id.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
diff.LiveTotal = live.Count;
|
||||
diff.AuthoredTotal = authored.Count;
|
||||
|
||||
foreach (string id in live)
|
||||
{
|
||||
if (!authored.Contains(id))
|
||||
{
|
||||
diff.Missing++;
|
||||
diff.MissingIds.Add(id);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string id in authored)
|
||||
{
|
||||
if (!live.Contains(id))
|
||||
{
|
||||
diff.Orphan++;
|
||||
diff.OrphanIds.Add(id);
|
||||
}
|
||||
}
|
||||
|
||||
return diff;
|
||||
}
|
||||
|
||||
public static void WriteReport(string outputDirectory, string fileName, string body)
|
||||
{
|
||||
if (string.IsNullOrEmpty(outputDirectory) || string.IsNullOrEmpty(fileName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
File.WriteAllText(Path.Combine(outputDirectory, fileName), body ?? "");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore IO
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class DomainEventAuditResult
|
||||
{
|
||||
public bool Passed;
|
||||
public string Detail = "";
|
||||
}
|
||||
|
||||
/// <summary>Coverage audits for relationship discrete events + live plot/era/book libraries.</summary>
|
||||
public static class DomainEventHarness
|
||||
{
|
||||
public static DomainEventAuditResult RunAudit(string outputDirectory)
|
||||
{
|
||||
var tsv = new StringBuilder();
|
||||
tsv.AppendLine("domain\tid\tauthored\towns_camera\tevent_strength\tnotes");
|
||||
|
||||
CatalogCoverageDiff plots = CatalogCoverageAudit.Diff(
|
||||
"plots",
|
||||
ActivityAssetCatalog.EnumerateLivePlotIds(),
|
||||
PlotInterestCatalog.AuthoredIds);
|
||||
AppendLibraryDomain(tsv, "plots", ActivityAssetCatalog.EnumerateLivePlotIds(), plots, PlotInterestCatalog.GetOrFallback);
|
||||
|
||||
CatalogCoverageDiff eras = CatalogCoverageAudit.Diff(
|
||||
"eras",
|
||||
ActivityAssetCatalog.EnumerateLiveEraIds(),
|
||||
EraInterestCatalog.AuthoredIds);
|
||||
AppendLibraryDomain(tsv, "eras", ActivityAssetCatalog.EnumerateLiveEraIds(), eras, EraInterestCatalog.GetOrFallback);
|
||||
|
||||
CatalogCoverageDiff books = CatalogCoverageAudit.Diff(
|
||||
"books",
|
||||
ActivityAssetCatalog.EnumerateLiveBookTypeIds(),
|
||||
BookInterestCatalog.AuthoredIds);
|
||||
AppendLibraryDomain(tsv, "books", ActivityAssetCatalog.EnumerateLiveBookTypeIds(), books, BookInterestCatalog.GetOrFallback);
|
||||
|
||||
CatalogCoverageDiff traits = CatalogCoverageAudit.Diff(
|
||||
"traits",
|
||||
ActivityAssetCatalog.EnumerateLiveTraitIds(),
|
||||
TraitInterestCatalog.AuthoredIds);
|
||||
AppendLibraryDomain(tsv, "traits", ActivityAssetCatalog.EnumerateLiveTraitIds(), traits, TraitInterestCatalog.GetOrFallback);
|
||||
|
||||
int relMissing = 0;
|
||||
int relTotal = 0;
|
||||
foreach (string id in RelationshipEventCatalog.AuthoredIds)
|
||||
{
|
||||
relTotal++;
|
||||
DiscreteEventEntry entry = RelationshipEventCatalog.GetOrFallback(id);
|
||||
bool ok = RelationshipEventCatalog.HasAuthored(id) && !entry.IsFallback;
|
||||
if (!ok)
|
||||
{
|
||||
relMissing++;
|
||||
}
|
||||
|
||||
tsv.Append("relationship\t").Append(id).Append('\t')
|
||||
.Append(ok ? "1" : "0").Append('\t')
|
||||
.Append(entry.OwnsCamera).Append('\t')
|
||||
.Append(entry.EventStrength.ToString("0.#")).Append('\t')
|
||||
.Append(ok ? "ok" : "missing_authored").AppendLine();
|
||||
}
|
||||
|
||||
CatalogCoverageAudit.WriteReport(outputDirectory, "domain_event_audit.tsv", tsv.ToString());
|
||||
|
||||
bool passed = plots.Passed && eras.Passed && books.Passed && traits.Passed
|
||||
&& relMissing == 0 && relTotal > 0;
|
||||
string detail =
|
||||
$"{plots.Detail}; {eras.Detail}; {books.Detail}; {traits.Detail}; relationship: authored={relTotal} missing={relMissing}";
|
||||
return new DomainEventAuditResult { Passed = passed, Detail = detail };
|
||||
}
|
||||
|
||||
private static void AppendLibraryDomain(
|
||||
StringBuilder tsv,
|
||||
string domain,
|
||||
IEnumerable<string> liveIds,
|
||||
CatalogCoverageDiff diff,
|
||||
Func<string, DiscreteEventEntry> lookup)
|
||||
{
|
||||
foreach (string id in liveIds)
|
||||
{
|
||||
DiscreteEventEntry entry = lookup(id);
|
||||
bool has = !entry.IsFallback;
|
||||
tsv.Append(domain).Append('\t').Append(id).Append('\t')
|
||||
.Append(has ? "1" : "0").Append('\t')
|
||||
.Append(entry.OwnsCamera).Append('\t')
|
||||
.Append(entry.EventStrength.ToString("0.#")).Append('\t')
|
||||
.Append(has ? "ok" : "missing_authored").AppendLine();
|
||||
}
|
||||
|
||||
foreach (string id in diff.OrphanIds)
|
||||
{
|
||||
tsv.Append(domain).Append('\t').Append(id).Append("\t1\t\t0\torphan_authored").AppendLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
68
IdleSpectator/EraInterestCatalog.cs
Normal file
68
IdleSpectator/EraInterestCatalog.cs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>Authored interest overlay for every live era_library asset.</summary>
|
||||
public static class EraInterestCatalog
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
static EraInterestCatalog()
|
||||
{
|
||||
Add("age_hope", 78f, "World", InterestOwnership.Signal, "Age of Hope");
|
||||
Add("age_sun", 80f, "World", InterestOwnership.Signal, "Age of Sun");
|
||||
Add("age_dark", 88f, "World", InterestOwnership.Signal, "Age of Dark");
|
||||
Add("age_tears", 86f, "World", InterestOwnership.Signal, "Age of Tears");
|
||||
Add("age_moon", 82f, "World", InterestOwnership.Signal, "Age of Moon");
|
||||
Add("age_chaos", 92f, "World", InterestOwnership.Signal, "Age of Chaos");
|
||||
Add("age_wonders", 84f, "World", InterestOwnership.Signal, "Age of Wonders");
|
||||
Add("age_ice", 87f, "World", InterestOwnership.Signal, "Age of Ice");
|
||||
Add("age_ash", 90f, "World", InterestOwnership.Signal, "Age of Ash");
|
||||
Add("age_despair", 91f, "World", InterestOwnership.Signal, "Age of Despair");
|
||||
Add("age_unknown", 70f, "World", InterestOwnership.Ambient, "Unknown age");
|
||||
}
|
||||
|
||||
private static void Add(
|
||||
string id,
|
||||
float strength,
|
||||
string category,
|
||||
InterestOwnership owns,
|
||||
string label)
|
||||
{
|
||||
Entries[id] = new DiscreteEventEntry
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = strength,
|
||||
Category = category,
|
||||
OwnsCamera = owns,
|
||||
CreatesInterest = true,
|
||||
LabelTemplate = label
|
||||
};
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds => Entries.Keys;
|
||||
|
||||
public static bool HasAuthored(string id) =>
|
||||
!string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
|
||||
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
|
||||
return new DiscreteEventEntry
|
||||
{
|
||||
Id = key,
|
||||
EventStrength = 70f,
|
||||
Category = "World",
|
||||
OwnsCamera = InterestOwnership.Signal,
|
||||
LabelTemplate = "Era ({id})",
|
||||
IsFallback = true
|
||||
};
|
||||
}
|
||||
}
|
||||
182
IdleSpectator/EventFeedUtil.cs
Normal file
182
IdleSpectator/EventFeedUtil.cs
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>Whether an interest candidate may steal the idle camera.</summary>
|
||||
public enum InterestOwnership
|
||||
{
|
||||
/// <summary>May cut in / own EventLed camera focus.</summary>
|
||||
Signal,
|
||||
/// <summary>Tracked for fill/enrichment; fill-capped so it does not preempt Signal drama.</summary>
|
||||
Ambient
|
||||
}
|
||||
|
||||
/// <summary>Shared registration helpers for domain event feeds.</summary>
|
||||
public static class EventFeedUtil
|
||||
{
|
||||
public static InterestCandidate Register(
|
||||
string key,
|
||||
string source,
|
||||
string category,
|
||||
string label,
|
||||
string assetId,
|
||||
float eventStrength,
|
||||
InterestOwnership ownership,
|
||||
Vector3 position,
|
||||
Actor follow,
|
||||
float minWatch = 4f,
|
||||
float maxWatch = 22f,
|
||||
InterestCompletionKind completion = InterestCompletionKind.FixedDwell)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key) || position == Vector3.zero && follow == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (follow == null && position == Vector3.zero)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Vector3 pos = follow != null ? follow.current_position : position;
|
||||
if (pos == Vector3.zero)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
bool ambient = ownership == InterestOwnership.Ambient;
|
||||
float strength = eventStrength;
|
||||
if (ambient)
|
||||
{
|
||||
strength = Mathf.Min(strength, InterestScoringConfig.W.fillScoreMax - 1f);
|
||||
if (strength < 8f)
|
||||
{
|
||||
strength = 8f;
|
||||
}
|
||||
}
|
||||
|
||||
var candidate = new InterestCandidate
|
||||
{
|
||||
Key = key,
|
||||
LeadKind = ambient ? InterestLeadKind.CharacterLed : InterestLeadKind.EventLed,
|
||||
Category = string.IsNullOrEmpty(category) ? "Event" : category,
|
||||
Source = source ?? "event",
|
||||
EventStrength = strength,
|
||||
CharacterSignificance = ambient ? strength * 0.5f : 0f,
|
||||
VisualConfidence = follow != null ? 0.7f : 0.35f,
|
||||
Position = pos,
|
||||
FollowUnit = follow,
|
||||
SubjectId = follow != null ? SafeId(follow) : 0,
|
||||
Label = label ?? assetId ?? key,
|
||||
AssetId = assetId ?? "",
|
||||
SpeciesId = follow?.asset != null ? follow.asset.id : "",
|
||||
CreatedAt = Time.unscaledTime,
|
||||
LastSeenAt = Time.unscaledTime,
|
||||
ExpiresAt = Time.unscaledTime + (ambient ? 18f : 35f),
|
||||
MinWatch = minWatch,
|
||||
MaxWatch = maxWatch,
|
||||
Completion = completion
|
||||
};
|
||||
InterestScoring.ScoreCheap(candidate);
|
||||
return InterestRegistry.Upsert(candidate);
|
||||
}
|
||||
|
||||
public static long SafeId(Actor actor)
|
||||
{
|
||||
if (actor == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return actor.getID();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public static string SafeName(Actor actor)
|
||||
{
|
||||
if (actor == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return actor.getName() ?? "";
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public static Actor NearestUnit(Vector3 near, float radius = 200f)
|
||||
{
|
||||
try
|
||||
{
|
||||
return WorldActivityScanner.FindNearestAliveUnit(near, radius);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static Actor AnyAliveUnit()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (World.world?.units == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (Actor actor in World.world.units)
|
||||
{
|
||||
if (actor != null && actor.isAlive())
|
||||
{
|
||||
return actor;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static bool TryResolveAnchor(Actor preferred, Vector3 position, out Actor follow, out Vector3 pos)
|
||||
{
|
||||
follow = preferred;
|
||||
pos = position;
|
||||
if (follow != null && follow.isAlive())
|
||||
{
|
||||
pos = follow.current_position;
|
||||
return pos != Vector3.zero || true;
|
||||
}
|
||||
|
||||
if (position != Vector3.zero)
|
||||
{
|
||||
follow = NearestUnit(position) ?? AnyAliveUnit();
|
||||
pos = position;
|
||||
return true;
|
||||
}
|
||||
|
||||
follow = AnyAliveUnit();
|
||||
if (follow == null)
|
||||
{
|
||||
pos = Vector3.zero;
|
||||
return false;
|
||||
}
|
||||
|
||||
pos = follow.current_position;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -55,6 +55,10 @@ internal static class HarnessScenarios
|
|||
return DisasterWarAudit();
|
||||
case "event_coverage":
|
||||
return EventCoverage();
|
||||
case "domain_event_audit":
|
||||
return DomainEventAudit();
|
||||
case "event_tracking":
|
||||
return EventTracking();
|
||||
case "mutation_discovery":
|
||||
return MutationDiscovery();
|
||||
case "chronicle":
|
||||
|
|
@ -1099,6 +1103,53 @@ internal static class HarnessScenarios
|
|||
};
|
||||
}
|
||||
|
||||
private static List<HarnessCommand> DomainEventAudit()
|
||||
{
|
||||
return new List<HarnessCommand>
|
||||
{
|
||||
Step("dea0", "wait_world"),
|
||||
Step("dea1", "assert", expect: "domain_event_audit"),
|
||||
Step("dea2", "snapshot")
|
||||
};
|
||||
}
|
||||
|
||||
private static List<HarnessCommand> EventTracking()
|
||||
{
|
||||
return new List<HarnessCommand>
|
||||
{
|
||||
Step("et0", "dismiss_windows"),
|
||||
Step("et1", "wait_world"),
|
||||
Step("et2", "set_setting", expect: "enabled", value: "true"),
|
||||
Step("et3", "pick_unit", asset: "auto"),
|
||||
Step("et4", "spectator", value: "off"),
|
||||
Step("et5", "spectator", value: "on"),
|
||||
Step("et6", "focus", asset: "auto"),
|
||||
|
||||
Step("et10", "assert", expect: "domain_event_audit"),
|
||||
|
||||
Step("et20", "domain_feed", asset: "relationship", value: "set_lover"),
|
||||
Step("et21", "assert", expect: "interest_has_key", value: "rel:set_lover"),
|
||||
Step("et22", "domain_feed", asset: "plot", value: "summon_meteor_rain"),
|
||||
Step("et23", "assert", expect: "interest_has_key", value: "plot:summon_meteor_rain"),
|
||||
Step("et24", "domain_feed", asset: "era", value: "age_chaos"),
|
||||
Step("et25", "assert", expect: "interest_has_key", value: "era:age_chaos"),
|
||||
Step("et26", "domain_feed", asset: "meta", value: "new_city"),
|
||||
Step("et27", "assert", expect: "interest_has_key", value: "meta:new_city"),
|
||||
Step("et28", "domain_feed", asset: "book", value: "bad_story_about_king"),
|
||||
Step("et29", "assert", expect: "interest_has_key", value: "book:bad_story_about_king"),
|
||||
Step("et30", "domain_feed", asset: "trait", value: "immortal"),
|
||||
Step("et31", "assert", expect: "interest_has_key", value: "trait:immortal"),
|
||||
Step("et32", "domain_feed", asset: "boat"),
|
||||
Step("et33", "assert", expect: "interest_has_key", value: "boat:"),
|
||||
Step("et34", "domain_feed", asset: "building"),
|
||||
Step("et35", "assert", expect: "interest_has_key", value: "building:"),
|
||||
|
||||
Step("et90", "assert", expect: "health"),
|
||||
Step("et91", "assert", expect: "no_bad"),
|
||||
Step("et99", "snapshot")
|
||||
};
|
||||
}
|
||||
|
||||
private static List<HarnessCommand> EventCoverage()
|
||||
{
|
||||
return new List<HarnessCommand>
|
||||
|
|
@ -1116,6 +1167,7 @@ internal static class HarnessScenarios
|
|||
Step("ec11", "assert", expect: "disaster_war_audit"),
|
||||
Step("ec12", "assert", expect: "activity_status_audit"),
|
||||
Step("ec13", "assert", expect: "activity_happiness_audit"),
|
||||
Step("ec14", "assert", expect: "domain_event_audit"),
|
||||
|
||||
// Previously silent-dropped WorldLog disaster id must enter the registry
|
||||
Step("ec20", "world_log_feed", asset: "disaster_tornado"),
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ public static class InterestFeeds
|
|||
unit = message.unit;
|
||||
}
|
||||
|
||||
if (unit == null && position == Vector3.zero)
|
||||
if (!EventFeedUtil.TryResolveAnchor(unit, position, out Actor follow, out Vector3 pos))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -117,30 +117,18 @@ public static class InterestFeeds
|
|||
float boost = PeekCivicBoost(kingdom, assetId);
|
||||
string category = string.IsNullOrEmpty(entry.Category) ? "WorldLog" : entry.Category;
|
||||
|
||||
var candidate = new InterestCandidate
|
||||
{
|
||||
Key = key,
|
||||
LeadKind = InterestLeadKind.EventLed,
|
||||
Category = category,
|
||||
Source = "worldlog",
|
||||
EventStrength = evtSeed + boost,
|
||||
VisualConfidence = unit != null ? 0.7f : 0.4f,
|
||||
Position = unit != null ? unit.current_position : position,
|
||||
FollowUnit = unit,
|
||||
SubjectId = SafeId(unit),
|
||||
Label = entry.MakeLabel(message),
|
||||
AssetId = assetId,
|
||||
KingdomKey = kingdom,
|
||||
SpeciesId = unit?.asset != null ? unit.asset.id : "",
|
||||
CreatedAt = Time.unscaledTime,
|
||||
LastSeenAt = Time.unscaledTime,
|
||||
ExpiresAt = Time.unscaledTime + 40f,
|
||||
MinWatch = entry.MinWatch,
|
||||
MaxWatch = entry.MaxWatch,
|
||||
Completion = InterestCompletionKind.FixedDwell
|
||||
};
|
||||
InterestScoring.ScoreCheap(candidate);
|
||||
InterestRegistry.Upsert(candidate);
|
||||
EventFeedUtil.Register(
|
||||
key,
|
||||
"worldlog",
|
||||
category,
|
||||
entry.MakeLabel(message),
|
||||
assetId,
|
||||
evtSeed + boost,
|
||||
entry.OwnsCamera,
|
||||
pos,
|
||||
follow,
|
||||
entry.MinWatch,
|
||||
entry.MaxWatch);
|
||||
}
|
||||
|
||||
/// <summary>Legacy / discovery / harness enqueue path → registry.</summary>
|
||||
|
|
@ -353,38 +341,34 @@ public static class InterestFeeds
|
|||
long id = SafeId(actor);
|
||||
if (entry.ExtendsGrief || statusId == "crying")
|
||||
{
|
||||
// Extend existing grief candidate rather than spawning a second scene.
|
||||
string griefPrefix = "happiness:death_";
|
||||
ExtendMatching(id, griefPrefix, statusId);
|
||||
return;
|
||||
if (ExtendMatching(id, griefPrefix, statusId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Standalone crying with no grief scene still creates interest.
|
||||
if (statusId != "crying" && entry.ExtendsGrief)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
string phrase = ActivityStatusProse.Phrase(statusId, gained: true, out _);
|
||||
string key = "status:" + statusId + ":" + id;
|
||||
var candidate = new InterestCandidate
|
||||
{
|
||||
Key = key,
|
||||
LeadKind = InterestLeadKind.EventLed,
|
||||
Category = string.IsNullOrEmpty(entry.Category) ? "StatusTransformation" : entry.Category,
|
||||
Source = "status",
|
||||
EventStrength = entry.EventStrength,
|
||||
VisualConfidence = 0.85f,
|
||||
Position = actor.current_position,
|
||||
FollowUnit = actor,
|
||||
SubjectId = id,
|
||||
StatusId = statusId,
|
||||
Label = SafeName(actor) + " · " + phrase,
|
||||
AssetId = actor.asset != null ? actor.asset.id : "",
|
||||
SpeciesId = actor.asset != null ? actor.asset.id : "",
|
||||
CreatedAt = Time.unscaledTime,
|
||||
LastSeenAt = Time.unscaledTime,
|
||||
ExpiresAt = Time.unscaledTime + 18f,
|
||||
MinWatch = 2f,
|
||||
MaxWatch = 22f,
|
||||
Completion = InterestCompletionKind.StatusPhase
|
||||
};
|
||||
InterestScoring.ScoreCheap(candidate);
|
||||
InterestRegistry.Upsert(candidate);
|
||||
EventFeedUtil.Register(
|
||||
key,
|
||||
"status",
|
||||
string.IsNullOrEmpty(entry.Category) ? "StatusTransformation" : entry.Category,
|
||||
SafeName(actor) + " · " + phrase,
|
||||
statusId,
|
||||
entry.EventStrength,
|
||||
entry.OwnsCamera,
|
||||
actor.current_position,
|
||||
actor,
|
||||
2f,
|
||||
22f,
|
||||
InterestCompletionKind.StatusPhase);
|
||||
}
|
||||
|
||||
public static void OnChronicleMilestone(Actor subject, string milestoneKey, Actor related, string label)
|
||||
|
|
@ -660,11 +644,14 @@ public static class InterestFeeds
|
|||
// Ongoing wars from WarManager (complements WorldLog war start/end).
|
||||
WarInterestFeed.Tick();
|
||||
|
||||
// Active plots from PlotManager / World.plots.
|
||||
PlotInterestFeed.Tick();
|
||||
|
||||
// Character-led vignette seeds (not sole one-best monopoly: register best + a few notables).
|
||||
WorldActivityScanner.RegisterTopCharacterCandidates(max: 4);
|
||||
}
|
||||
|
||||
private static void ExtendMatching(long subjectId, string keyPrefix, string statusId)
|
||||
private static bool ExtendMatching(long subjectId, string keyPrefix, string statusId)
|
||||
{
|
||||
var pending = new List<InterestCandidate>(32);
|
||||
InterestRegistry.CopyPending(pending);
|
||||
|
|
@ -682,9 +669,11 @@ public static class InterestFeeds
|
|||
c.LastSeenAt = Time.unscaledTime;
|
||||
c.ExpiresAt = Time.unscaledTime + 20f;
|
||||
InterestRegistry.Upsert(c);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void StampCivicBoost(string kingdom, string effectId, float amount)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using HarmonyLib;
|
||||
using NeoModLoader.api;
|
||||
|
|
@ -56,12 +57,42 @@ public class ModClass : MonoBehaviour, IMod, IConfigurable, ILocalizable
|
|||
InterestScoringConfig.LoadFromModFolder(ModFolder);
|
||||
|
||||
_harmony = new Harmony(pModDecl.UID);
|
||||
_harmony.PatchAll(typeof(ModClass).Assembly);
|
||||
ApplyHarmonyPatches(pModDecl.Name);
|
||||
LogService.LogInfo($"[{pModDecl.Name}]: Harmony patches applied");
|
||||
LogService.LogInfo($"[{pModDecl.Name}]: Press I for Idle Spectator, L for Lore");
|
||||
Chronicle.EnsureWindow();
|
||||
}
|
||||
|
||||
private void ApplyHarmonyPatches(string modName)
|
||||
{
|
||||
Exception firstFailure = null;
|
||||
foreach (Type type in typeof(ModClass).Assembly.GetTypes())
|
||||
{
|
||||
try
|
||||
{
|
||||
new PatchClassProcessor(_harmony, type).Patch();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.LogError($"[{modName}]: Harmony failed on {type.FullName}: {ex}");
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
LogService.LogError($"[{modName}]: Inner: {ex.InnerException}");
|
||||
}
|
||||
|
||||
if (firstFailure == null)
|
||||
{
|
||||
firstFailure = ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (firstFailure != null)
|
||||
{
|
||||
throw firstFailure;
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
AgentHarness.Update();
|
||||
|
|
|
|||
156
IdleSpectator/MetaEventPatches.cs
Normal file
156
IdleSpectator/MetaEventPatches.cs
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
using HarmonyLib;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>Era + meta genesis patches from mutation discovery.</summary>
|
||||
[HarmonyPatch]
|
||||
public static class MetaEventPatches
|
||||
{
|
||||
[HarmonyPatch(typeof(WorldAgeManager), nameof(WorldAgeManager.startNextAge))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixStartNextAge()
|
||||
{
|
||||
MetaInterestFeed.EmitEra(ReadCurrentEraId());
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(CultureManager), nameof(CultureManager.newCulture))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixNewCulture(Culture __result)
|
||||
{
|
||||
EmitNew("new_culture", "Culture", 68f, InterestOwnership.Signal, __result, "New culture: {a}");
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(ReligionManager), nameof(ReligionManager.newReligion))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixNewReligion(Religion __result)
|
||||
{
|
||||
EmitNew("new_religion", "Culture", 72f, InterestOwnership.Signal, __result, "New religion: {a}");
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(LanguageManager), nameof(LanguageManager.newLanguage))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixNewLanguage(Language __result)
|
||||
{
|
||||
EmitNew("new_language", "Culture", 60f, InterestOwnership.Ambient, __result, "New language: {a}");
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(ClanManager), nameof(ClanManager.newClan))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixNewClan(Clan __result)
|
||||
{
|
||||
EmitNew("new_clan", "Politics", 70f, InterestOwnership.Signal, __result, "New clan: {a}");
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(ArmyManager), nameof(ArmyManager.newArmy))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixNewArmy(Army __result)
|
||||
{
|
||||
EmitNew("new_army", "Politics", 66f, InterestOwnership.Ambient, __result, "New army: {a}");
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(AllianceManager), nameof(AllianceManager.newAlliance))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixNewAlliance(Alliance __result)
|
||||
{
|
||||
EmitNew("new_alliance", "Politics", 78f, InterestOwnership.Signal, __result, "New alliance: {a}");
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(CityManager), nameof(CityManager.newCity))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixNewCity(City __result)
|
||||
{
|
||||
EmitNew("new_city", "Politics", 74f, InterestOwnership.Signal, __result, "New city: {a}");
|
||||
}
|
||||
|
||||
private static void EmitNew(
|
||||
string eventId,
|
||||
string category,
|
||||
float strength,
|
||||
InterestOwnership owns,
|
||||
object meta,
|
||||
string labelTemplate)
|
||||
{
|
||||
Actor anchor = TryReadActor(meta) ?? EventFeedUtil.AnyAliveUnit();
|
||||
string name = EventFeedUtil.SafeName(anchor);
|
||||
if (string.IsNullOrEmpty(name) && meta != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
name = meta.GetType().GetMethod("getName")?.Invoke(meta, null) as string
|
||||
?? meta.GetType().GetProperty("name")?.GetValue(meta, null) as string
|
||||
?? "";
|
||||
}
|
||||
catch
|
||||
{
|
||||
name = "";
|
||||
}
|
||||
}
|
||||
|
||||
string label = (labelTemplate ?? "{a}")
|
||||
.Replace("{a}", name ?? "")
|
||||
.Replace("{id}", eventId);
|
||||
MetaInterestFeed.EmitMeta(eventId, category, strength, owns, anchor, label);
|
||||
}
|
||||
|
||||
private static Actor TryReadActor(object meta)
|
||||
{
|
||||
if (meta == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
foreach (string name in new[] { "founder", "creator", "leader", "king", "actor", "unit" })
|
||||
{
|
||||
object value = meta.GetType().GetField(name)?.GetValue(meta)
|
||||
?? meta.GetType().GetProperty(name)?.GetValue(meta, null);
|
||||
if (value is Actor actor && actor.isAlive())
|
||||
{
|
||||
return actor;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string ReadCurrentEraId()
|
||||
{
|
||||
try
|
||||
{
|
||||
object world = World.world;
|
||||
object ageManager = world?.GetType().GetField("era_manager")?.GetValue(world)
|
||||
?? world?.GetType().GetProperty("era_manager")?.GetValue(world, null)
|
||||
?? world?.GetType().GetField("age_manager")?.GetValue(world)
|
||||
?? MapBox.instance?.GetType().GetField("era_manager")?.GetValue(MapBox.instance);
|
||||
if (ageManager == null)
|
||||
{
|
||||
return "age_unknown";
|
||||
}
|
||||
|
||||
object asset = ageManager.GetType().GetMethod("getCurrentAge")?.Invoke(ageManager, null)
|
||||
?? ageManager.GetType().GetProperty("current_age")?.GetValue(ageManager, null)
|
||||
?? ageManager.GetType().GetField("current_age")?.GetValue(ageManager);
|
||||
if (asset != null)
|
||||
{
|
||||
object id = asset.GetType().GetField("id")?.GetValue(asset)
|
||||
?? asset.GetType().GetProperty("id")?.GetValue(asset, null);
|
||||
if (id != null)
|
||||
{
|
||||
return id.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
return "age_unknown";
|
||||
}
|
||||
}
|
||||
73
IdleSpectator/MetaInterestFeed.cs
Normal file
73
IdleSpectator/MetaInterestFeed.cs
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>Registers era and meta-genesis interest candidates.</summary>
|
||||
public static class MetaInterestFeed
|
||||
{
|
||||
public static void EmitEra(string eraId)
|
||||
{
|
||||
if (AgentHarness.Busy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DiscreteEventEntry entry = EraInterestCatalog.GetOrFallback(eraId);
|
||||
if (!entry.CreatesInterest)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Actor follow = EventFeedUtil.AnyAliveUnit();
|
||||
Vector3 pos = follow != null ? follow.current_position : Vector3.zero;
|
||||
if (follow == null && pos == Vector3.zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string key = "era:" + entry.Id;
|
||||
EventFeedUtil.Register(
|
||||
key,
|
||||
"era",
|
||||
entry.Category,
|
||||
entry.MakeLabel("", ""),
|
||||
entry.Id,
|
||||
entry.EventStrength,
|
||||
entry.OwnsCamera,
|
||||
pos,
|
||||
follow,
|
||||
minWatch: 6f,
|
||||
maxWatch: 28f);
|
||||
}
|
||||
|
||||
public static void EmitMeta(string eventId, string category, float strength, InterestOwnership owns, Actor anchor, string label)
|
||||
{
|
||||
if (AgentHarness.Busy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Actor follow = anchor;
|
||||
if (follow == null || !follow.isAlive())
|
||||
{
|
||||
follow = EventFeedUtil.AnyAliveUnit();
|
||||
}
|
||||
|
||||
if (follow == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string key = "meta:" + eventId + ":" + EventFeedUtil.SafeId(follow);
|
||||
EventFeedUtil.Register(
|
||||
key,
|
||||
"meta",
|
||||
category,
|
||||
label,
|
||||
eventId,
|
||||
strength,
|
||||
owns,
|
||||
follow.current_position,
|
||||
follow);
|
||||
}
|
||||
}
|
||||
|
|
@ -292,6 +292,34 @@ public static class MutationDiscoveryHarness
|
|||
.Append("0").Append('\t')
|
||||
.Append("count=" + ids.Count).AppendLine();
|
||||
}
|
||||
|
||||
// Full id lists for priority event catalogs.
|
||||
if (!string.IsNullOrEmpty(outputDirectory)
|
||||
&& (field == "plots_library"
|
||||
|| field == "plot_category_library"
|
||||
|| field == "era_library"
|
||||
|| field == "book_types"
|
||||
|| field == "traits"
|
||||
|| field == "buildings"))
|
||||
{
|
||||
try
|
||||
{
|
||||
var full = new StringBuilder();
|
||||
full.AppendLine("id");
|
||||
for (int s = 0; s < ids.Count; s++)
|
||||
{
|
||||
full.AppendLine(ids[s]);
|
||||
}
|
||||
|
||||
File.WriteAllText(
|
||||
Path.Combine(outputDirectory, "library-" + field + ".tsv"),
|
||||
full.ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// World / MapBox live manager fields
|
||||
|
|
|
|||
102
IdleSpectator/PlotEventPatches.cs
Normal file
102
IdleSpectator/PlotEventPatches.cs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
using HarmonyLib;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>Plot lifecycle patches from mutation discovery.</summary>
|
||||
[HarmonyPatch]
|
||||
public static class PlotEventPatches
|
||||
{
|
||||
[HarmonyPatch(typeof(PlotManager), nameof(PlotManager.newPlot))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixNewPlot(Plot __result)
|
||||
{
|
||||
if (__result == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PlotInterestFeed.EmitFromPlot(__result, "new");
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(PlotManager), nameof(PlotManager.cancelPlot))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixCancelPlot(Plot pPlot)
|
||||
{
|
||||
if (pPlot == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PlotInterestFeed.EmitFromPlot(pPlot, "cancel");
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Actor), nameof(Actor.setPlot))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixSetPlot(Actor __instance, Plot pObject)
|
||||
{
|
||||
if (__instance == null || !__instance.isAlive() || pObject == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PlotInterestFeed.EmitFromPlot(pObject, "join");
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Actor), nameof(Actor.leavePlot))]
|
||||
[HarmonyPrefix]
|
||||
public static void PrefixLeavePlot(Actor __instance, out string __state)
|
||||
{
|
||||
__state = "";
|
||||
try
|
||||
{
|
||||
object plot = __instance?.GetType().GetField("plot")?.GetValue(__instance)
|
||||
?? __instance?.GetType().GetProperty("plot")?.GetValue(__instance, null);
|
||||
if (plot != null)
|
||||
{
|
||||
__state = ReadPlotAssetId(plot);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
__state = "";
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Actor), nameof(Actor.leavePlot))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixLeavePlot(Actor __instance, string __state)
|
||||
{
|
||||
if (__instance == null || !__instance.isAlive() || string.IsNullOrEmpty(__state))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PlotInterestFeed.Emit(__state, __instance, "leave");
|
||||
}
|
||||
|
||||
private static string ReadPlotAssetId(object plot)
|
||||
{
|
||||
try
|
||||
{
|
||||
object asset = plot.GetType().GetField("asset")?.GetValue(plot)
|
||||
?? plot.GetType().GetProperty("asset")?.GetValue(plot, null)
|
||||
?? plot.GetType().GetMethod("getAsset")?.Invoke(plot, null)
|
||||
?? plot.GetType().GetField("plot_asset")?.GetValue(plot);
|
||||
if (asset != null)
|
||||
{
|
||||
object id = asset.GetType().GetField("id")?.GetValue(asset)
|
||||
?? asset.GetType().GetProperty("id")?.GetValue(asset, null);
|
||||
if (id != null)
|
||||
{
|
||||
return id.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
||||
85
IdleSpectator/PlotInterestCatalog.cs
Normal file
85
IdleSpectator/PlotInterestCatalog.cs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>Authored interest overlay for every live plots_library asset.</summary>
|
||||
public static class PlotInterestCatalog
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
static PlotInterestCatalog()
|
||||
{
|
||||
Add("rebellion", 92f, "Politics", InterestOwnership.Signal, "Rebellion plot: {a}");
|
||||
Add("new_war", 94f, "Politics", InterestOwnership.Signal, "War plot: {a}");
|
||||
Add("alliance_create", 80f, "Politics", InterestOwnership.Signal, "Alliance plot: {a}");
|
||||
Add("alliance_join", 72f, "Politics", InterestOwnership.Signal, "Join alliance: {a}");
|
||||
Add("alliance_destroy", 86f, "Politics", InterestOwnership.Signal, "Break alliance: {a}");
|
||||
Add("attacker_stop_war", 78f, "Politics", InterestOwnership.Signal, "End war plot: {a}");
|
||||
Add("new_book", 55f, "Culture", InterestOwnership.Ambient, "Book plot: {a}");
|
||||
Add("new_language", 62f, "Culture", InterestOwnership.Ambient, "Language plot: {a}");
|
||||
Add("new_religion", 70f, "Culture", InterestOwnership.Signal, "Religion plot: {a}");
|
||||
Add("new_culture", 68f, "Culture", InterestOwnership.Signal, "Culture plot: {a}");
|
||||
Add("clan_ascension", 82f, "Politics", InterestOwnership.Signal, "Clan ascension: {a}");
|
||||
Add("culture_divide", 75f, "Culture", InterestOwnership.Signal, "Culture divides: {a}");
|
||||
Add("religion_schism", 84f, "Culture", InterestOwnership.Signal, "Religion schism: {a}");
|
||||
Add("language_divergence", 60f, "Culture", InterestOwnership.Ambient, "Language splits: {a}");
|
||||
Add("summon_meteor_rain", 96f, "Spectacle", InterestOwnership.Signal, "Meteor plot: {a}");
|
||||
Add("summon_earthquake", 95f, "Spectacle", InterestOwnership.Signal, "Earthquake plot: {a}");
|
||||
Add("summon_thunderstorm", 93f, "Spectacle", InterestOwnership.Signal, "Thunderstorm plot: {a}");
|
||||
Add("summon_stormfront", 93f, "Spectacle", InterestOwnership.Signal, "Stormfront plot: {a}");
|
||||
Add("summon_hellstorm", 97f, "Spectacle", InterestOwnership.Signal, "Hellstorm plot: {a}");
|
||||
Add("summon_demons", 96f, "Spectacle", InterestOwnership.Signal, "Summon demons: {a}");
|
||||
Add("summon_angles", 96f, "Spectacle", InterestOwnership.Signal, "Summon angels: {a}");
|
||||
Add("summon_skeletons", 92f, "Spectacle", InterestOwnership.Signal, "Summon skeletons: {a}");
|
||||
Add("summon_living_plants", 90f, "Spectacle", InterestOwnership.Signal, "Summon plants: {a}");
|
||||
Add("big_cast_coffee", 58f, "Spectacle", InterestOwnership.Ambient, "Coffee ritual: {a}");
|
||||
Add("big_cast_bubble_shield", 64f, "Spectacle", InterestOwnership.Ambient, "Bubble shield: {a}");
|
||||
Add("big_cast_madness", 80f, "Spectacle", InterestOwnership.Signal, "Madness cast: {a}");
|
||||
Add("big_cast_slowness", 70f, "Spectacle", InterestOwnership.Ambient, "Slowness cast: {a}");
|
||||
Add("cause_rebellion", 90f, "Politics", InterestOwnership.Signal, "Cause rebellion: {a}");
|
||||
}
|
||||
|
||||
private static void Add(
|
||||
string id,
|
||||
float strength,
|
||||
string category,
|
||||
InterestOwnership owns,
|
||||
string label)
|
||||
{
|
||||
Entries[id] = new DiscreteEventEntry
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = strength,
|
||||
Category = category,
|
||||
OwnsCamera = owns,
|
||||
CreatesInterest = true,
|
||||
LabelTemplate = label
|
||||
};
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds => Entries.Keys;
|
||||
|
||||
public static bool HasAuthored(string id) =>
|
||||
!string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
|
||||
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
|
||||
return new DiscreteEventEntry
|
||||
{
|
||||
Id = key,
|
||||
EventStrength = 55f,
|
||||
Category = "Plot",
|
||||
OwnsCamera = InterestOwnership.Ambient,
|
||||
LabelTemplate = "Plot ({id}): {a}",
|
||||
IsFallback = true
|
||||
};
|
||||
}
|
||||
}
|
||||
182
IdleSpectator/PlotInterestFeed.cs
Normal file
182
IdleSpectator/PlotInterestFeed.cs
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
using System.Collections;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>Registers plot interest from Harmony hooks and World.plots polling.</summary>
|
||||
public static class PlotInterestFeed
|
||||
{
|
||||
public static void Emit(string plotAssetId, Actor subject, string phase)
|
||||
{
|
||||
if (AgentHarness.Busy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DiscreteEventEntry entry = PlotInterestCatalog.GetOrFallback(plotAssetId);
|
||||
if (!entry.CreatesInterest)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Actor follow = subject;
|
||||
if (follow == null || !follow.isAlive())
|
||||
{
|
||||
follow = EventFeedUtil.AnyAliveUnit();
|
||||
}
|
||||
|
||||
if (follow == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string name = EventFeedUtil.SafeName(follow);
|
||||
string label = entry.MakeLabel(name, phase ?? "");
|
||||
string key = "plot:" + entry.Id + ":" + (phase ?? "active") + ":" + EventFeedUtil.SafeId(follow);
|
||||
EventFeedUtil.Register(
|
||||
key,
|
||||
"plot",
|
||||
entry.Category,
|
||||
label,
|
||||
entry.Id,
|
||||
entry.EventStrength,
|
||||
entry.OwnsCamera,
|
||||
follow.current_position,
|
||||
follow);
|
||||
}
|
||||
|
||||
public static void EmitFromPlot(object plot, string phase)
|
||||
{
|
||||
if (plot == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string assetId = ReadPlotAssetId(plot);
|
||||
Actor author = ReadPlotAuthor(plot);
|
||||
Emit(assetId, author, phase);
|
||||
}
|
||||
|
||||
public static void Tick()
|
||||
{
|
||||
if (AgentHarness.Busy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IEnumerable plots = ResolvePlotsEnumerable();
|
||||
if (plots == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int registered = 0;
|
||||
foreach (object plot in plots)
|
||||
{
|
||||
if (plot == null || registered >= 8)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
EmitFromPlot(plot, "active");
|
||||
registered++;
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable ResolvePlotsEnumerable()
|
||||
{
|
||||
try
|
||||
{
|
||||
object world = World.world;
|
||||
if (world == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
object plots = world.GetType().GetField("plots")?.GetValue(world)
|
||||
?? world.GetType().GetProperty("plots")?.GetValue(world, null);
|
||||
if (plots is IEnumerable direct)
|
||||
{
|
||||
return direct;
|
||||
}
|
||||
|
||||
object manager = world.GetType().GetField("plots_manager")?.GetValue(world)
|
||||
?? world.GetType().GetProperty("plots_manager")?.GetValue(world, null)
|
||||
?? typeof(PlotManager);
|
||||
if (manager is System.Type)
|
||||
{
|
||||
manager = null;
|
||||
}
|
||||
|
||||
object mapBox = MapBox.instance;
|
||||
if (manager == null && mapBox != null)
|
||||
{
|
||||
manager = mapBox.GetType().GetField("plots")?.GetValue(mapBox)
|
||||
?? mapBox.GetType().GetProperty("plots")?.GetValue(mapBox, null);
|
||||
}
|
||||
|
||||
if (manager == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
MethodInfo getAll = manager.GetType().GetMethod("getSimpleList")
|
||||
?? manager.GetType().GetMethod("getList")
|
||||
?? manager.GetType().GetMethod("getPlots");
|
||||
if (getAll != null)
|
||||
{
|
||||
return getAll.Invoke(manager, null) as IEnumerable;
|
||||
}
|
||||
|
||||
return manager as IEnumerable;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ReadPlotAssetId(object plot)
|
||||
{
|
||||
try
|
||||
{
|
||||
object asset = plot.GetType().GetField("asset")?.GetValue(plot)
|
||||
?? plot.GetType().GetProperty("asset")?.GetValue(plot, null)
|
||||
?? plot.GetType().GetMethod("getAsset")?.Invoke(plot, null);
|
||||
if (asset != null)
|
||||
{
|
||||
object id = asset.GetType().GetField("id")?.GetValue(asset)
|
||||
?? asset.GetType().GetProperty("id")?.GetValue(asset, null);
|
||||
if (id != null)
|
||||
{
|
||||
return id.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
object typeId = plot.GetType().GetField("type")?.GetValue(plot)
|
||||
?? plot.GetType().GetProperty("type")?.GetValue(plot, null);
|
||||
return typeId?.ToString() ?? "unknown";
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
private static Actor ReadPlotAuthor(object plot)
|
||||
{
|
||||
try
|
||||
{
|
||||
object author = plot.GetType().GetField("author")?.GetValue(plot)
|
||||
?? plot.GetType().GetProperty("author")?.GetValue(plot, null)
|
||||
?? plot.GetType().GetMethod("getAuthor")?.Invoke(plot, null)
|
||||
?? plot.GetType().GetField("founder")?.GetValue(plot);
|
||||
return author as Actor;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
87
IdleSpectator/RelationshipEventCatalog.cs
Normal file
87
IdleSpectator/RelationshipEventCatalog.cs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
public sealed class DiscreteEventEntry
|
||||
{
|
||||
public string Id = "";
|
||||
public float EventStrength = 50f;
|
||||
public string Category = "Event";
|
||||
public InterestOwnership OwnsCamera = InterestOwnership.Signal;
|
||||
public bool CreatesInterest = true;
|
||||
public string LabelTemplate = "{id}";
|
||||
public bool IsFallback;
|
||||
|
||||
public string MakeLabel(string a, string b)
|
||||
{
|
||||
string template = string.IsNullOrEmpty(LabelTemplate) ? "{id}" : LabelTemplate;
|
||||
return template
|
||||
.Replace("{id}", Id ?? "")
|
||||
.Replace("{a}", a ?? "")
|
||||
.Replace("{b}", b ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Authored discrete relationship lifecycle events (not a live asset library).</summary>
|
||||
public static class RelationshipEventCatalog
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
static RelationshipEventCatalog()
|
||||
{
|
||||
Add("set_lover", 72f, "Relationship", InterestOwnership.Signal, "Lovers: {a} + {b}");
|
||||
Add("clear_lover", 55f, "Relationship", InterestOwnership.Signal, "Lover parted: {a}");
|
||||
Add("add_child", 68f, "Relationship", InterestOwnership.Signal, "New child: {a} + {b}");
|
||||
Add("new_family", 70f, "Relationship", InterestOwnership.Signal, "New family: {a}");
|
||||
Add("family_removed", 50f, "Relationship", InterestOwnership.Ambient, "Family ended: {a}");
|
||||
Add("find_lover", 45f, "Relationship", InterestOwnership.Ambient, "Seeking a lover: {a}");
|
||||
Add("family_group_new", 60f, "Relationship", InterestOwnership.Signal, "Family pack forms: {a}");
|
||||
Add("family_group_join", 48f, "Relationship", InterestOwnership.Ambient, "Joins family pack: {a}");
|
||||
Add("family_group_leave", 48f, "Relationship", InterestOwnership.Ambient, "Leaves family pack: {a}");
|
||||
Add("baby_created", 70f, "Relationship", InterestOwnership.Signal, "Baby born: {a}");
|
||||
}
|
||||
|
||||
private static void Add(
|
||||
string id,
|
||||
float strength,
|
||||
string category,
|
||||
InterestOwnership owns,
|
||||
string label)
|
||||
{
|
||||
Entries[id] = new DiscreteEventEntry
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = strength,
|
||||
Category = category,
|
||||
OwnsCamera = owns,
|
||||
CreatesInterest = true,
|
||||
LabelTemplate = label
|
||||
};
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds => Entries.Keys;
|
||||
|
||||
public static bool HasAuthored(string id) =>
|
||||
!string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
|
||||
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
|
||||
return new DiscreteEventEntry
|
||||
{
|
||||
Id = key,
|
||||
EventStrength = 40f,
|
||||
Category = "Relationship",
|
||||
OwnsCamera = InterestOwnership.Ambient,
|
||||
LabelTemplate = "{id}: {a}",
|
||||
IsFallback = true
|
||||
};
|
||||
}
|
||||
}
|
||||
186
IdleSpectator/RelationshipEventPatches.cs
Normal file
186
IdleSpectator/RelationshipEventPatches.cs
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
using System;
|
||||
using ai.behaviours;
|
||||
using HarmonyLib;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>Relationship lifecycle patches from mutation discovery.</summary>
|
||||
[HarmonyPatch]
|
||||
public static class RelationshipEventPatches
|
||||
{
|
||||
[HarmonyPatch(typeof(Actor), nameof(Actor.setLover))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixSetLover(Actor __instance, Actor pActor)
|
||||
{
|
||||
if (__instance == null || !__instance.isAlive())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (pActor == null)
|
||||
{
|
||||
RelationshipInterestFeed.Emit("clear_lover", __instance, null);
|
||||
return;
|
||||
}
|
||||
|
||||
RelationshipInterestFeed.Emit("set_lover", __instance, pActor);
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Actor), nameof(Actor.addChild))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixAddChild(Actor __instance, object pObject)
|
||||
{
|
||||
Actor child = ResolveRelatedActor(pObject);
|
||||
if (__instance == null || child == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RelationshipInterestFeed.Emit("add_child", __instance, child);
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Actor), "addChildSimple")]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixAddChildSimple(Actor __instance, object pObject)
|
||||
{
|
||||
Actor child = ResolveRelatedActor(pObject);
|
||||
if (__instance == null || child == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RelationshipInterestFeed.Emit("add_child", __instance, child);
|
||||
}
|
||||
|
||||
private static Actor ResolveRelatedActor(object raw)
|
||||
{
|
||||
if (raw == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (raw is Actor actor)
|
||||
{
|
||||
return actor;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (raw is BaseSimObject sim && sim.isActor())
|
||||
{
|
||||
return sim.a;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// fall through
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var type = raw.GetType();
|
||||
foreach (string name in new[] { "actor", "Actor", "a", "parent" })
|
||||
{
|
||||
var prop = type.GetProperty(name);
|
||||
if (prop != null && typeof(Actor).IsAssignableFrom(prop.PropertyType))
|
||||
{
|
||||
return prop.GetValue(raw, null) as Actor;
|
||||
}
|
||||
|
||||
var field = type.GetField(name);
|
||||
if (field != null && typeof(Actor).IsAssignableFrom(field.FieldType))
|
||||
{
|
||||
return field.GetValue(raw) as Actor;
|
||||
}
|
||||
}
|
||||
|
||||
var getActor = type.GetMethod("getActor", Type.EmptyTypes)
|
||||
?? type.GetMethod("GetActor", Type.EmptyTypes);
|
||||
if (getActor != null && typeof(Actor).IsAssignableFrom(getActor.ReturnType))
|
||||
{
|
||||
return getActor.Invoke(raw, null) as Actor;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(FamilyManager), nameof(FamilyManager.newFamily))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixNewFamily(Family __result, Actor pActor)
|
||||
{
|
||||
Actor anchor = pActor != null && pActor.isAlive() ? pActor : EventFeedUtil.AnyAliveUnit();
|
||||
RelationshipInterestFeed.EmitFamily("new_family", __result, anchor);
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(FamilyManager), "removeObject", new Type[] { typeof(Family) })]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixRemoveFamily(Family pObject)
|
||||
{
|
||||
RelationshipInterestFeed.EmitFamily("family_removed", pObject, EventFeedUtil.AnyAliveUnit());
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(ActorManager), "createBabyActorFromData")]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixCreateBaby(Actor __result)
|
||||
{
|
||||
if (__result == null || !__result.isAlive())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RelationshipInterestFeed.Emit("baby_created", __result, null);
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(BehFindLover), nameof(BehFindLover.execute))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixFindLover(Actor pActor, BehResult __result)
|
||||
{
|
||||
if (pActor == null || !pActor.isAlive() || __result == BehResult.Stop)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RelationshipInterestFeed.Emit("find_lover", pActor, null);
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(BehFamilyGroupNew), nameof(BehFamilyGroupNew.execute))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixFamilyNew(Actor pActor, BehResult __result)
|
||||
{
|
||||
if (pActor == null || !pActor.isAlive() || __result == BehResult.Stop)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RelationshipInterestFeed.Emit("family_group_new", pActor, null);
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(BehFamilyGroupJoin), nameof(BehFamilyGroupJoin.execute))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixFamilyJoin(Actor pActor, BehResult __result)
|
||||
{
|
||||
if (pActor == null || !pActor.isAlive() || __result == BehResult.Stop)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RelationshipInterestFeed.Emit("family_group_join", pActor, null);
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(BehFamilyGroupLeave), nameof(BehFamilyGroupLeave.execute))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixFamilyLeave(Actor pActor, BehResult __result)
|
||||
{
|
||||
if (pActor == null || !pActor.isAlive() || __result == BehResult.Stop)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RelationshipInterestFeed.Emit("family_group_leave", pActor, null);
|
||||
}
|
||||
}
|
||||
80
IdleSpectator/RelationshipInterestFeed.cs
Normal file
80
IdleSpectator/RelationshipInterestFeed.cs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>Registers relationship lifecycle interest candidates.</summary>
|
||||
public static class RelationshipInterestFeed
|
||||
{
|
||||
public static void Emit(string eventId, Actor subject, Actor related, string labelOverride = null)
|
||||
{
|
||||
if (AgentHarness.Busy || subject == null || !subject.isAlive())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DiscreteEventEntry entry = RelationshipEventCatalog.GetOrFallback(eventId);
|
||||
if (!entry.CreatesInterest)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string a = EventFeedUtil.SafeName(subject);
|
||||
string b = EventFeedUtil.SafeName(related);
|
||||
string label = string.IsNullOrEmpty(labelOverride) ? entry.MakeLabel(a, b) : labelOverride;
|
||||
string key = "rel:" + entry.Id + ":" + EventFeedUtil.SafeId(subject) + ":" + EventFeedUtil.SafeId(related);
|
||||
EventFeedUtil.Register(
|
||||
key,
|
||||
"relationship",
|
||||
entry.Category,
|
||||
label,
|
||||
entry.Id,
|
||||
entry.EventStrength,
|
||||
entry.OwnsCamera,
|
||||
subject.current_position,
|
||||
subject);
|
||||
}
|
||||
|
||||
public static void EmitFamily(string eventId, object family, Actor anchor)
|
||||
{
|
||||
if (AgentHarness.Busy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DiscreteEventEntry entry = RelationshipEventCatalog.GetOrFallback(eventId);
|
||||
Actor follow = anchor;
|
||||
if (follow == null || !follow.isAlive())
|
||||
{
|
||||
follow = EventFeedUtil.AnyAliveUnit();
|
||||
}
|
||||
|
||||
if (follow == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string name = "";
|
||||
try
|
||||
{
|
||||
name = family?.GetType().GetMethod("getName")?.Invoke(family, null) as string
|
||||
?? family?.GetType().GetProperty("name")?.GetValue(family, null) as string
|
||||
?? "";
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
string key = "rel:" + entry.Id + ":" + (name ?? "family");
|
||||
EventFeedUtil.Register(
|
||||
key,
|
||||
"relationship",
|
||||
entry.Category,
|
||||
entry.MakeLabel(name, ""),
|
||||
entry.Id,
|
||||
entry.EventStrength,
|
||||
entry.OwnsCamera,
|
||||
follow.current_position,
|
||||
follow);
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ public sealed class StatusInterestEntry
|
|||
public float EventStrength = 40f;
|
||||
public string Category = "Status";
|
||||
public bool CreatesInterest;
|
||||
public InterestOwnership OwnsCamera = InterestOwnership.Signal;
|
||||
public bool ExtendsGrief;
|
||||
public bool IsFallback;
|
||||
}
|
||||
|
|
@ -52,7 +53,14 @@ public static class StatusInterestCatalog
|
|||
Add("fell_in_love", 55f, "Relationship", true);
|
||||
Add("pregnant", 58f, "LifeChapter", true);
|
||||
Add("pregnant_parthenogenesis", 58f, "LifeChapter", true);
|
||||
Add("crying", 50f, "Grief", true, extendsGrief: true);
|
||||
Add("crying", 50f, "Grief", true, InterestOwnership.Signal, extendsGrief: true);
|
||||
|
||||
// Authored Ambient interest (tracked, fill-only)
|
||||
Add("festive_spirit", 28f, "StatusAmbient", true, InterestOwnership.Ambient);
|
||||
Add("laughing", 26f, "StatusAmbient", true, InterestOwnership.Ambient);
|
||||
Add("singing", 26f, "StatusAmbient", true, InterestOwnership.Ambient);
|
||||
Add("sleeping", 22f, "StatusAmbient", true, InterestOwnership.Ambient);
|
||||
Add("handsome_migrant", 30f, "StatusAmbient", true, InterestOwnership.Ambient);
|
||||
|
||||
// Authored but do not create interest candidates (cooldowns / ambient / brief FX)
|
||||
AddNoInterest("afterglow");
|
||||
|
|
@ -62,22 +70,17 @@ public static class StatusInterestCatalog
|
|||
AddNoInterest("cough");
|
||||
AddNoInterest("dash");
|
||||
AddNoInterest("dodge");
|
||||
AddNoInterest("festive_spirit");
|
||||
AddNoInterest("flicked");
|
||||
AddNoInterest("had_bad_dream");
|
||||
AddNoInterest("had_good_dream");
|
||||
AddNoInterest("had_nightmare");
|
||||
AddNoInterest("handsome_migrant");
|
||||
AddNoInterest("just_ate");
|
||||
AddNoInterest("laughing");
|
||||
AddNoInterest("on_guard");
|
||||
AddNoInterest("recovery_combat_action");
|
||||
AddNoInterest("recovery_plot");
|
||||
AddNoInterest("recovery_social");
|
||||
AddNoInterest("recovery_spell");
|
||||
AddNoInterest("shield");
|
||||
AddNoInterest("singing");
|
||||
AddNoInterest("sleeping");
|
||||
AddNoInterest("slowness");
|
||||
AddNoInterest("spell_boost");
|
||||
AddNoInterest("spell_silence");
|
||||
|
|
@ -92,6 +95,17 @@ public static class StatusInterestCatalog
|
|||
string category,
|
||||
bool createsInterest,
|
||||
bool extendsGrief = false)
|
||||
{
|
||||
Add(id, strength, category, createsInterest, InterestOwnership.Signal, extendsGrief);
|
||||
}
|
||||
|
||||
private static void Add(
|
||||
string id,
|
||||
float strength,
|
||||
string category,
|
||||
bool createsInterest,
|
||||
InterestOwnership ownsCamera,
|
||||
bool extendsGrief = false)
|
||||
{
|
||||
Entries[id] = new StatusInterestEntry
|
||||
{
|
||||
|
|
@ -99,13 +113,14 @@ public static class StatusInterestCatalog
|
|||
EventStrength = strength,
|
||||
Category = category,
|
||||
CreatesInterest = createsInterest,
|
||||
OwnsCamera = ownsCamera,
|
||||
ExtendsGrief = extendsGrief
|
||||
};
|
||||
}
|
||||
|
||||
private static void AddNoInterest(string id)
|
||||
{
|
||||
Add(id, 20f, "StatusAmbient", false);
|
||||
Add(id, 20f, "StatusAmbient", false, InterestOwnership.Ambient);
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds => Entries.Keys;
|
||||
|
|
|
|||
121
IdleSpectator/TraitEventPatches.cs
Normal file
121
IdleSpectator/TraitEventPatches.cs
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
using System;
|
||||
using HarmonyLib;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>Actor trait add/remove interest patches from mutation discovery.</summary>
|
||||
[HarmonyPatch]
|
||||
public static class TraitEventPatches
|
||||
{
|
||||
[HarmonyPatch(typeof(Actor), nameof(Actor.addTrait), new Type[] { typeof(string), typeof(bool) })]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixAddTraitString(Actor __instance, string pTraitID, bool __result)
|
||||
{
|
||||
if (!__result || __instance == null || !__instance.isAlive() || string.IsNullOrEmpty(pTraitID))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Emit(pTraitID, __instance, gained: true);
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Actor), nameof(Actor.addTrait), new Type[] { typeof(ActorTrait), typeof(bool) })]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixAddTraitAsset(Actor __instance, ActorTrait pTrait, bool __result)
|
||||
{
|
||||
if (!__result || __instance == null || !__instance.isAlive() || pTrait == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string id = "";
|
||||
try
|
||||
{
|
||||
id = pTrait.id ?? "";
|
||||
}
|
||||
catch
|
||||
{
|
||||
id = "";
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Emit(id, __instance, gained: true);
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Actor), nameof(Actor.removeTrait), new Type[] { typeof(string) })]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixRemoveTraitString(Actor __instance, string pTraitID)
|
||||
{
|
||||
if (__instance == null || !__instance.isAlive() || string.IsNullOrEmpty(pTraitID))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Emit(pTraitID, __instance, gained: false);
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Actor), nameof(Actor.removeTrait), new Type[] { typeof(ActorTrait) })]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixRemoveTraitAsset(Actor __instance, ActorTrait pTrait, bool __result)
|
||||
{
|
||||
if (!__result || __instance == null || !__instance.isAlive() || pTrait == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string id = "";
|
||||
try
|
||||
{
|
||||
id = pTrait.id ?? "";
|
||||
}
|
||||
catch
|
||||
{
|
||||
id = "";
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Emit(id, __instance, gained: false);
|
||||
}
|
||||
|
||||
private static void Emit(string traitId, Actor actor, bool gained)
|
||||
{
|
||||
if (AgentHarness.Busy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DiscreteEventEntry entry = TraitInterestCatalog.GetOrFallback(traitId);
|
||||
if (!entry.CreatesInterest)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
InterestOwnership owns = entry.OwnsCamera;
|
||||
if (!gained && owns == InterestOwnership.Signal)
|
||||
{
|
||||
owns = InterestOwnership.Ambient;
|
||||
}
|
||||
|
||||
string phase = gained ? "gains" : "loses";
|
||||
string label = EventFeedUtil.SafeName(actor) + " " + phase + " " + entry.Id;
|
||||
string key = "trait:" + entry.Id + ":" + phase + ":" + EventFeedUtil.SafeId(actor);
|
||||
EventFeedUtil.Register(
|
||||
key,
|
||||
"trait",
|
||||
entry.Category,
|
||||
label,
|
||||
entry.Id,
|
||||
entry.EventStrength,
|
||||
owns,
|
||||
actor.current_position,
|
||||
actor);
|
||||
}
|
||||
}
|
||||
168
IdleSpectator/TraitInterestCatalog.cs
Normal file
168
IdleSpectator/TraitInterestCatalog.cs
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>Authored interest overlay for every live traits library asset.</summary>
|
||||
public static class TraitInterestCatalog
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
static TraitInterestCatalog()
|
||||
{
|
||||
Add("zombie", 82f, InterestOwnership.Signal);
|
||||
Add("wise", 42f, InterestOwnership.Ambient);
|
||||
Add("whirlwind", 82f, InterestOwnership.Signal);
|
||||
Add("weightless", 42f, InterestOwnership.Ambient);
|
||||
Add("weak", 42f, InterestOwnership.Ambient);
|
||||
Add("veteran", 42f, InterestOwnership.Ambient);
|
||||
Add("venomous", 82f, InterestOwnership.Signal);
|
||||
Add("unlucky", 42f, InterestOwnership.Ambient);
|
||||
Add("ugly", 42f, InterestOwnership.Ambient);
|
||||
Add("tumor_infection", 82f, InterestOwnership.Signal);
|
||||
Add("tough", 42f, InterestOwnership.Ambient);
|
||||
Add("titan_lungs", 42f, InterestOwnership.Ambient);
|
||||
Add("tiny", 42f, InterestOwnership.Ambient);
|
||||
Add("thorns", 42f, InterestOwnership.Ambient);
|
||||
Add("thief", 42f, InterestOwnership.Ambient);
|
||||
Add("super_health", 42f, InterestOwnership.Ambient);
|
||||
Add("sunblessed", 42f, InterestOwnership.Ambient);
|
||||
Add("stupid", 42f, InterestOwnership.Ambient);
|
||||
Add("strong_minded", 42f, InterestOwnership.Ambient);
|
||||
Add("strong", 42f, InterestOwnership.Ambient);
|
||||
Add("soft_skin", 42f, InterestOwnership.Ambient);
|
||||
Add("slow", 42f, InterestOwnership.Ambient);
|
||||
Add("skin_burns", 42f, InterestOwnership.Ambient);
|
||||
Add("short_sighted", 42f, InterestOwnership.Ambient);
|
||||
Add("shiny", 42f, InterestOwnership.Ambient);
|
||||
Add("scar_of_divinity", 82f, InterestOwnership.Signal);
|
||||
Add("savage", 82f, InterestOwnership.Signal);
|
||||
Add("regeneration", 42f, InterestOwnership.Ambient);
|
||||
Add("pyromaniac", 82f, InterestOwnership.Signal);
|
||||
Add("psychopath", 82f, InterestOwnership.Signal);
|
||||
Add("poisonous", 82f, InterestOwnership.Signal);
|
||||
Add("poison_immune", 42f, InterestOwnership.Ambient);
|
||||
Add("plague", 82f, InterestOwnership.Signal);
|
||||
Add("peaceful", 42f, InterestOwnership.Ambient);
|
||||
Add("paranoid", 42f, InterestOwnership.Ambient);
|
||||
Add("pacifist", 42f, InterestOwnership.Ambient);
|
||||
Add("nightchild", 42f, InterestOwnership.Ambient);
|
||||
Add("mute", 42f, InterestOwnership.Ambient);
|
||||
Add("mush_spores", 82f, InterestOwnership.Signal);
|
||||
Add("moonchild", 42f, InterestOwnership.Ambient);
|
||||
Add("miracle_born", 82f, InterestOwnership.Signal);
|
||||
Add("miracle_bearer", 82f, InterestOwnership.Signal);
|
||||
Add("miner", 42f, InterestOwnership.Ambient);
|
||||
Add("metamorphed", 42f, InterestOwnership.Ambient);
|
||||
Add("mega_heartbeat", 42f, InterestOwnership.Ambient);
|
||||
Add("mageslayer", 82f, InterestOwnership.Signal);
|
||||
Add("madness", 82f, InterestOwnership.Signal);
|
||||
Add("lustful", 42f, InterestOwnership.Ambient);
|
||||
Add("lucky", 42f, InterestOwnership.Ambient);
|
||||
Add("long_liver", 42f, InterestOwnership.Ambient);
|
||||
Add("light_lamp", 42f, InterestOwnership.Ambient);
|
||||
Add("kingslayer", 82f, InterestOwnership.Signal);
|
||||
Add("infertile", 42f, InterestOwnership.Ambient);
|
||||
Add("infected", 82f, InterestOwnership.Signal);
|
||||
Add("immune", 42f, InterestOwnership.Ambient);
|
||||
Add("immortal", 82f, InterestOwnership.Signal);
|
||||
Add("hotheaded", 42f, InterestOwnership.Ambient);
|
||||
Add("honest", 42f, InterestOwnership.Ambient);
|
||||
Add("heliophobia", 42f, InterestOwnership.Ambient);
|
||||
Add("heart_of_wizard", 42f, InterestOwnership.Ambient);
|
||||
Add("healing_aura", 42f, InterestOwnership.Ambient);
|
||||
Add("hard_skin", 42f, InterestOwnership.Ambient);
|
||||
Add("greedy", 42f, InterestOwnership.Ambient);
|
||||
Add("golden_tooth", 42f, InterestOwnership.Ambient);
|
||||
Add("gluttonous", 42f, InterestOwnership.Ambient);
|
||||
Add("giant", 82f, InterestOwnership.Signal);
|
||||
Add("genius", 42f, InterestOwnership.Ambient);
|
||||
Add("freeze_proof", 42f, InterestOwnership.Ambient);
|
||||
Add("fragile_health", 42f, InterestOwnership.Ambient);
|
||||
Add("flower_prints", 42f, InterestOwnership.Ambient);
|
||||
Add("flesh_eater", 82f, InterestOwnership.Signal);
|
||||
Add("fire_proof", 42f, InterestOwnership.Ambient);
|
||||
Add("fire_blood", 82f, InterestOwnership.Signal);
|
||||
Add("fertile", 42f, InterestOwnership.Ambient);
|
||||
Add("fat", 42f, InterestOwnership.Ambient);
|
||||
Add("fast", 42f, InterestOwnership.Ambient);
|
||||
Add("eyepatch", 42f, InterestOwnership.Ambient);
|
||||
Add("evil", 82f, InterestOwnership.Signal);
|
||||
Add("energized", 42f, InterestOwnership.Ambient);
|
||||
Add("eagle_eyed", 42f, InterestOwnership.Ambient);
|
||||
Add("dragonslayer", 82f, InterestOwnership.Signal);
|
||||
Add("dodge", 42f, InterestOwnership.Ambient);
|
||||
Add("desire_harp", 42f, InterestOwnership.Ambient);
|
||||
Add("desire_golden_egg", 42f, InterestOwnership.Ambient);
|
||||
Add("desire_computer", 42f, InterestOwnership.Ambient);
|
||||
Add("desire_alien_mold", 42f, InterestOwnership.Ambient);
|
||||
Add("deflect_projectile", 42f, InterestOwnership.Ambient);
|
||||
Add("deceitful", 42f, InterestOwnership.Ambient);
|
||||
Add("death_nuke", 82f, InterestOwnership.Signal);
|
||||
Add("death_mark", 82f, InterestOwnership.Signal);
|
||||
Add("death_bomb", 82f, InterestOwnership.Signal);
|
||||
Add("dash", 42f, InterestOwnership.Ambient);
|
||||
Add("crippled", 42f, InterestOwnership.Ambient);
|
||||
Add("content", 42f, InterestOwnership.Ambient);
|
||||
Add("contagious", 82f, InterestOwnership.Signal);
|
||||
Add("cold_aura", 82f, InterestOwnership.Signal);
|
||||
Add("clumsy", 42f, InterestOwnership.Ambient);
|
||||
Add("clone", 42f, InterestOwnership.Ambient);
|
||||
Add("chosen_one", 82f, InterestOwnership.Signal);
|
||||
Add("burning_feet", 82f, InterestOwnership.Signal);
|
||||
Add("bubble_defense", 42f, InterestOwnership.Ambient);
|
||||
Add("boosted_vitality", 42f, InterestOwnership.Ambient);
|
||||
Add("bomberman", 82f, InterestOwnership.Signal);
|
||||
Add("boat", 42f, InterestOwnership.Ambient);
|
||||
Add("bloodlust", 82f, InterestOwnership.Signal);
|
||||
Add("block", 42f, InterestOwnership.Ambient);
|
||||
Add("blessed", 42f, InterestOwnership.Ambient);
|
||||
Add("battle_reflexes", 42f, InterestOwnership.Ambient);
|
||||
Add("backstep", 42f, InterestOwnership.Ambient);
|
||||
Add("attractive", 42f, InterestOwnership.Ambient);
|
||||
Add("arcane_reflexes", 42f, InterestOwnership.Ambient);
|
||||
Add("ambitious", 42f, InterestOwnership.Ambient);
|
||||
Add("agile", 42f, InterestOwnership.Ambient);
|
||||
Add("acid_touch", 82f, InterestOwnership.Signal);
|
||||
Add("acid_proof", 42f, InterestOwnership.Ambient);
|
||||
Add("acid_blood", 42f, InterestOwnership.Ambient);
|
||||
}
|
||||
|
||||
private static void Add(string id, float strength, InterestOwnership owns)
|
||||
{
|
||||
Entries[id] = new DiscreteEventEntry
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = strength,
|
||||
Category = "Trait",
|
||||
OwnsCamera = owns,
|
||||
CreatesInterest = true,
|
||||
LabelTemplate = "Trait: {id} ({a})"
|
||||
};
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds => Entries.Keys;
|
||||
|
||||
public static bool HasAuthored(string id) =>
|
||||
!string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
|
||||
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
|
||||
return new DiscreteEventEntry
|
||||
{
|
||||
Id = key,
|
||||
EventStrength = 40f,
|
||||
Category = "Trait",
|
||||
OwnsCamera = InterestOwnership.Ambient,
|
||||
LabelTemplate = "Trait: {id} ({a})",
|
||||
IsFallback = true
|
||||
};
|
||||
}
|
||||
}
|
||||
46
IdleSpectator/WarEventPatches.cs
Normal file
46
IdleSpectator/WarEventPatches.cs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
using HarmonyLib;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>War genesis/end patches from mutation discovery.</summary>
|
||||
[HarmonyPatch]
|
||||
public static class WarEventPatches
|
||||
{
|
||||
[HarmonyPatch(typeof(WarManager), nameof(WarManager.newWar))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixNewWar(War __result)
|
||||
{
|
||||
if (__result == null || AgentHarness.Busy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
WarInterestFeed.EmitFromWarObject(__result, phase: "new");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fallback: poll path still covers ongoing wars.
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(WarManager), nameof(WarManager.endWar))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixEndWar(object[] __args)
|
||||
{
|
||||
if (AgentHarness.Busy || __args == null || __args.Length == 0 || __args[0] == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
WarInterestFeed.EmitFromWarObject(__args[0], phase: "end");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,16 @@ namespace IdleSpectator;
|
|||
/// </summary>
|
||||
public static class WarInterestFeed
|
||||
{
|
||||
public static void EmitFromWarObject(object war, string phase = "active")
|
||||
{
|
||||
if (war == null || AgentHarness.Busy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TryRegisterWar(war, phase);
|
||||
}
|
||||
|
||||
public static void Tick()
|
||||
{
|
||||
if (AgentHarness.Busy)
|
||||
|
|
@ -39,14 +49,14 @@ public static class WarInterestFeed
|
|||
break;
|
||||
}
|
||||
|
||||
if (TryRegisterWar(war))
|
||||
if (TryRegisterWar(war, "active"))
|
||||
{
|
||||
registered++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryRegisterWar(object war)
|
||||
private static bool TryRegisterWar(object war, string phase)
|
||||
{
|
||||
string warTypeId = ReadWarTypeId(war);
|
||||
WarTypeInterestEntry entry = WarTypeInterestCatalog.GetOrFallback(warTypeId);
|
||||
|
|
@ -74,31 +84,37 @@ public static class WarInterestFeed
|
|||
return false;
|
||||
}
|
||||
|
||||
string key = "war:" + (ReadString(war, "id") ?? labelPair) + ":" + (warTypeId ?? "normal");
|
||||
string phaseKey = string.IsNullOrEmpty(phase) ? "active" : phase;
|
||||
string key = "war:" + phaseKey + ":" + (ReadString(war, "id") ?? labelPair) + ":" + (warTypeId ?? "normal");
|
||||
string label = WarTypeInterestCatalog.MakeLabel(entry, labelPair);
|
||||
var candidate = new InterestCandidate
|
||||
if (phaseKey == "new")
|
||||
{
|
||||
Key = key,
|
||||
LeadKind = InterestLeadKind.EventLed,
|
||||
Category = entry.Category,
|
||||
Source = "war",
|
||||
EventStrength = entry.EventStrength,
|
||||
VisualConfidence = follow != null ? 0.65f : 0.35f,
|
||||
Position = position,
|
||||
FollowUnit = follow,
|
||||
SubjectId = follow != null ? SafeActorId(follow) : 0,
|
||||
Label = label,
|
||||
AssetId = warTypeId ?? "war",
|
||||
KingdomKey = attackerName,
|
||||
CreatedAt = Time.unscaledTime,
|
||||
LastSeenAt = Time.unscaledTime,
|
||||
ExpiresAt = Time.unscaledTime + 25f,
|
||||
MinWatch = 6f,
|
||||
MaxWatch = 30f,
|
||||
Completion = InterestCompletionKind.FixedDwell
|
||||
};
|
||||
InterestScoring.ScoreCheap(candidate);
|
||||
InterestRegistry.Upsert(candidate);
|
||||
label = "War begins: " + labelPair;
|
||||
}
|
||||
else if (phaseKey == "end")
|
||||
{
|
||||
label = "War ends: " + labelPair;
|
||||
}
|
||||
|
||||
float strength = entry.EventStrength;
|
||||
if (phaseKey == "end")
|
||||
{
|
||||
strength = Mathf.Max(60f, strength - 10f);
|
||||
}
|
||||
|
||||
InterestOwnership owns = InterestOwnership.Signal;
|
||||
EventFeedUtil.Register(
|
||||
key,
|
||||
"war",
|
||||
entry.Category,
|
||||
label,
|
||||
warTypeId ?? "war",
|
||||
strength,
|
||||
owns,
|
||||
position,
|
||||
follow,
|
||||
minWatch: 6f,
|
||||
maxWatch: 30f);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ public sealed class WorldLogEventEntry
|
|||
public float EventStrength = 40f;
|
||||
public string Category = "WorldLog";
|
||||
public bool CreatesInterest = true;
|
||||
public InterestOwnership OwnsCamera = InterestOwnership.Signal;
|
||||
public bool ChronicleEligible;
|
||||
public float MinWatch = 6f;
|
||||
public float MaxWatch = 28f;
|
||||
|
|
@ -98,7 +99,7 @@ public static class WorldLogEventCatalog
|
|||
Add("disaster_mad_thoughts", 90f, "Disaster", true, "Mad thoughts: {a}", 8f, 35f);
|
||||
|
||||
// Harness / internal - authored but never owns camera interest
|
||||
Add("auto_tester", 10f, "Harness", false, "Auto tester: {a}", 2f, 6f);
|
||||
Add("auto_tester", 10f, "Harness", false, InterestOwnership.Ambient, "Auto tester: {a}", 2f, 6f);
|
||||
}
|
||||
|
||||
private static void Add(
|
||||
|
|
@ -109,6 +110,19 @@ public static class WorldLogEventCatalog
|
|||
string labelTemplate,
|
||||
float minWatch,
|
||||
float maxWatch)
|
||||
{
|
||||
Add(id, strength, category, createsInterest, InterestOwnership.Signal, labelTemplate, minWatch, maxWatch);
|
||||
}
|
||||
|
||||
private static void Add(
|
||||
string id,
|
||||
float strength,
|
||||
string category,
|
||||
bool createsInterest,
|
||||
InterestOwnership ownsCamera,
|
||||
string labelTemplate,
|
||||
float minWatch,
|
||||
float maxWatch)
|
||||
{
|
||||
Entries[id] = new WorldLogEventEntry
|
||||
{
|
||||
|
|
@ -116,6 +130,7 @@ public static class WorldLogEventCatalog
|
|||
EventStrength = strength,
|
||||
Category = category,
|
||||
CreatesInterest = createsInterest,
|
||||
OwnsCamera = ownsCamera,
|
||||
ChronicleEligible = createsInterest && strength >= 65f,
|
||||
LabelTemplate = labelTemplate,
|
||||
MinWatch = minWatch,
|
||||
|
|
@ -163,6 +178,7 @@ public static class WorldLogEventCatalog
|
|||
EventStrength = strength,
|
||||
Category = category,
|
||||
CreatesInterest = true,
|
||||
OwnsCamera = InterestOwnership.Signal,
|
||||
ChronicleEligible = strength >= 65f,
|
||||
LabelTemplate = "{id}: {a}",
|
||||
MinWatch = strength >= 90f ? 8f : 6f,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "IdleSpectator",
|
||||
"author": "dazed",
|
||||
"version": "0.16.11",
|
||||
"version": "0.16.32",
|
||||
"description": "AFK Idle Spectator (I) + Lore (L). Event-centered scene director + happiness Activity/Life logging.",
|
||||
"GUID": "com.dazed.idlespectator"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,10 +30,16 @@ worldbox_running() {
|
|||
|
||||
mod_ready() {
|
||||
[[ -f "$LOG" ]] || return 1
|
||||
# Compile errors (NML prints error CS during Compile Mod IdleSpectator).
|
||||
if rg -q "error CS" "$LOG" 2>/dev/null \
|
||||
&& rg -q "Compile Mod IdleSpectator|IdleSpectator.*error CS|error CS.*IdleSpectator" "$LOG" 2>/dev/null; then
|
||||
return 2
|
||||
fi
|
||||
# Harmony PatchAll failures disable the mod without a CS error.
|
||||
if rg -q "IdleSpectator has been disabled due to an error" "$LOG" 2>/dev/null \
|
||||
|| rg -q "\[IdleSpectator\]: Harmony failed" "$LOG" 2>/dev/null; then
|
||||
return 3
|
||||
fi
|
||||
rg -q "\[IdleSpectator\]: Harmony patches applied" "$LOG" 2>/dev/null
|
||||
}
|
||||
|
||||
|
|
@ -103,6 +109,11 @@ cmd_start() {
|
|||
rg -n "error CS|Compile Mod IdleSpectator" "$LOG" 2>/dev/null | tail -n 40 >&2 || true
|
||||
return 1
|
||||
fi
|
||||
if (( ready_rc == 3 )); then
|
||||
echo "IdleSpectator Harmony failed - see $LOG" >&2
|
||||
rg -n "Harmony failed|Parameter \"|IdleSpectator has been disabled|IL Compile Error" "$LOG" 2>/dev/null | tail -n 40 >&2 || true
|
||||
return 1
|
||||
fi
|
||||
printf '.'
|
||||
else
|
||||
printf 'o'
|
||||
|
|
|
|||
Loading…
Reference in a new issue