worldbox-observer-mod/IdleSpectator/SpeciesDiscovery.cs
2026-07-14 14:23:11 -05:00

209 lines
5.9 KiB
C#

using System.Collections.Generic;
using NeoModLoader.services;
using UnityEngine;
namespace IdleSpectator;
/// <summary>
/// Captures subspecies creation during map gen / before spectator is on,
/// then drains a few as Curiosity once the world is playable.
/// </summary>
public static class SpeciesDiscovery
{
private const float DrainIntervalSeconds = 12f;
private const float NotReadyBackoffSeconds = 5f;
private const int MaxBuffered = 24;
private const int MaxDrainPerSession = 8;
private const int MaxNotReadyAttempts = 8;
private static readonly List<PendingSpecies> Pending = new List<PendingSpecies>();
private static readonly HashSet<string> SeenAssets = new HashSet<string>();
private static readonly HashSet<string> PresentedAssets = new HashSet<string>();
private static float _lastDrainAt = -999f;
private static int _drainedThisSession;
private struct PendingSpecies
{
public string AssetId;
public Vector3 Position;
public Subspecies Subspecies;
public int NotReadyAttempts;
}
public static void OnSpectatorEnabled()
{
_drainedThisSession = 0;
_lastDrainAt = Time.unscaledTime - DrainIntervalSeconds + 2f;
}
public static void OnSpectatorDisabled()
{
// Keep buffer so re-enabling can still discover; clear session drain counter only.
_drainedThisSession = 0;
}
public static void NoteCreated(ActorAsset asset, WorldTile tile, bool mutation, Subspecies subspecies)
{
if (mutation || asset == null || string.IsNullOrEmpty(asset.id))
{
return;
}
if (!SeenAssets.Add(asset.id))
{
return;
}
// Already shown via live path before buffer ran - nothing to queue.
if (PresentedAssets.Contains(asset.id))
{
return;
}
if (Pending.Count >= MaxBuffered)
{
Pending.RemoveAt(0);
}
Vector3 pos = tile != null ? tile.posV3 : Vector3.zero;
Pending.Add(new PendingSpecies
{
AssetId = asset.id,
Position = pos,
Subspecies = subspecies,
NotReadyAttempts = 0
});
}
/// <summary>
/// Live newSpecies path already tipped this asset - drop any buffered copy.
/// </summary>
public static void MarkPresented(string assetId)
{
if (string.IsNullOrEmpty(assetId))
{
return;
}
PresentedAssets.Add(assetId);
SeenAssets.Add(assetId);
for (int i = Pending.Count - 1; i >= 0; i--)
{
if (Pending[i].AssetId == assetId)
{
Pending.RemoveAt(i);
}
}
}
public static bool WasPresented(string assetId)
{
return !string.IsNullOrEmpty(assetId) && PresentedAssets.Contains(assetId);
}
public static void Update()
{
if (!SpectatorMode.Active || !Config.game_loaded || World.world == null)
{
return;
}
if (SmoothLoader.isLoading())
{
return;
}
if (_drainedThisSession >= MaxDrainPerSession || Pending.Count == 0)
{
return;
}
float now = Time.unscaledTime;
if (now - _lastDrainAt < DrainIntervalSeconds)
{
return;
}
// Wait until the director can actually show a curiosity tip.
if (!InterestDirector.WouldAcceptCuriosity())
{
return;
}
int index = FindNextReadyIndex();
if (index < 0)
{
// Front items not spawnable yet - retry later without dequeue flap.
_lastDrainAt = now - DrainIntervalSeconds + NotReadyBackoffSeconds;
return;
}
PendingSpecies next = Pending[index];
ActorAsset asset = AssetManager.actor_library?.get(next.AssetId);
if (asset == null)
{
Pending.RemoveAt(index);
return;
}
InterestEvent curiosity = WorldActivityScanner.FromNewSpecies(next.Subspecies, asset, null);
if (curiosity == null)
{
next.NotReadyAttempts++;
if (next.NotReadyAttempts >= MaxNotReadyAttempts)
{
Pending.RemoveAt(index);
LogService.LogInfo($"[IdleSpectator] Discovery skip (never spawned): {next.AssetId}");
}
else
{
Pending[index] = next;
}
_lastDrainAt = now - DrainIntervalSeconds + NotReadyBackoffSeconds;
return;
}
// Prefer the recorded spawn tile when the unit search used a weak position.
if (next.Position != Vector3.zero && curiosity.Position == Vector3.zero)
{
curiosity.Position = next.Position;
}
Pending.RemoveAt(index);
PresentedAssets.Add(next.AssetId);
_lastDrainAt = now;
_drainedThisSession++;
InterestCollector.EnqueueDirect(curiosity);
LogService.LogInfo($"[IdleSpectator] Discovery drain: {next.AssetId} ({_drainedThisSession}/{MaxDrainPerSession}, queued={Pending.Count})");
}
private static int FindNextReadyIndex()
{
for (int i = 0; i < Pending.Count; i++)
{
PendingSpecies next = Pending[i];
if (PresentedAssets.Contains(next.AssetId))
{
Pending.RemoveAt(i);
i--;
continue;
}
ActorAsset asset = AssetManager.actor_library?.get(next.AssetId);
if (asset == null)
{
Pending.RemoveAt(i);
i--;
continue;
}
if (WorldActivityScanner.FromNewSpecies(next.Subspecies, asset, null) != null)
{
return i;
}
}
return -1;
}
}