using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Vintagestory.API.Common; using Vintagestory.API.MathTools; using Vintagestory.GameContent; namespace OrekiWoofsBees.Common; /// /// tracks plant positions for all registered beehives. /// Instead of each beehive scanning its radius every tick, this registry /// incrementally scans blocks across all beehives to keep performance consistent /// public class PlantPositionRegistryModSystem2 : ModSystem, IPlantPositionRegistry { private readonly static BlockPos staticBlockPos = new(0); private const int default_blocks_per_tick = 20; private const int tick_interval_ms = 20; private static readonly Dictionary offsetTables = []; private long? tickListenerId; private readonly Dictionary beehives = []; private readonly HashSet flowerPositions = []; private readonly HashSet cropPositions = []; // blocks below this are skipped private readonly Dictionary<(int X, int Z), int> soilFloorCache = []; private int lastScannedBeehiveIndex = 0; private int blocksPerTick = default_blocks_per_tick; public ICoreAPI? Api { get; private set; } public int BlocksPerTick { get => blocksPerTick; set => blocksPerTick = Math.Clamp(value, 0, 1000); } public event Action? FlowerEvent; public event Action? CropEvent; public override double ExecuteOrder() => 0.10; public override void Start(ICoreAPI api) { Api = api; if (api.Side.IsServer()) tickListenerId = api.Event.RegisterGameTickListener(OnTick, tick_interval_ms); } public override void Dispose() { if (Api != null && tickListenerId.HasValue) Api.Event.UnregisterGameTickListener(tickListenerId.Value); beehives.Clear(); flowerPositions.Clear(); cropPositions.Clear(); FlowerEvent = null; CropEvent = null; } public void RegisterBeehive(BlockPos pos, int radius) { var key = StructVec3i.FromBlockPos(pos); if (beehives.ContainsKey(key)) return; if (!offsetTables.ContainsKey(radius)) offsetTables[radius] = new ScanOffsetTable(radius); beehives[key] = new BeehiveScanCursor(pos, radius); } public void UnregisterBeehive(BlockPos pos) { var key = StructVec3i.FromBlockPos(pos); beehives.Remove(key); } public ( float InitialScanProgress, float RescanProgress ) GetPlantsNearPosition(BlockPos hivePos, int radius, List flowerPositionsBuffer, List cropPositionsBuffer) { flowerPositionsBuffer.Clear(); cropPositionsBuffer.Clear(); var key = StructVec3i.FromBlockPos(hivePos); foreach (var flowerPos in flowerPositions) { if (!Overlaps.IsWithinSphericalRadius(hivePos, flowerPos, radius)) continue; flowerPositionsBuffer.Add(flowerPos); } foreach (var cropPos in cropPositions) { if (!Overlaps.IsWithinSphericalRadius(hivePos, cropPos, radius)) continue; cropPositionsBuffer.Add(cropPos); } float initialProgress = 1.0f; float rescanProgress = 0.0f; if (!beehives.TryGetValue(key, out var cursor)) return (initialProgress, rescanProgress); var table = offsetTables[cursor.Radius]; if (table.Count > 0) { initialProgress = Math.Min(1.0f, (float)cursor.BlocksCheckedCount / table.Count); rescanProgress = cursor.GetRescanProgress(offsetTables); } return (initialProgress, rescanProgress); } public (int FlowerCount, int CropCount, float InitialScanProgress, float RescanProgress) GetPlantCountsNearPosition(BlockPos hivePos, int radius) { int flowers = 0; foreach (var flowerPos in flowerPositions) { if (Overlaps.IsWithinSphericalRadius(hivePos, flowerPos, radius)) flowers++; } int crops = 0; foreach (var cropPos in cropPositions) { if (Overlaps.IsWithinSphericalRadius(hivePos, cropPos, radius)) crops++; } float initialProgress = 0.0f; float rescanProgress = 0.0f; var key = StructVec3i.FromBlockPos(hivePos); if (!beehives.TryGetValue(key, out var cursor)) return (flowers, crops, initialProgress, rescanProgress); var table = offsetTables[cursor.Radius]; if (table.Count > 0) { initialProgress = Math.Min(1.0f, (float)cursor.BlocksCheckedCount / table.Count); rescanProgress = cursor.GetRescanProgress(offsetTables); } return (flowers, crops, initialProgress, rescanProgress); } public void AddPlantPosition(BlockPos pos, Block block) { if (Api is null) return; var structPos = new StructVec3i(pos.X, pos.Y, pos.Z); if (PlantRecognitionUtilities.IsCrop(block)) { cropPositions.Add(structPos); flowerPositions.Remove(structPos); CropEvent?.Invoke(pos, 1); } else if (PlantRecognitionUtilities.IsFlower(block, Api.World.BlockAccessor, pos)) { flowerPositions.Add(structPos); cropPositions.Remove(structPos); FlowerEvent?.Invoke(pos, 1); } } public void RemovePlantPosition(BlockPos pos, Block block) { if (Api is null) return; var structPos = new StructVec3i(pos.X, pos.Y, pos.Z); flowerPositions.Remove(structPos); cropPositions.Remove(structPos); if (PlantRecognitionUtilities.IsCrop(block)) CropEvent?.Invoke(pos, -1); else if (PlantRecognitionUtilities.IsFlower(block, Api.World.BlockAccessor, pos)) FlowerEvent?.Invoke(pos, -1); } public int CountBeehivesInRadius(BlockPos pos, int radius) { var plantPos = StructVec3i.FromBlockPos(pos); int count = 0; foreach (var (beehivePos, _) in beehives) { if (Overlaps.IsWithinSphericalRadius(beehivePos, plantPos, radius)) count++; } return count; } private void OnTick(float dt) { if (Api is null) return; var stopwatch = Stopwatch.StartNew(); if (beehives.Count == 0) return; var accessor = Api.World.BlockAccessor; for (int i = 0; i < BlocksPerTick; i++) { var nextBlock = GetNextBlockToCheck(); if (nextBlock == null) break; var (cursor, blockPos) = nextBlock.Value; // check if this position is below the soil floor for this X/Z var xz = (blockPos.X, blockPos.Z); if (soilFloorCache.TryGetValue(xz, out int soilFloorY) && blockPos.Y < soilFloorY) { // it's below the soil floor, skip cursor.Advance(); continue; } cursor.Advance(); var block = accessor.GetBlock(staticBlockPos.Set(blockPos.X, blockPos.Y, blockPos.Z)); if (block == null || block.BlockId == 0) { // block is air or unloaded - remove from caches if present flowerPositions.Remove(blockPos); cropPositions.Remove(blockPos); continue; } // check if this is soil with soil below - mark as soil floor if (block is BlockSoil) { var blockBelow = accessor.GetBlockRaw(blockPos.X, blockPos.Y - 1, blockPos.Z); if (blockBelow is BlockSoil) { // found soil floor - record it bool isNewFloor = false; if (!soilFloorCache.TryGetValue(xz, out int existingFloor) || blockPos.Y < existingFloor) { soilFloorCache[xz] = blockPos.Y; isNewFloor = true; } if (isNewFloor) cursor.CountAndSkipBlocksBelowY(blockPos.Y, xz, offsetTables); } } if (PlantRecognitionUtilities.IsCrop(block)) { cropPositions.Add(blockPos); flowerPositions.Remove(blockPos); } else if (PlantRecognitionUtilities.IsFlower(block, accessor, blockPos)) { flowerPositions.Add(blockPos); cropPositions.Remove(blockPos); } else { flowerPositions.Remove(blockPos); cropPositions.Remove(blockPos); } } stopwatch.Stop(); if (stopwatch.Elapsed.TotalSeconds > 0.2) { Mod.Logger.Warning($"{nameof(PlantPositionRegistryModSystem2)} {nameof(OnTick)} took {stopwatch.Elapsed.TotalSeconds:F2}s (beehives: {beehives.Count})."); } } /// /// 1. pick the beehive with the least blocks checked in its radius /// 2. pick the closest horizontal space vertically /// 3. check the next closest block to the beehive in this horizontal space /// private (BeehiveScanCursor Cursor, StructVec3i BlockPos)? GetNextBlockToCheck() { if (beehives.Count == 0) return null; bool allCompleted = true; foreach (var cursor in beehives.Values) { var table = offsetTables[cursor.Radius]; if (cursor.BlocksCheckedCount < table.Count) { allCompleted = false; break; } } BeehiveScanCursor? bestCursor = null; if (!allCompleted) { var lowestRelativeProgress = float.MaxValue; foreach (var cursor in beehives.Values) { var table = offsetTables[cursor.Radius]; if (cursor.BlocksCheckedCount >= table.Count) continue; var relativeProgress = (float)cursor.BlocksCheckedCount / table.Count; if (relativeProgress < lowestRelativeProgress) { lowestRelativeProgress = relativeProgress; bestCursor = cursor; } } } else { var beehivesList = beehives.Values; lastScannedBeehiveIndex = (lastScannedBeehiveIndex + 1) % beehivesList.Count; bestCursor = beehivesList.ElementAt(lastScannedBeehiveIndex); } if (bestCursor == null) return null; if (bestCursor.IsStartingNewCycle()) soilFloorCache.Clear(); var blockPos = bestCursor.GetCurrentBlockPos(offsetTables); return (bestCursor, blockPos); } private class BeehiveScanCursor(BlockPos hivePos, int radius) { public BlockPos HivePos { get; } = hivePos; public int Radius { get; } = radius; public int BlocksCheckedCount { get; private set; } = 0; private int currentIndex = 0; public StructVec3i GetCurrentBlockPos(Dictionary tables) { var table = tables[Radius]; var (X, Y, Z) = table.GetOffset(currentIndex); return new( HivePos.X + X, HivePos.Y + Y, HivePos.Z + Z ); } public void Advance() { var table = offsetTables[Radius]; currentIndex++; if (currentIndex >= table.Count) currentIndex = 0; if (BlocksCheckedCount < table.Count) BlocksCheckedCount++; } public bool IsStartingNewCycle() { return currentIndex == 0 && BlocksCheckedCount >= offsetTables[Radius].Count; } public int GetCurrentIndex() { return currentIndex; } public void CountAndSkipBlocksBelowY(int floorY, (int X, int Z) xz, Dictionary tables) { var table = tables[Radius]; int skippedCount = 0; for (int i = currentIndex + 1; i < table.Count; i++) { var (offsetX, offsetY, offsetZ) = table.GetOffset(i); int worldX = HivePos.X + offsetX; int worldZ = HivePos.Z + offsetZ; int worldY = HivePos.Y + offsetY; if (worldX == xz.X && worldZ == xz.Z && worldY < floorY) { skippedCount++; if (BlocksCheckedCount < table.Count) BlocksCheckedCount++; } } } public float GetRescanProgress(Dictionary tables) { var table = tables[Radius]; if (table.Count == 0) return 0.0f; if (BlocksCheckedCount < table.Count) return 0.0f; return (float)currentIndex / table.Count; } } /// /// shared table of offsets for a given radius, sorted by priority. /// only one instance per radius is created and shared across all beehives. /// private class ScanOffsetTable { private readonly (int X, int Y, int Z)[] offsets; public int Count => offsets.Length; public ScanOffsetTable(int radius) { int radiusSq = radius * radius; var offsets = new List<(int X, int Y, int Z, int YDist, int HorizontalDistSq)>(); for (int dy = -radius; dy <= radius; dy++) { for (int dx = -radius; dx <= radius; dx++) { for (int dz = -radius; dz <= radius; dz++) { int distSq = dx * dx + dy * dy + dz * dz; if (distSq <= radiusSq) { int yDist = Math.Abs(dy); int horizontalDistSq = dx * dx + dz * dz; offsets.Add((dx, dy, dz, yDist, horizontalDistSq)); } } } } this.offsets = [.. offsets .OrderBy(o => o.YDist) .ThenBy(o => o.HorizontalDistSq) .Select(o => (o.X, o.Y, o.Z))]; } public (int X, int Y, int Z) GetOffset(int index) { return offsets[index]; } } }