using System; using System.Collections.Generic; using InfiniteWorld.VoxelWorld.Contracts; using MessagePipe; using UnityEngine; using VContainer.Unity; namespace InfiniteWorld.VoxelWorld.NavMesh { /// /// Stores short-lived route hints and expands them into interest points so nav coverage can prewarm ahead of movement. /// public sealed class NavCoverageHintService : ITickable, INavCoverageHintRegistry, INavCoverageHintReader { private readonly IChunkNavSourceReader chunkNavSourceReader; private readonly VoxelWorldNavMeshConfig config; private readonly IPublisher hintChangedPublisher; private readonly Dictionary hints = new Dictionary(); private readonly List expiredHintOwnerIds = new List(8); private int hintVersion; public NavCoverageHintService( IChunkNavSourceReader chunkNavSourceReader, VoxelWorldNavMeshConfig config, IPublisher hintChangedPublisher) { this.chunkNavSourceReader = chunkNavSourceReader; this.config = config ?? new VoxelWorldNavMeshConfig(); this.hintChangedPublisher = hintChangedPublisher; } /// /// Increments whenever the effective set of active hints changes and cached coverage planning should be invalidated. /// public int HintVersion => hintVersion; /// /// Expires hints whose time-to-live has elapsed so stale route bias does not keep shaping coverage forever. /// public void Tick() { if (hints.Count == 0) { return; } float now = Time.time; expiredHintOwnerIds.Clear(); foreach (KeyValuePair pair in hints) { if (pair.Value.ExpireAt > now) { continue; } expiredHintOwnerIds.Add(pair.Key); } if (expiredHintOwnerIds.Count == 0) { return; } for (int i = 0; i < expiredHintOwnerIds.Count; i++) { hints.Remove(expiredHintOwnerIds[i]); } NotifyHintsChanged(); } /// /// Registers or refreshes a temporary linear corridor for one owner so coverage can be biased along an upcoming route. /// public void SetLinearHint(int ownerId, Vector3 from, Vector3 to, float priority, float ttlSeconds) { if (ownerId == 0) { ownerId = 1; } float expireAt = Time.time + Mathf.Max(0.1f, ttlSeconds); WorldInterestPoint[] points = BuildLinearHintPoints(from, to, Mathf.Max(0.01f, priority)); hints[ownerId] = new HintEntry(points, expireAt); NotifyHintsChanged(); } /// /// Removes a previously registered route hint once the owner no longer needs prewarmed coverage. /// public void ClearHint(int ownerId) { if (!hints.Remove(ownerId)) { return; } NotifyHintsChanged(); } /// /// Appends the currently active hint points so the main coverage scheduler can treat them like supplemental interest. /// public void GetHintPoints(List results) { if (results == null) { throw new ArgumentNullException(nameof(results)); } foreach (KeyValuePair pair in hints) { WorldInterestPoint[] points = pair.Value.Points; for (int i = 0; i < points.Length; i++) { results.Add(points[i]); } } } private WorldInterestPoint[] BuildLinearHintPoints(Vector3 from, Vector3 to, float priority) { float chunkWorldSize = Mathf.Max(1f, chunkNavSourceReader.ChunkWorldSize); float spacing = Mathf.Max(chunkWorldSize, config.clusterMergeDistanceInChunks * chunkWorldSize * 0.75f); float distance = Vector3.Distance(from, to); int segmentCount = Mathf.Max(1, Mathf.CeilToInt(distance / Mathf.Max(0.01f, spacing))); int pointCount = segmentCount + 1; WorldInterestPoint[] points = new WorldInterestPoint[pointCount]; for (int i = 0; i < pointCount; i++) { float t = pointCount == 1 ? 1f : i / (float)(pointCount - 1); Vector3 position = Vector3.Lerp(from, to, t); points[i] = new WorldInterestPoint(position, priority, WorldInterestKind.TransientNavHint); } return points; } private void NotifyHintsChanged() { hintVersion++; hintChangedPublisher?.Publish(new NavCoverageHintChangedMessage(hintVersion)); } private readonly struct HintEntry { public HintEntry(WorldInterestPoint[] points, float expireAt) { Points = points; ExpireAt = expireAt; } public WorldInterestPoint[] Points { get; } public float ExpireAt { get; } } } }