reinit branch
This commit is contained in:
445
OrekiWoofsBees.Common/PlantPositionRegistryModSystem2.cs
Normal file
445
OrekiWoofsBees.Common/PlantPositionRegistryModSystem2.cs
Normal file
@@ -0,0 +1,445 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
public class PlantPositionRegistryModSystem2 : ModSystem, IPlantPositionRegistry
|
||||
{
|
||||
private const int default_blocks_per_tick = 20;
|
||||
private const int tick_interval_ms = 20;
|
||||
|
||||
private static readonly Dictionary<int, ScanOffsetTable> offsetTables = [];
|
||||
|
||||
private long? tickListenerId;
|
||||
private readonly Dictionary<StructVec3i, BeehiveScanCursor> beehives = [];
|
||||
private readonly HashSet<StructVec3i> flowerPositions = [];
|
||||
private readonly HashSet<StructVec3i> 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<BlockPos, int>? FlowerEvent;
|
||||
public event Action<BlockPos, int>? 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 (
|
||||
IEnumerable<BlockPos> Flowers,
|
||||
IEnumerable<BlockPos> Crops,
|
||||
float InitialScanProgress,
|
||||
float RescanProgress
|
||||
) GetPlantsNearPosition(BlockPos hivePos, int radius)
|
||||
{
|
||||
var key = StructVec3i.FromBlockPos(hivePos);
|
||||
|
||||
var flowers = flowerPositions
|
||||
.Where(p => Overlaps.IsWithinSphericalRadius(hivePos, p, radius))
|
||||
.Select(p => new BlockPos(p.X, p.Y, p.Z, hivePos.dimension));
|
||||
|
||||
var crops = cropPositions
|
||||
.Where(p => Overlaps.IsWithinSphericalRadius(hivePos, p, radius))
|
||||
.Select(p => new BlockPos(p.X, p.Y, p.Z, hivePos.dimension));
|
||||
|
||||
float initialProgress = 1.0f;
|
||||
float rescanProgress = 0.0f;
|
||||
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 (int FlowerCount, int CropCount, float InitialScanProgress, float RescanProgress) GetPlantCountsNearPosition(BlockPos hivePos, int radius)
|
||||
{
|
||||
int flowers = flowerPositions.Count(p => Overlaps.IsWithinSphericalRadius(hivePos, p, radius));
|
||||
int crops = cropPositions.Count(p => Overlaps.IsWithinSphericalRadius(hivePos, p, radius));
|
||||
|
||||
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(blockPos);
|
||||
var structPos = new StructVec3i(blockPos.X, blockPos.Y, blockPos.Z);
|
||||
|
||||
if (block == null || block.BlockId == 0)
|
||||
{
|
||||
// block is air or unloaded - remove from caches if present
|
||||
flowerPositions.Remove(structPos);
|
||||
cropPositions.Remove(structPos);
|
||||
continue;
|
||||
}
|
||||
|
||||
// check if this is soil with soil below - mark as soil floor
|
||||
if (block is BlockSoil)
|
||||
{
|
||||
var blockBelow = accessor.GetBlock(blockPos.DownCopy());
|
||||
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(structPos);
|
||||
flowerPositions.Remove(structPos);
|
||||
}
|
||||
else if (PlantRecognitionUtilities.IsFlower(block, accessor, blockPos))
|
||||
{
|
||||
flowerPositions.Add(structPos);
|
||||
cropPositions.Remove(structPos);
|
||||
}
|
||||
else
|
||||
{
|
||||
flowerPositions.Remove(structPos);
|
||||
cropPositions.Remove(structPos);
|
||||
}
|
||||
}
|
||||
|
||||
stopwatch.Stop();
|
||||
if (stopwatch.Elapsed.TotalSeconds > 0.2)
|
||||
{
|
||||
Mod.Logger.Warning($"{nameof(PlantPositionRegistryModSystem2)} {nameof(OnTick)} took {stopwatch.Elapsed.TotalSeconds:F2}s (beehives: {beehives.Count}).");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
private (BeehiveScanCursor Cursor, BlockPos 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.ToList();
|
||||
lastScannedBeehiveIndex = (lastScannedBeehiveIndex + 1) % beehivesList.Count;
|
||||
bestCursor = beehivesList[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 BlockPos GetCurrentBlockPos(Dictionary<int, ScanOffsetTable> tables)
|
||||
{
|
||||
var table = tables[Radius];
|
||||
var (X, Y, Z) = table.GetOffset(currentIndex);
|
||||
return new BlockPos(
|
||||
HivePos.X + X,
|
||||
HivePos.Y + Y,
|
||||
HivePos.Z + Z,
|
||||
HivePos.dimension
|
||||
);
|
||||
}
|
||||
|
||||
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<int, ScanOffsetTable> 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<int, ScanOffsetTable> tables)
|
||||
{
|
||||
var table = tables[Radius];
|
||||
if (table.Count == 0)
|
||||
return 0.0f;
|
||||
|
||||
if (BlocksCheckedCount < table.Count)
|
||||
return 0.0f;
|
||||
|
||||
return (float)currentIndex / table.Count;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// shared table of offsets for a given radius, sorted by priority.
|
||||
/// only one instance per radius is created and shared across all beehives.
|
||||
/// </summary>
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user