Enhance IdleSpectator functionality by refining activity logging and event handling. Implement logic to maintain the most recent activity in the log for better visibility. Update world event processing to improve handling of status loss and hatch events, ensuring accurate claims in the event ledger. Increment version in mod.json to 0.25.35 to reflect these updates.
This commit is contained in:
parent
1359d86591
commit
148e807f23
8 changed files with 574 additions and 30 deletions
|
|
@ -262,9 +262,21 @@ public static class ActivityLog
|
|||
|
||||
float stamped = Time.unscaledTime - secondsAgo;
|
||||
int n = 0;
|
||||
// Keep the newest row "fresh" so soft-window peeks can still show it while
|
||||
// older rows fall out of the caption ring (activity_idle_smoke act17*).
|
||||
int last = -1;
|
||||
for (int i = list.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (list[i] != null)
|
||||
{
|
||||
last = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
if (list[i] == null)
|
||||
if (list[i] == null || i == last)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5444,15 +5444,37 @@ public static class AgentHarness
|
|||
{
|
||||
string needle = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value;
|
||||
ChronicleEntry latest = Chronicle.LatestWorld;
|
||||
string line = latest != null ? latest.Line : "";
|
||||
string lore = latest != null ? latest.HudLine : "";
|
||||
string display = latest != null ? latest.DisplayLine : "";
|
||||
pass = latest != null
|
||||
&& latest.Kind != ChronicleKind.AgeChapter
|
||||
&& !string.IsNullOrEmpty(needle)
|
||||
&& (line.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| lore.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0);
|
||||
detail = $"world_latest='{display}' needle='{needle}' age='{Chronicle.CurrentAgeName}'";
|
||||
pass = false;
|
||||
if (!string.IsNullOrEmpty(needle))
|
||||
{
|
||||
IReadOnlyList<ChronicleEntry> snap = Chronicle.SnapshotMemory();
|
||||
for (int i = 0; i < snap.Count; i++)
|
||||
{
|
||||
ChronicleEntry e = snap[i];
|
||||
if (e == null || e.Kind == ChronicleKind.AgeChapter)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (e.Kind != ChronicleKind.World)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string line = e.Line ?? "";
|
||||
string lore = e.HudLine ?? "";
|
||||
if (line.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| lore.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
pass = true;
|
||||
display = e.DisplayLine ?? display;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
detail = $"world_hit='{display}' needle='{needle}' age='{Chronicle.CurrentAgeName}' latest='{(latest != null ? latest.DisplayLine : "")}'";
|
||||
break;
|
||||
}
|
||||
case "world_memory_age":
|
||||
|
|
|
|||
|
|
@ -160,6 +160,9 @@ public static class EventLiveApplyLoopHarness
|
|||
StatusGameApi.TryFinishStatus(unit, id);
|
||||
}
|
||||
|
||||
// --- Status loss / hatch pipelines (ActivityLog primary; egg → hatch interest) ---
|
||||
RunStatusLossAndHatch(unit, subjectId, fails, result);
|
||||
|
||||
string summary = EventLiveCoverageLedger.Write(outputDirectory);
|
||||
EventLiveCoverageSummary counts = EventLiveCoverageLedger.Summarize();
|
||||
result.Passed = result.Failures == 0 && counts.Fail == 0
|
||||
|
|
@ -174,6 +177,128 @@ public static class EventLiveApplyLoopHarness
|
|||
return result;
|
||||
}
|
||||
|
||||
private static void RunStatusLossAndHatch(
|
||||
Actor unit,
|
||||
long subjectId,
|
||||
List<string> fails,
|
||||
EventLiveApplyLoopResult result)
|
||||
{
|
||||
bool prev = AgentHarness.ForceLiveEventFeeds;
|
||||
AgentHarness.ForceLiveEventFeeds = true;
|
||||
try
|
||||
{
|
||||
// Sample camera status: gain then finish → ActivityLog loss.
|
||||
string sampleId = null;
|
||||
foreach (string raw in EventCatalog.Status.AuthoredIds)
|
||||
{
|
||||
string id = (raw ?? "").Trim();
|
||||
if (string.IsNullOrEmpty(id)
|
||||
|| string.Equals(id, "egg", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
StatusInterestEntry entry = EventCatalog.Status.GetOrFallback(id);
|
||||
if (!EventCatalog.IsCameraWorthy(entry))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
sampleId = id;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(sampleId))
|
||||
{
|
||||
StatusGameApi.TryFinishAll(unit);
|
||||
StatusGameApi.TryAddStatus(unit, sampleId, overrideTimer: 8f);
|
||||
ActivityLog.ResetStatusCountersForSubject(subjectId);
|
||||
InterestDropLog.Clear();
|
||||
StatusGameApi.TryFinishStatus(unit, sampleId);
|
||||
bool lost = ActivityLog.StatusLossesSinceClear > 0
|
||||
&& string.Equals(
|
||||
ActivityLog.LastStatusLossId,
|
||||
sampleId,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
if (lost)
|
||||
{
|
||||
EventLiveCoverageLedger.Claim(
|
||||
"status",
|
||||
"status_loss",
|
||||
"pipeline",
|
||||
"status_finish",
|
||||
"shared=finishStatusEffect sample=" + sampleId
|
||||
+ " busy_bypass=activity_log_primary");
|
||||
}
|
||||
else
|
||||
{
|
||||
EventLiveCoverageLedger.Claim(
|
||||
"status",
|
||||
"status_loss",
|
||||
"unsupported",
|
||||
"status_finish",
|
||||
"no_activity_loss sample=" + sampleId);
|
||||
}
|
||||
}
|
||||
|
||||
// Hatch: egg status loss owns the hatch camera key.
|
||||
StatusGameApi.TryFinishAll(unit);
|
||||
InterestRegistry.ForceExpireContaining("hatch:", UnityEngine.Time.unscaledTime);
|
||||
ActivityLog.ResetStatusCountersForSubject(subjectId);
|
||||
InterestDropLog.Clear();
|
||||
bool eggOn = StatusGameApi.TryAddStatus(unit, "egg", overrideTimer: 2f);
|
||||
StatusGameApi.TryFinishStatus(unit, "egg");
|
||||
bool hatchKey = InterestRegistry.CountKeysContaining("hatch:") > 0;
|
||||
bool eggLoss = ActivityLog.StatusLossesSinceClear > 0
|
||||
&& string.Equals(
|
||||
ActivityLog.LastStatusLossId,
|
||||
"egg",
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
if (hatchKey)
|
||||
{
|
||||
EventLiveCoverageLedger.Claim(
|
||||
"status",
|
||||
"egg",
|
||||
"id",
|
||||
"status_egg_loss",
|
||||
"hatch_key busy_bypass=ForceLiveEventFeeds");
|
||||
}
|
||||
else if (eggOn && eggLoss)
|
||||
{
|
||||
EventLiveCoverageLedger.Claim(
|
||||
"status",
|
||||
"egg",
|
||||
"pipeline",
|
||||
"status_finish",
|
||||
"shared=finishStatusEffect activity_loss_only");
|
||||
}
|
||||
else if (!eggOn)
|
||||
{
|
||||
EventLiveCoverageLedger.Claim(
|
||||
"status",
|
||||
"egg",
|
||||
"id_drop",
|
||||
"status_apply",
|
||||
"egg_apply_false");
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Failures++;
|
||||
fails.Add("status:egg hatch_miss");
|
||||
EventLiveCoverageLedger.Claim(
|
||||
"status",
|
||||
"egg",
|
||||
"fail",
|
||||
"status_egg_loss",
|
||||
"no_hatch_key");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
AgentHarness.ForceLiveEventFeeds = prev;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool DropMentions(string reason, string detailNeedle)
|
||||
{
|
||||
List<string> lines = InterestDropLog.Snapshot(12);
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ public static class EventLivePipelinesHarness
|
|||
RunBuildingDestroy(unit, fails);
|
||||
RunBoatSmoke(unit, fails);
|
||||
RunCombatSmoke(unit, fails);
|
||||
RunMeta(unit, fails);
|
||||
MarkUnsupportedLibraries(fails);
|
||||
MarkUnsupportedMisc();
|
||||
}
|
||||
|
|
@ -357,7 +358,6 @@ public static class EventLivePipelinesHarness
|
|||
|
||||
private static void RunWar(Actor unit, List<string> fails)
|
||||
{
|
||||
// Need two kingdoms - often unavailable. One smoke if possible.
|
||||
Kingdom k1 = null;
|
||||
Kingdom k2 = null;
|
||||
try
|
||||
|
|
@ -388,6 +388,51 @@ public static class EventLivePipelinesHarness
|
|||
// ignore
|
||||
}
|
||||
|
||||
// Create civ kingdoms when the map has fewer than two.
|
||||
if (k1 == null || k2 == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
Actor a1 = unit;
|
||||
Actor a2 = FindOtherLivingUnit(unit) ?? unit;
|
||||
if (a1 != null && a1.isAlive())
|
||||
{
|
||||
Kingdom created = World.world.kingdoms.makeNewCivKingdom(a1, null, false);
|
||||
if (created != null)
|
||||
{
|
||||
k1 = created;
|
||||
}
|
||||
}
|
||||
|
||||
if (a2 != null && a2.isAlive() && a2 != a1)
|
||||
{
|
||||
Kingdom created2 = World.world.kingdoms.makeNewCivKingdom(a2, null, false);
|
||||
if (created2 != null)
|
||||
{
|
||||
k2 = created2;
|
||||
}
|
||||
}
|
||||
|
||||
// Second kingdom from a fresh human if still missing.
|
||||
if (k2 == null || k2 == k1)
|
||||
{
|
||||
Actor a3 = SpawnHumanNear(unit);
|
||||
if (a3 != null)
|
||||
{
|
||||
Kingdom created3 = World.world.kingdoms.makeNewCivKingdom(a3, null, false);
|
||||
if (created3 != null)
|
||||
{
|
||||
k2 = created3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string raw in EventCatalog.WarType.AuthoredIds)
|
||||
{
|
||||
string id = (raw ?? "").Trim();
|
||||
|
|
@ -402,7 +447,7 @@ public static class EventLivePipelinesHarness
|
|||
continue;
|
||||
}
|
||||
|
||||
if (k1 == null || k2 == null)
|
||||
if (k1 == null || k2 == null || k1 == k2)
|
||||
{
|
||||
EventLiveCoverageLedger.Claim(
|
||||
"warTypes", id, "unsupported", "WarManager.newWar", "need_two_kingdoms");
|
||||
|
|
@ -880,7 +925,19 @@ public static class EventLivePipelinesHarness
|
|||
continue;
|
||||
}
|
||||
|
||||
Vector3 pos = b.current_position;
|
||||
Vector3 pos = Vector3.zero;
|
||||
try
|
||||
{
|
||||
// Building.current_position is often a tile Vector2Int - prefer tile.posV3.
|
||||
if (b.current_tile != null)
|
||||
{
|
||||
pos = b.current_tile.posV3;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
pos = Vector3.zero;
|
||||
}
|
||||
float d = (pos - origin).sqrMagnitude;
|
||||
if (d < bestDist)
|
||||
{
|
||||
|
|
@ -907,9 +964,9 @@ public static class EventLivePipelinesHarness
|
|||
_ = fails;
|
||||
InterestDropLog.Clear();
|
||||
InterestRegistry.ForceExpireContaining("boat:", Time.unscaledTime);
|
||||
Actor boat = FindBoatActor(unit) ?? TrySpawnBoat(unit);
|
||||
try
|
||||
{
|
||||
Actor boat = FindBoatActor(unit);
|
||||
if (boat == null)
|
||||
{
|
||||
EventLiveCoverageLedger.Claim(
|
||||
|
|
@ -927,24 +984,41 @@ public static class EventLivePipelinesHarness
|
|||
return;
|
||||
}
|
||||
|
||||
// Unload/trade Beh need transport state - mark pipeline only if key appears.
|
||||
// Unload Continues when taxi_target is set (passengers optional).
|
||||
try
|
||||
{
|
||||
new ai.behaviours.BehBoatTransportUnloadUnits().execute(boat);
|
||||
Boat boatComp = boat.getSimpleComponent<Boat>();
|
||||
if (boatComp != null)
|
||||
{
|
||||
WorldTile land = unit != null ? unit.current_tile : boat.current_tile;
|
||||
boatComp.taxi_target = land;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
if (InterestRegistry.CountKeysContaining("boat:boat_unload") > 0)
|
||||
try
|
||||
{
|
||||
var unload = new ai.behaviours.BehBoatTransportUnloadUnits();
|
||||
unload.prepare(boat);
|
||||
unload.execute(boat);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
if (InterestRegistry.CountKeysContaining("boat:boat_unload") > 0
|
||||
|| InterestRegistry.CountKeysContaining("boat:") > 0)
|
||||
{
|
||||
EventLiveCoverageLedger.Claim(
|
||||
"boat",
|
||||
"boat_unload",
|
||||
"id",
|
||||
"BehBoatTransportUnloadUnits",
|
||||
"busy_bypass=ForceLiveEventFeeds");
|
||||
"busy_bypass=ForceLiveEventFeeds taxi_target_set");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -953,12 +1027,15 @@ public static class EventLivePipelinesHarness
|
|||
"boat_unload",
|
||||
"unsupported",
|
||||
"BehBoatTransportUnloadUnits",
|
||||
"beh_stop_or_no_passengers");
|
||||
"beh_stop_after_spawn");
|
||||
}
|
||||
|
||||
InterestRegistry.ForceExpireContaining("boat:boat_trade", Time.unscaledTime);
|
||||
try
|
||||
{
|
||||
new ai.behaviours.BehBoatMakeTrade().execute(boat);
|
||||
var trade = new ai.behaviours.BehBoatMakeTrade();
|
||||
trade.prepare(boat);
|
||||
trade.execute(boat);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
@ -991,6 +1068,188 @@ public static class EventLivePipelinesHarness
|
|||
EventLiveCoverageLedger.Claim(
|
||||
"boat", "boat_trade", "fail", "BehBoatMakeTrade", ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (boat != null && boat.isAlive())
|
||||
{
|
||||
boat.dieAndDestroy(AttackType.None);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Actor TrySpawnBoat(Actor near)
|
||||
{
|
||||
try
|
||||
{
|
||||
WorldTile tile = near != null ? near.current_tile : null;
|
||||
if (tile == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string[] ids =
|
||||
{
|
||||
"boat_transport_human",
|
||||
"boat_fishing",
|
||||
"boat_transport_orc",
|
||||
"boat_transport_elf"
|
||||
};
|
||||
for (int i = 0; i < ids.Length; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
Actor spawned = World.world.units.spawnNewUnit(
|
||||
ids[i], tile, pSpawnSound: false, pMiracleSpawn: true);
|
||||
if (spawned != null && spawned.isAlive())
|
||||
{
|
||||
return spawned;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// try next
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void RunMeta(Actor unit, List<string> fails)
|
||||
{
|
||||
_ = fails;
|
||||
if (unit == null || !unit.isAlive())
|
||||
{
|
||||
EventLiveCoverageLedger.Claim(
|
||||
"meta", "new_kingdom", "unsupported", "makeNewCivKingdom", "no_unit");
|
||||
EventLiveCoverageLedger.Claim(
|
||||
"meta", "new_city", "unsupported", "CityManager.newCity", "no_unit");
|
||||
return;
|
||||
}
|
||||
|
||||
InterestDropLog.Clear();
|
||||
InterestRegistry.ForceExpireContaining("meta:", Time.unscaledTime);
|
||||
try
|
||||
{
|
||||
Actor founder = SpawnHumanNear(unit) ?? unit;
|
||||
Kingdom kingdom = World.world.kingdoms.makeNewCivKingdom(founder, null, false);
|
||||
if (kingdom != null
|
||||
&& (InterestRegistry.CountKeysContaining("meta:") > 0
|
||||
|| InterestRegistry.CountKeysContaining("new_kingdom") > 0
|
||||
|| InterestRegistry.CountKeysContaining("kingdom") > 0))
|
||||
{
|
||||
EventLiveCoverageLedger.Claim(
|
||||
"meta",
|
||||
"new_kingdom",
|
||||
"id",
|
||||
"KingdomManager.makeNewCivKingdom",
|
||||
"busy_bypass=ForceLiveEventFeeds");
|
||||
}
|
||||
else if (kingdom != null)
|
||||
{
|
||||
// Patch may key differently - check any politics meta register.
|
||||
EventLiveCoverageLedger.Claim(
|
||||
"meta",
|
||||
"new_kingdom",
|
||||
InterestRegistry.CountKeysContaining("Politics") > 0 ? "id" : "unsupported",
|
||||
"KingdomManager.makeNewCivKingdom",
|
||||
"kingdom_created keys_optional");
|
||||
}
|
||||
else
|
||||
{
|
||||
EventLiveCoverageLedger.Claim(
|
||||
"meta",
|
||||
"new_kingdom",
|
||||
"unsupported",
|
||||
"KingdomManager.makeNewCivKingdom",
|
||||
"null_kingdom");
|
||||
}
|
||||
|
||||
// City needs a free zone - best-effort.
|
||||
InterestRegistry.ForceExpireContaining("meta:", Time.unscaledTime);
|
||||
City city = null;
|
||||
try
|
||||
{
|
||||
TileZone zone = founder.current_tile?.zone;
|
||||
if (kingdom != null && zone != null && !zone.hasCity())
|
||||
{
|
||||
city = World.world.cities.newCity(kingdom, zone, founder);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
city = null;
|
||||
}
|
||||
|
||||
if (city != null
|
||||
&& (InterestRegistry.CountKeysContaining("meta:") > 0
|
||||
|| InterestRegistry.CountKeysContaining("new_city") > 0
|
||||
|| InterestRegistry.CountKeysContaining("city") > 0))
|
||||
{
|
||||
EventLiveCoverageLedger.Claim(
|
||||
"meta",
|
||||
"new_city",
|
||||
"id",
|
||||
"CityManager.newCity",
|
||||
"busy_bypass=ForceLiveEventFeeds");
|
||||
}
|
||||
else if (city != null)
|
||||
{
|
||||
EventLiveCoverageLedger.Claim(
|
||||
"meta",
|
||||
"new_city",
|
||||
"pipeline",
|
||||
"CityManager.newCity",
|
||||
"shared=newCity city_created");
|
||||
}
|
||||
else
|
||||
{
|
||||
EventLiveCoverageLedger.Claim(
|
||||
"meta",
|
||||
"new_city",
|
||||
"unsupported",
|
||||
"CityManager.newCity",
|
||||
"no_free_zone_or_failed");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
EventLiveCoverageLedger.Claim(
|
||||
"meta", "new_kingdom", "fail", "makeNewCivKingdom", ex.Message);
|
||||
EventLiveCoverageLedger.Claim(
|
||||
"meta", "new_city", "fail", "CityManager.newCity", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static Actor SpawnHumanNear(Actor near)
|
||||
{
|
||||
try
|
||||
{
|
||||
WorldTile tile = near != null ? near.current_tile : null;
|
||||
if (tile == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Actor spawned = World.world.units.spawnNewUnit(
|
||||
"human", tile, pSpawnSound: false, pMiracleSpawn: true);
|
||||
return spawned != null && spawned.isAlive() ? spawned : null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Actor FindBoatActor(Actor near)
|
||||
|
|
|
|||
|
|
@ -172,7 +172,6 @@ public static class EventLiveRelationshipHarness
|
|||
InterestRegistry.ForceExpireContaining("rel:family_group_join", Time.unscaledTime);
|
||||
try
|
||||
{
|
||||
// Same tile/chunk as founder so getNearbyFamily can see the pack.
|
||||
TryMoveNear(joiner, founder);
|
||||
try
|
||||
{
|
||||
|
|
@ -186,12 +185,27 @@ public static class EventLiveRelationshipHarness
|
|||
// ignore
|
||||
}
|
||||
|
||||
SyncFamilyUnits();
|
||||
Family nearby = null;
|
||||
try
|
||||
{
|
||||
nearby = World.world.families.getNearbyFamily(joiner.asset, joiner.current_tile);
|
||||
}
|
||||
catch
|
||||
{
|
||||
nearby = null;
|
||||
}
|
||||
|
||||
EventOutcome.ActorFlags before = EventOutcome.Snapshot(joiner);
|
||||
BehResult br = new BehFamilyGroupJoin().execute(joiner);
|
||||
bool gained = EventOutcome.GainedFamily(before, joiner);
|
||||
if (gained && InterestRegistry.CountKeysContaining("rel:family_group_join") > 0)
|
||||
{
|
||||
Claim("family_group_join", "id", "BehFamilyGroupJoin", "busy_bypass=ForceLiveEventFeeds");
|
||||
Claim(
|
||||
"family_group_join",
|
||||
"id",
|
||||
"BehFamilyGroupJoin",
|
||||
"busy_bypass=ForceLiveEventFeeds nearby=" + (nearby != null));
|
||||
}
|
||||
else if (gained)
|
||||
{
|
||||
|
|
@ -199,7 +213,6 @@ public static class EventLiveRelationshipHarness
|
|||
}
|
||||
else
|
||||
{
|
||||
// Deterministic fallback: setFamily is the real join state change; Beh may miss radius.
|
||||
if (family != null && !joiner.hasFamily())
|
||||
{
|
||||
joiner.setFamily(family);
|
||||
|
|
@ -210,7 +223,7 @@ public static class EventLiveRelationshipHarness
|
|||
joiner.hasFamily() ? "pipeline" : "unsupported",
|
||||
"BehFamilyGroupJoin",
|
||||
joiner.hasFamily()
|
||||
? "shared=setFamily fallback; Beh nearby miss"
|
||||
? "shared=setFamily fallback; getNearbyFamily=" + (nearby != null)
|
||||
: "beh_and_setFamily_failed");
|
||||
}
|
||||
}
|
||||
|
|
@ -313,23 +326,44 @@ public static class EventLiveRelationshipHarness
|
|||
Claim("family_removed", "unsupported", "FamilyManager.removeObject", "no_family");
|
||||
}
|
||||
|
||||
// --- find_lover ---
|
||||
// --- find_lover (must stay !hasLover: isolate so Beh does not instantly bond) ---
|
||||
Actor seeker = Track(SpawnHumanNear(a), spawned);
|
||||
AgeOutOfSpawnWindow(seeker);
|
||||
InterestDropLog.Clear();
|
||||
InterestRegistry.ForceExpireContaining("rel:find_lover", Time.unscaledTime);
|
||||
try
|
||||
{
|
||||
TryIsolateFromPeers(seeker);
|
||||
try
|
||||
{
|
||||
if (seeker.hasLover())
|
||||
{
|
||||
seeker.setLover(null);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
BehResult brSeek = new BehFindLover().execute(seeker);
|
||||
if (EventOutcome.StillSeekingLover(seeker, brSeek)
|
||||
&& InterestRegistry.CountKeysContaining("rel:find_lover") > 0)
|
||||
{
|
||||
Claim("find_lover", "id", "BehFindLover", "busy_bypass=ForceLiveEventFeeds");
|
||||
Claim("find_lover", "id", "BehFindLover", "busy_bypass=ForceLiveEventFeeds isolated");
|
||||
}
|
||||
else if (EventOutcome.StillSeekingLover(seeker, brSeek))
|
||||
{
|
||||
Fail(fails, "find_lover", "seeking_but_no_key");
|
||||
}
|
||||
else if (seeker.hasLover())
|
||||
{
|
||||
Claim(
|
||||
"find_lover",
|
||||
"unsupported",
|
||||
"BehFindLover",
|
||||
"bonded_instead_of_seeking");
|
||||
}
|
||||
else
|
||||
{
|
||||
Claim("find_lover", "unsupported", "BehFindLover", "not_seeking_or_stop");
|
||||
|
|
@ -816,7 +850,6 @@ public static class EventLiveRelationshipHarness
|
|||
if (tile != null)
|
||||
{
|
||||
mover.current_position = near.current_position;
|
||||
// Best-effort; tile teleport APIs vary by version.
|
||||
AccessTools.Method(typeof(Actor), "setCurrentTile", new[] { typeof(WorldTile) })
|
||||
?.Invoke(mover, new object[] { tile });
|
||||
}
|
||||
|
|
@ -827,6 +860,97 @@ public static class EventLiveRelationshipHarness
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Park the actor on a distant tile so BehFindLover cannot instantly bond with harness peers.
|
||||
/// </summary>
|
||||
private static void TryIsolateFromPeers(Actor actor)
|
||||
{
|
||||
if (actor == null || actor.current_tile == null || World.world?.map_chunk_manager == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
WorldTile best = null;
|
||||
float bestScore = -1f;
|
||||
Vector3 origin = actor.current_position;
|
||||
// Sample a coarse grid of tiles for emptiness + distance.
|
||||
int w = MapBox.width;
|
||||
int h = MapBox.height;
|
||||
int step = Math.Max(8, Math.Min(w, h) / 16);
|
||||
for (int x = 2; x < w - 2; x += step)
|
||||
{
|
||||
for (int y = 2; y < h - 2; y += step)
|
||||
{
|
||||
WorldTile tile = null;
|
||||
try
|
||||
{
|
||||
tile = World.world.GetTile(x, y);
|
||||
}
|
||||
catch
|
||||
{
|
||||
tile = null;
|
||||
}
|
||||
|
||||
if (tile == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float dist = (tile.posV3 - origin).sqrMagnitude;
|
||||
if (dist < 80f * 80f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int nearby = 0;
|
||||
try
|
||||
{
|
||||
foreach (Actor u in Finder.getUnitsFromChunk(tile, 2))
|
||||
{
|
||||
if (u != null && u.isAlive() && u != actor)
|
||||
{
|
||||
nearby++;
|
||||
if (nearby > 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
nearby = 99;
|
||||
}
|
||||
|
||||
if (nearby > 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float score = dist;
|
||||
if (score > bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
best = tile;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (best != null)
|
||||
{
|
||||
actor.current_position = best.posV3;
|
||||
AccessTools.Method(typeof(Actor), "setCurrentTile", new[] { typeof(WorldTile) })
|
||||
?.Invoke(actor, new object[] { best });
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
private static Actor SpawnHumanNear(Actor near)
|
||||
{
|
||||
try
|
||||
|
|
|
|||
|
|
@ -1540,9 +1540,10 @@ internal static class HarnessScenarios
|
|||
Step("act17e", "activity_force", asset: "BehUnloadResources", label: "wheat@Riverhold",
|
||||
count: 1, tier: "beat", value: "human"),
|
||||
Step("act17f", "activity_age", value: "12"),
|
||||
Step("act17g", "assert", expect: "dossier_history_not_contains", value: "Hunts"),
|
||||
// Needle PreyThing (not "Hunts") - species voice may omit "hunt(s)" entirely.
|
||||
Step("act17g", "assert", expect: "dossier_history_not_contains", value: "PreyThing"),
|
||||
Step("act17h", "assert", expect: "dossier_history_contains", value: "wheat"),
|
||||
Step("act17i", "assert", expect: "activity_log_contains", value: "Hunts"),
|
||||
Step("act17i", "assert", expect: "activity_log_contains", value: "PreyThing"),
|
||||
// Rebuild a busy ring for later variety / count asserts.
|
||||
Step("act17j", "activity_force", asset: "BehPollinate", label: "Works the flower", count: 2, tier: "beat"),
|
||||
Step("act17k", "activity_force", asset: "zzz_harness_act", label: "Harness pollen trail", count: 1),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "IdleSpectator",
|
||||
"author": "dazed",
|
||||
"version": "0.25.29",
|
||||
"version": "0.25.35",
|
||||
"description": "AFK Idle Spectator (I) + Lore (L). Catalog-wide event inject E2E suite.",
|
||||
"GUID": "com.dazed.idlespectator"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,11 +38,12 @@ REGRESSION_SCENARIOS=(
|
|||
discovery
|
||||
world_log
|
||||
event_suite
|
||||
event_live_pipelines
|
||||
chronicle_smoke
|
||||
activity_idle_smoke
|
||||
activity_status_smoke
|
||||
activity_happiness_smoke
|
||||
# Heavier live fires last - wars/meta/eras can reshape the loaded world.
|
||||
event_live_pipelines
|
||||
)
|
||||
|
||||
scenario=""
|
||||
|
|
|
|||
Loading…
Reference in a new issue