reinit branch
This commit is contained in:
99
OrekiWoofsBees.Common/Configs/ChatCommands.cs
Normal file
99
OrekiWoofsBees.Common/Configs/ChatCommands.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using Vintagestory.API.Common;
|
||||
using Vintagestory.API.Config;
|
||||
using Vintagestory.API.Server;
|
||||
|
||||
namespace OrekiWoofsBees.Common.Configs;
|
||||
|
||||
public static class ConfigCommands
|
||||
{
|
||||
public static void Register<T>(
|
||||
IChatCommandApi chatApi,
|
||||
string commandName,
|
||||
string langDomain,
|
||||
bool serverSide,
|
||||
Func<T> getInstance,
|
||||
Action<bool> saveConfig)
|
||||
{
|
||||
Register(chatApi.Create(commandName), chatApi, langDomain, serverSide, getInstance, saveConfig);
|
||||
}
|
||||
|
||||
public static void Register<T>(
|
||||
IChatCommand builder,
|
||||
IChatCommandApi chatApi,
|
||||
string langDomain,
|
||||
bool serverSide,
|
||||
Func<T> getInstance,
|
||||
Action<bool> saveConfig)
|
||||
{
|
||||
if (serverSide)
|
||||
builder = builder.RequiresPrivilege(Privilege.controlserver);
|
||||
|
||||
var p = chatApi.Parsers;
|
||||
foreach (var prop in typeof(T).GetProperties())
|
||||
{
|
||||
var attr = prop.GetCustomAttribute<ConfigCommandAttribute>();
|
||||
if (attr == null || attr.ServerSide != serverSide) continue;
|
||||
|
||||
var name = prop.Name;
|
||||
var descKey = $"{langDomain}:config-desc-{name}";
|
||||
|
||||
OnCommandDelegate handler;
|
||||
ICommandArgumentParser parser;
|
||||
|
||||
if (prop.PropertyType == typeof(int))
|
||||
{
|
||||
parser = p.OptionalIntRange("value", (int)attr.Min, (int)attr.Max);
|
||||
handler = args => HandleOptional(args, name, descKey,
|
||||
() => $"{prop.GetValue(getInstance())}",
|
||||
() => { prop.SetValue(getInstance(), (int)args.Parsers[0].GetValue()!); saveConfig(serverSide); },
|
||||
() => args.Parsers[0].GetValue() is int);
|
||||
}
|
||||
else if (prop.PropertyType == typeof(float))
|
||||
{
|
||||
parser = ParserExtensions.OptionalFloatRange("value", (float)attr.Min, (float)attr.Max);
|
||||
handler = args => HandleOptional(args, name, descKey,
|
||||
() => $"{(float)prop.GetValue(getInstance())!:G}",
|
||||
() => { prop.SetValue(getInstance(), (float)args.Parsers[0].GetValue()!); saveConfig(serverSide); },
|
||||
() => args.Parsers[0].GetValue() is float);
|
||||
}
|
||||
else if (prop.PropertyType == typeof(bool))
|
||||
{
|
||||
parser = p.OptionalBool("value");
|
||||
handler = args => HandleOptional(args, name, descKey,
|
||||
() => $"{prop.GetValue(getInstance())}",
|
||||
() => { prop.SetValue(getInstance(), (bool)args.Parsers[0].GetValue()!); saveConfig(serverSide); },
|
||||
() => args.Parsers[0].GetValue() is bool);
|
||||
}
|
||||
else if (prop.PropertyType == typeof(string))
|
||||
{
|
||||
parser = p.OptionalWordRange("value", attr.AllowedValues);
|
||||
handler = args => HandleOptional(args, name, descKey,
|
||||
() => $"{prop.GetValue(getInstance())}",
|
||||
() => { prop.SetValue(getInstance(), (string)args.Parsers[0].GetValue()!); saveConfig(serverSide); },
|
||||
() => args.Parsers[0].GetValue() is string);
|
||||
}
|
||||
else continue;
|
||||
|
||||
builder
|
||||
.BeginSubCommand(name)
|
||||
.WithDescription(Lang.Get(descKey))
|
||||
.WithArgs(parser)
|
||||
.HandleWith(handler)
|
||||
.EndSubCommand();
|
||||
}
|
||||
}
|
||||
|
||||
private static TextCommandResult HandleOptional(
|
||||
TextCommandCallingArgs args, string name, string descKey,
|
||||
Func<string> formatValue, Action applyAndSave, Func<bool> canParse)
|
||||
{
|
||||
if (args.Parsers[0].IsMissing)
|
||||
return TextCommandResult.Success($"{Lang.Get(descKey)}\n{name}={formatValue()}");
|
||||
if (!canParse())
|
||||
return TextCommandResult.Error("Couldn't parse.");
|
||||
applyAndSave();
|
||||
return TextCommandResult.Success($"{name}={formatValue()}");
|
||||
}
|
||||
}
|
||||
23
OrekiWoofsBees.Common/Configs/ConfigCommandAttribute.cs
Normal file
23
OrekiWoofsBees.Common/Configs/ConfigCommandAttribute.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
|
||||
namespace OrekiWoofsBees.Common.Configs;
|
||||
|
||||
/// <summary>
|
||||
/// Marks a Config property as having a /beehives or .beehives chat command.
|
||||
/// The registration loop in ChatCommands.cs picks these up.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class ConfigCommandAttribute(bool serverSide) : Attribute
|
||||
{
|
||||
/// <summary>True = registered as a server command and broadcasts on change.</summary>
|
||||
public bool ServerSide { get; } = serverSide;
|
||||
|
||||
/// <summary>Inclusive</summary>
|
||||
public double Min { get; set; }
|
||||
|
||||
/// <summary>Inclusive</summary>
|
||||
public double Max { get; set; }
|
||||
|
||||
/// <remarks>Allowed values for string enum properties (uses OptionalWordRange).</remarks>
|
||||
public string[] AllowedValues { get; set; } = [];
|
||||
}
|
||||
16
OrekiWoofsBees.Common/Configs/ParserExtensions.cs
Normal file
16
OrekiWoofsBees.Common/Configs/ParserExtensions.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Vintagestory.API.Common;
|
||||
|
||||
namespace OrekiWoofsBees.Common.Configs;
|
||||
|
||||
public static class ParserExtensions
|
||||
{
|
||||
public static FloatArgParser OptionalFloatRange(string argName, float min, float max)
|
||||
{
|
||||
return new FloatArgParser(argName, min, max, isMandatoryArg: false);
|
||||
}
|
||||
|
||||
public static DoubleArgParser OptionalDoubleRange(string argName, double min, double max)
|
||||
{
|
||||
return new DoubleArgParser(argName, min, max, isMandatoryArg: false);
|
||||
}
|
||||
}
|
||||
40
OrekiWoofsBees.Common/IPlantPositionRegistry.cs
Normal file
40
OrekiWoofsBees.Common/IPlantPositionRegistry.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Vintagestory.API.Common;
|
||||
using Vintagestory.API.MathTools;
|
||||
|
||||
namespace OrekiWoofsBees.Common;
|
||||
|
||||
public interface IPlantPositionRegistry
|
||||
{
|
||||
ICoreAPI? Api { get; }
|
||||
|
||||
event Action<BlockPos, int>? CropEvent;
|
||||
event Action<BlockPos, int>? FlowerEvent;
|
||||
|
||||
void AddPlantPosition(BlockPos pos, Block block);
|
||||
|
||||
int CountBeehivesInRadius(BlockPos pos, int radius);
|
||||
|
||||
(
|
||||
int FlowerCount,
|
||||
int CropCount,
|
||||
float InitialScanProgress,
|
||||
float RescanProgress
|
||||
)
|
||||
GetPlantCountsNearPosition(BlockPos hivePos, int radius);
|
||||
|
||||
(
|
||||
IEnumerable<BlockPos> Flowers,
|
||||
IEnumerable<BlockPos> Crops,
|
||||
float InitialScanProgress,
|
||||
float RescanProgress
|
||||
)
|
||||
GetPlantsNearPosition(BlockPos hivePos, int radius);
|
||||
|
||||
void RegisterBeehive(BlockPos pos, int radius);
|
||||
|
||||
void RemovePlantPosition(BlockPos pos, Block block);
|
||||
|
||||
void UnregisterBeehive(BlockPos pos);
|
||||
}
|
||||
62
OrekiWoofsBees.Common/OrekiWoofsBees.Common.csproj
Normal file
62
OrekiWoofsBees.Common/OrekiWoofsBees.Common.csproj
Normal file
@@ -0,0 +1,62 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<Nullable>enable</Nullable>
|
||||
<Configurations>Debug;Release;Debug22</Configurations>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Debug22'">
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<VINTAGE_STORY>$(VINTAGE_STORY_22PRE2)</VINTAGE_STORY>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="VintagestoryAPI">
|
||||
<HintPath>$(VINTAGE_STORY)/VintagestoryAPI.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="VSSurvivalMod">
|
||||
<HintPath>$(VINTAGE_STORY)/Mods/VSSurvivalMod.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="VSEssentials">
|
||||
<HintPath>$(VINTAGE_STORY)/Mods/VSEssentials.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="VSCreativeMod">
|
||||
<HintPath>$(VINTAGE_STORY)/Mods/VSCreativeMod.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Newtonsoft.Json">
|
||||
<HintPath>$(VINTAGE_STORY)/Lib/Newtonsoft.Json.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="0Harmony">
|
||||
<HintPath>$(VINTAGE_STORY)/Lib/0Harmony.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="VintagestoryLib">
|
||||
<HintPath>$(VINTAGE_STORY)/VintagestoryLib.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="protobuf-net">
|
||||
<HintPath>$(VINTAGE_STORY)/Lib/protobuf-net.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="cairo-sharp">
|
||||
<HintPath>$(VINTAGE_STORY)/Lib/cairo-sharp.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Data.Sqlite">
|
||||
<HintPath>$(VINTAGE_STORY)/Lib/Microsoft.Data.Sqlite.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="configlib">
|
||||
<HintPath>E:\Code\VintageStory\configlib_1.10.14\configlib.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
32
OrekiWoofsBees.Common/Overlaps.cs
Normal file
32
OrekiWoofsBees.Common/Overlaps.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Vintagestory.API.MathTools;
|
||||
|
||||
namespace OrekiWoofsBees.Common;
|
||||
|
||||
public static class Overlaps
|
||||
{
|
||||
public static bool IsWithinSphericalRadius(BlockPos center, StructVec3i pos, int radius)
|
||||
{
|
||||
return IsWithinSphericalRadiusSq(center, pos, radius * radius);
|
||||
}
|
||||
|
||||
public static bool IsWithinSphericalRadiusSq(BlockPos center, StructVec3i pos, int radiusSq)
|
||||
{
|
||||
int dx = pos.X - center.X;
|
||||
int dy = pos.Y - center.Y;
|
||||
int dz = pos.Z - center.Z;
|
||||
return dx * dx + dy * dy + dz * dz <= radiusSq;
|
||||
}
|
||||
|
||||
public static bool IsWithinSphericalRadius(StructVec3i center, StructVec3i pos, int radius)
|
||||
{
|
||||
return IsWithinSphericalRadiusSq(center, pos, radius * radius);
|
||||
}
|
||||
|
||||
public static bool IsWithinSphericalRadiusSq(StructVec3i center, StructVec3i pos, int radiusSq)
|
||||
{
|
||||
int dx = pos.X - center.X;
|
||||
int dy = pos.Y - center.Y;
|
||||
int dz = pos.Z - center.Z;
|
||||
return dx * dx + dy * dy + dz * dz <= radiusSq;
|
||||
}
|
||||
}
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
45
OrekiWoofsBees.Common/PlantRecognitionUtilities.cs
Normal file
45
OrekiWoofsBees.Common/PlantRecognitionUtilities.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using Vintagestory.API.Common;
|
||||
using Vintagestory.API.MathTools;
|
||||
using Vintagestory.GameContent;
|
||||
|
||||
namespace OrekiWoofsBees.Common;
|
||||
|
||||
public static class PlantRecognitionUtilities
|
||||
{
|
||||
public static bool IsCrop(Block block)
|
||||
{
|
||||
return block is BlockCrop;
|
||||
}
|
||||
|
||||
public static bool IsFlower(Block block, IBlockAccessor accessor, BlockPos pos)
|
||||
{
|
||||
if (block.FirstCodePart() == "flower")
|
||||
return true;
|
||||
|
||||
if (block is BlockPlantContainer)
|
||||
{
|
||||
var plantContainer = block.GetBlockEntity<BlockEntityPlantContainer?>(pos);
|
||||
if (plantContainer is null)
|
||||
return false;
|
||||
|
||||
var contents = plantContainer.GetContents();
|
||||
if (contents is null)
|
||||
return false;
|
||||
if (contents.Block?.FirstCodePart() == "flower")
|
||||
return true;
|
||||
}
|
||||
|
||||
if (block is BlockBerryBush && accessor.GetBlockEntity(pos) is BlockEntityBerryBush blockEntityBerryBush)
|
||||
return blockEntityBerryBush.IsFlowering;
|
||||
|
||||
if (block is BlockFruitTreePart && accessor.GetBlockEntity(pos) is BlockEntityFruitTreeFoliage fruitTreeFoliage)
|
||||
return fruitTreeFoliage.FoliageState == EnumFoliageState.Flowering;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool IsPlant(Block block, IBlockAccessor accessor, BlockPos pos)
|
||||
{
|
||||
return IsCrop(block) || IsFlower(block, accessor, pos);
|
||||
}
|
||||
}
|
||||
8
OrekiWoofsBees.Common/StructVec3i.cs
Normal file
8
OrekiWoofsBees.Common/StructVec3i.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Vintagestory.API.MathTools;
|
||||
|
||||
namespace OrekiWoofsBees.Common;
|
||||
|
||||
public readonly record struct StructVec3i(int X, int Y, int Z)
|
||||
{
|
||||
public static StructVec3i FromBlockPos(BlockPos pos) => new(pos.X, pos.Y, pos.Z);
|
||||
}
|
||||
8
OrekiWoofsBees.Common/SwarmState.cs
Normal file
8
OrekiWoofsBees.Common/SwarmState.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace OrekiWoofsBees.Common;
|
||||
|
||||
public enum SwarmState
|
||||
{
|
||||
BuildingSwarm,
|
||||
HangingOut,
|
||||
MigratingToNewHive,
|
||||
}
|
||||
10
OrekiWoofsBees.Common/VectorConversionUtils.cs
Normal file
10
OrekiWoofsBees.Common/VectorConversionUtils.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Numerics;
|
||||
using Vintagestory.API.MathTools;
|
||||
|
||||
namespace OrekiWoofsBees.Common;
|
||||
|
||||
public static class VectorConversionUtils
|
||||
{
|
||||
public static Vector3 ToVector3(this Vec3f v) => new(v.X, v.Y, v.Z);
|
||||
public static Vec3f ToVec3f(this Vector3 v) => new(v.X, v.Y, v.Z);
|
||||
}
|
||||
Reference in New Issue
Block a user