Private
Public Access
1
0

reinit branch

This commit is contained in:
2026-03-11 01:46:34 +01:00
commit bff9251737
129 changed files with 16115 additions and 0 deletions

View 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()}");
}
}

View 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; } = [];
}

View 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);
}
}

View 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);
}

View 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>

View 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;
}
}

View 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];
}
}
}

View 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);
}
}

View 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);
}

View File

@@ -0,0 +1,8 @@
namespace OrekiWoofsBees.Common;
public enum SwarmState
{
BuildingSwarm,
HangingOut,
MigratingToNewHive,
}

View 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);
}