Private
Public Access
1
0
Files
OrekiWoofsBeehives/RoamingBees/RoamingBees/Behaviors/BlockEntityBehaviorRoamingBees.cs

528 lines
20 KiB
C#

using OrekiWoofsBees.Common;
using RoamingBees.Particles;
using RoamingBees.Particles.Catchup;
using RoamingBees.Utilities;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using Vintagestory.API.Client;
using Vintagestory.API.Common;
using Vintagestory.API.Datastructures;
using Vintagestory.API.MathTools;
using Vintagestory.GameContent;
namespace RoamingBees.Behaviors;
public class BlockEntityBehaviorRoamingBees(BlockEntity blockEntity) : BlockEntityBehavior(blockEntity), IBeeSpawnHandler, IBeeSpawnCatchup, IModEntity
{
private const float spawn_cooldown_seconds = 1.5f;
private const string flower_count_attribute = "roamingbees_flowerCount";
private const string crop_count_attribute = "roamingbees_cropCount";
private const string initial_scan_progress_attribute = "roamingbees_initialScanProgress";
private const string rescan_progress_attribute = "roamingbees_rescanProgress";
private static readonly Random random = new();
private readonly List<InternalBeeParticle> activeBees = [];
private readonly TreeAttribute entityAttributeSnapshot = new();
private string facingVariantKey = "side";
private Dictionary<string, Vector3>? entrancePositions;
private Dictionary<string, Vector3>? frontDirections;
private RoamingBeesModSystem? modSystem;
private IPlantPositionRegistry? plantPositionRegistry;
private float rainfall;
private float temperature;
private bool initialized;
private ClimateCondition? climate;
private Vec3d? windVec;
private int onParticleTickStopwatchTriggerCount;
private int spawnStopwatchTriggerCount;
private readonly Stopwatch plantInfoStopwatch = new();
private readonly Stopwatch particleStopwatch = new();
private readonly Stopwatch serverSpawnStopwatch = new();
private readonly Stopwatch handleSpawnStopwatch = new();
private readonly List<StructVec3i> flowerPositions = [];
private readonly List<StructVec3i> cropPositions = [];
private readonly List<Vector3> relativePlantPositions = [];
public const string TARGET_BEE_PARTICLE_COUNT_ATTRIBUTE = "roamingbees_targetBeeParticleCount";
public const string RADIUS_ATTRIBUTE = "roamingbees_radius";
public const string NETWORK_CHANNEL_NAME = "bee particle spawns";
public int ActiveBeesCount => activeBees.Count;
public IEnumerable<BeeSpawnPacket> ActiveBeesPackets => activeBees.Select(x => x.SpawnPacket);
public float TimeSinceLastSpawn { get; private set; } = (float)(random.NextDouble() * spawn_cooldown_seconds);
public int TargetParticleCount { get; private set; }
public int ScanRadius => GetRadiusFromBlockEntity();
public int FlowerCount { get; private set; }
public int CropCount { get; private set; }
public float InitialScanProgress { get; private set; }
public float RescanProgress { get; private set; }
public Mod? Mod { get; private set; }
public override void Initialize(ICoreAPI api, JsonObject properties)
{
if (initialized)
return;
base.Initialize(api, properties);
initialized = true;
facingVariantKey = properties["facingVariantKey"].AsString("side");
entrancePositions = VectorParsing.ParseVector3Map(properties["entrancePositions"]);
frontDirections = VectorParsing.ParseVector3Map(properties["frontDirections"]);
modSystem = api.ModLoader.GetModSystem<RoamingBeesModSystem>();
Mod = modSystem?.Mod;
modSystem?.BeeSpawnPacketDistributor?.Register(Blockentity.Pos, this);
modSystem?.ClientChannel?.SendPacket(new BeeCatchupRequestPacket { HivePosition = Pos });
if (modSystem?.Mod.Info.Version.Contains("dev") == true && modSystem?.ClientChannel != null)
modSystem?.Mod.Logger.Event($"{nameof(BlockEntityBehaviorRoamingBees)} sent {nameof(BeeCatchupRequestPacket)} Pos: {Pos}");
plantPositionRegistry = api.GetPlantPositionRegistry();
var radius = GetRadiusFromBlockEntity();
plantPositionRegistry?.RegisterBeehive(Blockentity.Pos, radius);
var updateFrequency = 20;
Blockentity.RegisterGameTickListener(OnParticleTick, updateFrequency);
Blockentity.RegisterGameTickListener(UpdateClimateInfo, 10000);
Blockentity.RegisterGameTickListener(DecreaseStopwatchCounts, 60000);
if (api.Side.IsServer())
{
Blockentity.RegisterGameTickListener(OnSpawnTick, 500);
Blockentity.RegisterGameTickListener(OnUpdatePlantInfoTick, 5000);
}
}
private void DecreaseStopwatchCounts(float dt)
{
if (spawnStopwatchTriggerCount > 10)
modSystem?.Mod.Logger.Warning($"{nameof(spawnStopwatchTriggerCount)} is {spawnStopwatchTriggerCount}");
spawnStopwatchTriggerCount = Math.Max(0, spawnStopwatchTriggerCount - 1);
if (onParticleTickStopwatchTriggerCount > 10)
modSystem?.Mod.Logger.Warning($"{nameof(onParticleTickStopwatchTriggerCount)} is {onParticleTickStopwatchTriggerCount}");
onParticleTickStopwatchTriggerCount = Math.Max(0, onParticleTickStopwatchTriggerCount - 1);
}
private void UpdateClimateInfo(float dt)
{
windVec = Api.World.BlockAccessor.GetWindSpeedAt(Blockentity.Pos);
climate = Api.World.BlockAccessor.GetClimateAt(Blockentity.Pos, EnumGetClimateMode.NowValues);
}
private int GetRadiusFromBlockEntity()
{
var radius = properties[RADIUS_ATTRIBUTE].AsInt(10);
var tree = new TreeAttribute();
Blockentity.ToTreeAttributes(tree);
radius = tree.GetAsInt(RADIUS_ATTRIBUTE, radius);
return radius;
}
private int GetTargetBeeParticleCountFromBlockEntity()
{
var targetBeeParticleCount = properties[TARGET_BEE_PARTICLE_COUNT_ATTRIBUTE].AsInt(10);
targetBeeParticleCount = entityAttributeSnapshot.GetInt(TARGET_BEE_PARTICLE_COUNT_ATTRIBUTE, targetBeeParticleCount);
return targetBeeParticleCount;
}
public override void OnBlockRemoved()
{
Clear();
base.OnBlockRemoved();
}
public override void OnBlockUnloaded()
{
Clear();
base.OnBlockUnloaded();
}
public override void ToTreeAttributes(ITreeAttribute tree)
{
base.ToTreeAttributes(tree);
tree.SetInt(flower_count_attribute, FlowerCount);
tree.SetInt(crop_count_attribute, CropCount);
tree.SetFloat(initial_scan_progress_attribute, InitialScanProgress);
tree.SetFloat(rescan_progress_attribute, RescanProgress);
}
public override void FromTreeAttributes(ITreeAttribute tree, IWorldAccessor worldAccessForResolve)
{
base.FromTreeAttributes(tree, worldAccessForResolve);
if (Api is null || Api.Side == EnumAppSide.Server)
return;
FlowerCount = tree.GetInt(flower_count_attribute);
CropCount = tree.GetInt(crop_count_attribute);
InitialScanProgress = tree.GetFloat(initial_scan_progress_attribute);
RescanProgress = tree.GetFloat(rescan_progress_attribute);
}
public void HandleBeeParticleSpawn(BeeSpawnPacket packet, bool catchup = false)
{
handleSpawnStopwatch.Restart();
if (Blockentity.Pos != packet.HivePosition)
return;
if (packet.Path is null)
{
modSystem?.Mod.Logger.Warning("Received packet with Path == null");
return;
}
if (packet.Path.Length == 0)
{
modSystem?.Mod.Logger.Warning("Received packet with Path.Length == 0");
return;
}
if (catchup)
{
var totalDelta = Math.Abs(Api.World.Calendar.ElapsedSeconds - packet.TimeElapsedSeconds);
if (totalDelta > 240)
{
Mod?.Logger.Notification($"{nameof(BlockEntityBehaviorBeeSwarm)} {nameof(HandleBeeParticleSpawn)} totalDelta: {totalDelta}s, discarding");
return;
}
}
var facing = packet.Facing ?? GetFacing();
Vector3 entrancePosition = GetEntrancePosition(facing);
var bee = new InternalBeeParticle(entrancePosition, GetFrontDirection(facing), [.. packet.Path.Select(x => x.ToPoint())], BeeRole.Forager, entrancePosition, packet);
activeBees.Add(bee);
if (catchup)
{
var totalDelta = Math.Abs(Api.World.Calendar.ElapsedSeconds - packet.TimeElapsedSeconds);
if (modSystem?.Mod.Info.Version.Contains("dev") == true)
modSystem.Mod.Logger.Notification($"HandleBeeParticleSpawn catchup totalDelta: {totalDelta}s");
for (var i = 0; i < totalDelta / 0.1f; i++)
bee.Step(0.1f, 0.1f); // todo
}
handleSpawnStopwatch.StopAndLogTime(this, 0.05);
}
public void Clear()
{
if (modSystem is null)
return;
modSystem.GlobalActiveBees -= activeBees.Count;
modSystem.BeeSpawnPacketDistributor?.Unregister(Blockentity.Pos);
plantPositionRegistry?.UnregisterBeehive(Blockentity.Pos);
activeBees.Clear();
}
private void OnUpdatePlantInfoTick(float dt)
{
if (!ShouldTick())
return;
plantInfoStopwatch.Restart();
UpdatePlantInfo();
var note = $"global bees: {modSystem?.GlobalActiveBees}, registered hives: {modSystem?.BeeSpawnPacketDistributor?.SpawnHandlersCount}, radius: {Config.Instance.BeeRoamingRadius}";
plantInfoStopwatch.StopAndLogTime(this, 0.1, note);
}
private bool ShouldTick()
{
if (Block is BlockSkep && !Config.Instance.EnableOnVanillaSkeps)
return false;
var isFgc = Block?.Code?.Domain == "fromgoldencombs";
var path = Block?.Code?.Path ?? string.Empty;
var isFgcLangstroth = isFgc && path.Contains("langstrothstack", StringComparison.OrdinalIgnoreCase);
var isFgcCeramic = isFgc && path.Contains("ceramicbroodpot", StringComparison.OrdinalIgnoreCase);
if (isFgcLangstroth)
{
if (!Config.Instance.EnableOnFgcLangstroth)
return false;
if (!HasRequiredLangstrothBase())
return false;
}
if (isFgcCeramic && !Config.Instance.EnableOnFgcCeramic)
return false;
return true;
}
private void OnParticleTick(float dt)
{
if (!ShouldTick())
return;
var isFgc = Block?.Code?.Domain == "fromgoldencombs";
var path = Block?.Code?.Path ?? string.Empty;
var isFgcLangstroth = isFgc && path.Contains("langstrothstack", StringComparison.OrdinalIgnoreCase);
var isFgcCeramic = isFgc && path.Contains("ceramicbroodpot", StringComparison.OrdinalIgnoreCase);
particleStopwatch.Restart();
entityAttributeSnapshot.Clear();
Blockentity.ToTreeAttributes(entityAttributeSnapshot);
var targetBeeParticleCount = GetTargetBeeParticleCountFromBlockEntity();
TargetParticleCount = Math.Max(0, targetBeeParticleCount);
if (TargetParticleCount <= 0)
return;
if (Block is BlockSkep)
TargetParticleCount = Config.Instance.RoamingBeesPerVanillaSkep;
if (isFgcLangstroth)
TargetParticleCount = Config.Instance.RoamingBeesPerFgcLangstroth;
if (isFgcCeramic)
TargetParticleCount = Config.Instance.RoamingBeesPerFgcCeramic;
Update(dt);
if (particleStopwatch.StopAndLogTime(this, 0.1, note: $"global bees: {modSystem?.GlobalActiveBees}, registered hives: {modSystem?.BeeSpawnPacketDistributor?.SpawnHandlersCount}"))
onParticleTickStopwatchTriggerCount++;
}
private void Update(float dt)
{
if (dt > 10f)
return;
if (climate is null || windVec is null)
return;
dt = Math.Min(0.5f, dt);
var windSpeed = (float)Math.Min(windVec.Length(), 1.0);
rainfall = climate?.Rainfall ?? 0f;
temperature = climate?.Temperature ?? 20f;
// iterate from end to easily remove from list
var end = activeBees.Count - 1;
end -= onParticleTickStopwatchTriggerCount * 10;
for (int i = end; i >= 0; i--)
{
var bee = activeBees[i];
if (Api.Side == EnumAppSide.Client)
{
bee.Step(dt, windSpeed);
if (Config.Instance.ReceiveParticles && Api.World is IClientWorldAccessor clientWorld)
BeeVisualParticleRenderer.Spawn(clientWorld, Blockentity.Pos, bee);
}
if (Api.Side == EnumAppSide.Server)
bee.Step(dt, windSpeed);
if (bee.ShouldBeDespawned && modSystem != null)
{
activeBees.RemoveAt(i);
modSystem.GlobalActiveBees--;
}
}
TimeSinceLastSpawn += dt;
}
private void OnSpawnTick(float dt)
{
if (!ShouldTick())
return;
if (climate is null || windVec is null)
return;
serverSpawnStopwatch.Restart();
TrySpawnNewBee(TargetParticleCount);
if (serverSpawnStopwatch.StopAndLogTime(this, 0.1))
spawnStopwatchTriggerCount++;
}
private void TrySpawnNewBee(int targetParticleCount)
{
int desiredParticleCount = targetParticleCount;
if (activeBees.Count >= desiredParticleCount)
return;
if (TimeSinceLastSpawn < spawn_cooldown_seconds)
return;
if (relativePlantPositions.Count == 0)
return;
var cfg = Config.Instance;
var sunPos = Api.World.Calendar.GetSunPosition(new Vec3d(Blockentity.Pos.X, Blockentity.Pos.Y, Blockentity.Pos.Z), Api.World.Calendar.TotalDays);
float sunAltitudeDegrees = MathF.Asin(sunPos.Y) * 180f / GameMath.PI;
if (sunAltitudeDegrees <= cfg.SunAltitudeMinDegrees)
return;
if (rainfall >= cfg.RainfallSpawnStopThreshold)
return;
int maxGlobal = cfg.MaxGlobalRoamingBees;
if (modSystem?.ServerChannel is null)
return;
if (modSystem.GlobalActiveBees >= maxGlobal)
return;
float sunAltitudeModifier = 1 - Math.Clamp((sunAltitudeDegrees - cfg.SunAltitudeMinDegrees) / cfg.SunAltitudeRangeDegrees, 0f, 1f);
float sunAltitudeCooldownPenalty = sunAltitudeModifier * cfg.MaxSunAltitudeCooldownPenalty;
float rainfallFactor = rainfall / cfg.RainfallSpawnStopThreshold;
float rainfallCooldownPenalty = rainfallFactor * cfg.MaxRainfallCooldownPenalty;
float spawnCooldownSeconds = rainfallCooldownPenalty + sunAltitudeCooldownPenalty;
float temperatureRange = cfg.MaxTemperatureParticleSpawn - cfg.MinTemperatureParticleSpawn;
float temperatureFactor = Math.Clamp((cfg.MaxTemperatureParticleSpawn - temperature) / temperatureRange, 0f, 1f);
var randomChance = random.NextDouble() > temperatureFactor && random.NextDouble() > sunAltitudeModifier && random.NextDouble() > 0.3f;
if (!randomChance)
{
var noise = random.NextDouble() * 2;
TimeSinceLastSpawn = -(float)(random.NextDouble() * spawnCooldownSeconds + noise);
return;
}
var facing = GetFacing();
var entrancePos = GetEntrancePosition(facing);
var frontDirection = GetFrontDirection(facing);
var path = BeePathGeneration.GeneratePath(Api.World.BlockAccessor, Blockentity.Pos, entrancePos, frontDirection, relativePlantPositions);
if (path is null || path.Length == 0)
return;
var message = new BeeSpawnPacket
{
HivePosition = Blockentity.Pos,
Path = [.. path.Select(x => BeePlannedPathPointContract.FromPoint(x))],
Facing = facing,
Role = BeeRole.Forager,
TimeElapsedSeconds = Api.World.Calendar.ElapsedSeconds,
EntrancePosition = entrancePos,
DespawnPosition = entrancePos,
};
activeBees.Add(new InternalBeeParticle(entrancePos, frontDirection, path, BeeRole.Forager, entrancePos, message));
modSystem.GlobalActiveBees++;
TimeSinceLastSpawn = 0f;
modSystem.ServerChannel.BroadcastPacket(message);
}
private bool HasRequiredLangstrothBase()
{
if (Blockentity is not BlockEntityDisplay blockEntityDisplay)
return false;
return blockEntityDisplay.Inventory.Any(x => x.Itemstack?.Block?.Code?.Path?.Contains("langstrothbase", StringComparison.OrdinalIgnoreCase) == true);
}
public Vector3 GetEntrancePosition() => GetEntrancePosition(GetFacing());
public Vector3 GetFrontDirection() => GetFrontDirection(GetFacing());
private Vector3 GetEntrancePosition(string facing)
{
if (entrancePositions != null)
{
if (entrancePositions.TryGetValue(facing, out var entrance))
return entrance;
if (entrancePositions.TryGetValue("*", out entrance))
return entrance;
}
return facing switch
{
"north" => new Vector3(0.5f, 0.2f, 0.9f),
"east" => new Vector3(0.1f, 0.2f, 0.5f),
"south" => new Vector3(0.5f, 0.2f, 0.1f),
"west" => new Vector3(0.9f, 0.2f, 0.5f),
_ => new Vector3(0.5f, 0.2f, 0.9f)
};
}
private void UpdatePlantInfo()
{
relativePlantPositions.Clear();
int radius = Config.Instance.BeeRoamingRadius;
if (radius <= 0)
{
radius = entityAttributeSnapshot.GetInt(RADIUS_ATTRIBUTE, 10);
if (radius <= 0)
return;
}
var plantRegistry = Api.GetPlantPositionRegistry();
if (plantRegistry is null)
return;
var (initialScanProgress, rescanProgress) = plantRegistry.GetPlantsNearPosition(Blockentity.Pos, radius, flowerPositions, cropPositions);
FlowerCount = flowerPositions.Count;
CropCount = cropPositions.Count;
InitialScanProgress = initialScanProgress;
RescanProgress = rescanProgress;
foreach (var pos in flowerPositions)
{
float relX = pos.X - Blockentity.Pos.X + 0.5f;
float relY = pos.Y - Blockentity.Pos.Y + 0.5f;
float relZ = pos.Z - Blockentity.Pos.Z + 0.5f;
relativePlantPositions.Add(new Vector3(relX, relY, relZ));
}
foreach (var pos in cropPositions)
{
float relX = pos.X - Blockentity.Pos.X + 0.5f;
float relY = pos.Y - Blockentity.Pos.Y + 0.5f;
float relZ = pos.Z - Blockentity.Pos.Z + 0.5f;
relativePlantPositions.Add(new Vector3(relX, relY, relZ));
}
}
private Vector3 GetFrontDirection(string facing)
{
if (frontDirections != null)
{
if (frontDirections.TryGetValue(facing, out var frontDirection))
return frontDirection;
if (frontDirections.TryGetValue("*", out frontDirection))
return frontDirection;
}
return facing switch
{
"north" => new Vector3(0f, 0f, 1f),
"east" => new Vector3(-1f, 0f, 0f),
"south" => new Vector3(0f, 0f, -1f),
"west" => new Vector3(1f, 0f, 0f),
_ => new Vector3(0f, 0f, 1f)
};
}
private string GetFacing()
{
if (!string.IsNullOrWhiteSpace(facingVariantKey))
{
var block = Blockentity.Block;
if (block?.Variant?.ContainsKey(facingVariantKey) == true)
return block.Variant[facingVariantKey];
}
// omnidirectional block, randomly pick from available entrance directions
if (entrancePositions is { Count: > 0 })
{
var keys = entrancePositions.Keys.Where(k => k != "*").ToList();
if (keys.Count > 0)
return keys[random.Next(keys.Count)];
}
return "north";
}
}