1094 lines
36 KiB
C#
1094 lines
36 KiB
C#
using OrekiWoofsBeehives.Helpers;
|
|
using OrekiWoofsBeehives.Utilities;
|
|
using OrekiWoofsBees.Common;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Text;
|
|
using Vintagestory.API.Common;
|
|
using Vintagestory.API.Datastructures;
|
|
using Vintagestory.API.MathTools;
|
|
using Vintagestory.GameContent;
|
|
|
|
namespace OrekiWoofsBeehives.BlockEntities;
|
|
|
|
public class BlockEntityReusableBeehive : BlockEntityContainer, IModEntity
|
|
{
|
|
private const string feed_remaining_attribute = "feedRemaining";
|
|
private const double feed_full_amount = 1.0;
|
|
private const double feed_empty_threshold = 0.0001;
|
|
private const double swarm_building_hours = 3.0;
|
|
private const double swarm_hanging_hours = 5.0;
|
|
|
|
private readonly Stopwatch stopwatch = new();
|
|
private readonly StringBuilder infoStringBuilder = new();
|
|
private readonly InventoryGeneric inventory = new(8, null, null);
|
|
private double lastUpdateTotalHours = 0;
|
|
private bool wasFullyScanned;
|
|
private float? scanningProgress;
|
|
private float? rescanningProgress;
|
|
private RoomRegistry? roomRegistry;
|
|
private readonly List<BlockPos> positionsToCheckGreenhouse = [];
|
|
private bool isInGreenhouse;
|
|
private BlockPos? activeSwarmPos;
|
|
private BlockPos? incomingSwarmPos;
|
|
private int? registeredRadius;
|
|
|
|
public override InventoryBase Inventory => inventory;
|
|
public override string InventoryClassName => "beehive";
|
|
|
|
public int? FlowersAround { get; internal set; }
|
|
public int? CropsAround { get; internal set; }
|
|
|
|
public double BeePopulation { get; internal set; }
|
|
public double HoneyProgress { get; internal set; }
|
|
public double NextSwarmAllowedTotalDays { get; internal set; }
|
|
public bool IsReceivingIncomingSwarm => incomingSwarmPos != null;
|
|
|
|
public double PreSwarmProgress { get; internal set; }
|
|
public bool SwarmsDisabled { get; internal set; }
|
|
|
|
public float TimeSinceLastSpawn { get; private set; }
|
|
public bool WasFullyScanned => wasFullyScanned;
|
|
public bool OpenForIncomingSwarms => BeePopulation < Config.Instance.BeehiveConsideredEmptyBelowPopulation && !IsReceivingIncomingSwarm;
|
|
|
|
public Mod? Mod { get; private set; }
|
|
|
|
public bool IsOpen { get; set; }
|
|
|
|
public override void Initialize(ICoreAPI api)
|
|
{
|
|
base.Initialize(api);
|
|
Mod = api.GetOrekiWoofsBeehives()?.Mod;
|
|
|
|
roomRegistry = Api.ModLoader.GetModSystem<RoomRegistry>();
|
|
positionsToCheckGreenhouse.Clear();
|
|
positionsToCheckGreenhouse.AddRange([Pos.UpCopy(), Pos.DownCopy(), Pos.NorthCopy(), Pos.EastCopy(), Pos.WestCopy(), Pos.SouthCopy()]);
|
|
|
|
if (lastUpdateTotalHours == 0)
|
|
lastUpdateTotalHours = api.World.Calendar.TotalHours;
|
|
|
|
|
|
var plantPositionRegistry = Api.GetPlantPositionRegistry();
|
|
|
|
if (plantPositionRegistry is null)
|
|
{
|
|
api.Logger.Warning($"{nameof(plantPositionRegistry)} still not loaded!");
|
|
return;
|
|
}
|
|
|
|
plantPositionRegistry.RegisterBeehive(Pos, Config.Instance.BeehiveRadius);
|
|
registeredRadius = Config.Instance.BeehiveRadius;
|
|
|
|
var beehivesModSystem = Api.GetOrekiWoofsBeehives();
|
|
if (beehivesModSystem is null)
|
|
{
|
|
api.Logger.Warning($"{nameof(OrekiWoofsBeehivesModSystem)} still not loaded!");
|
|
return;
|
|
}
|
|
beehivesModSystem.BeehiveRegistry.Register(this);
|
|
|
|
#if DEBUG
|
|
RegisterGameTickListener(OnGameTick, 500);
|
|
#else
|
|
RegisterGameTickListener(OnGameTick, 5_000);
|
|
#endif
|
|
|
|
if (wasFullyScanned && api.Side.IsServer())
|
|
{
|
|
// we trust our loaded numbers, don't take the scanning of PlantPositionRegistry yet.
|
|
// However, we do need to update when plants are placed or removed.
|
|
// Should unsub when the new scan is done.
|
|
plantPositionRegistry.CropEvent += OnCropEvent;
|
|
plantPositionRegistry.FlowerEvent += OnFlowerEvent;
|
|
}
|
|
}
|
|
|
|
private void OnCropEvent(BlockPos pos, int delta)
|
|
{
|
|
var radius = Math.Max(1, Config.Instance.BeehiveRadius);
|
|
if (Overlaps.IsWithinSphericalRadius(Pos, StructVec3i.FromBlockPos(pos), radius))
|
|
{
|
|
CropsAround += delta;
|
|
base.MarkDirty(false);
|
|
}
|
|
}
|
|
|
|
private void OnFlowerEvent(BlockPos pos, int delta)
|
|
{
|
|
var radius = Math.Max(1, Config.Instance.BeehiveRadius);
|
|
if (Overlaps.IsWithinSphericalRadius(Pos, StructVec3i.FromBlockPos(pos), radius))
|
|
{
|
|
FlowersAround += delta;
|
|
base.MarkDirty(false);
|
|
}
|
|
}
|
|
|
|
public override void GetBlockInfo(IPlayer forPlayer, StringBuilder dsc)
|
|
{
|
|
dsc.Append(infoStringBuilder);
|
|
foreach (BlockEntityBehavior behavior in Behaviors)
|
|
behavior.GetBlockInfo(forPlayer, dsc);
|
|
}
|
|
|
|
public float GetScanningProgress() => scanningProgress ?? 0f;
|
|
public float GetRescanningProgress() => rescanningProgress ?? 0f;
|
|
|
|
private void OnGameTick(float dt)
|
|
{
|
|
stopwatch.Restart();
|
|
|
|
UpdateGreenhouseStatus();
|
|
stopwatch.LogTime(this, 0.05, "after greenhouse");
|
|
UpdateStatsText();
|
|
stopwatch.LogTime(this, 0.05, "after stats");
|
|
|
|
if (Api.Side == EnumAppSide.Client)
|
|
return;
|
|
UpdateFlowersAndCrops();
|
|
stopwatch.LogTime(this, 0.05, "after flowers and crops");
|
|
UpdateBeePopulationAndHoney();
|
|
stopwatch.LogTime(this, 0.05, "after population and honey");
|
|
UpdateRadius();
|
|
|
|
stopwatch.StopAndLogTime(this, 0.05);
|
|
}
|
|
|
|
private void UpdateRadius()
|
|
{
|
|
if (registeredRadius == Config.Instance.BeehiveRadius)
|
|
return;
|
|
|
|
scanningProgress = 0;
|
|
rescanningProgress = 0;
|
|
FlowersAround = null;
|
|
CropsAround = null;
|
|
wasFullyScanned = false;
|
|
|
|
var plantPositionRegistry = Api.GetPlantPositionRegistry();
|
|
try
|
|
{
|
|
plantPositionRegistry?.UpdateBeehiveRadius(Pos, Config.Instance.BeehiveRadius);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Mod?.Logger.Warning("Couldn't update radius");
|
|
Mod?.Logger.Warning(e);
|
|
}
|
|
registeredRadius = Config.Instance.BeehiveRadius;
|
|
}
|
|
|
|
private void ClearStaleIncomingSwarmReservation()
|
|
{
|
|
if (incomingSwarmPos == null)
|
|
return;
|
|
|
|
if (Api.World.BlockAccessor.GetBlockEntity(incomingSwarmPos) is BlockEntityBeeSwarm)
|
|
return;
|
|
|
|
incomingSwarmPos = null;
|
|
MarkDirty(false);
|
|
}
|
|
|
|
private void UpdateStatsText()
|
|
{
|
|
var stats = CalculateStats();
|
|
infoStringBuilder.Clear();
|
|
BeehiveInfoStringBuilder.BuildBeehiveInfo(infoStringBuilder, stats, this);
|
|
}
|
|
|
|
|
|
private void UpdateFlowersAndCrops()
|
|
{
|
|
int radius = Config.Instance.BeehiveRadius;
|
|
IPlantPositionRegistry? modSystem = Api.GetPlantPositionRegistry();
|
|
if (modSystem is null)
|
|
return;
|
|
|
|
var (flowerCount, cropCount, initialProgress, rescanProgress) = modSystem.GetPlantCountsNearPosition(Pos, radius);
|
|
|
|
if (Api.Side.IsServer())
|
|
{
|
|
scanningProgress = initialProgress;
|
|
rescanningProgress = rescanProgress;
|
|
}
|
|
|
|
if (!wasFullyScanned || !(wasFullyScanned && initialProgress < 1f))
|
|
{
|
|
FlowersAround = flowerCount;
|
|
CropsAround = cropCount;
|
|
}
|
|
if (initialProgress == 1)
|
|
{
|
|
wasFullyScanned = true;
|
|
Api.GetPlantPositionRegistry()!.CropEvent -= OnCropEvent;
|
|
Api.GetPlantPositionRegistry()!.FlowerEvent -= OnFlowerEvent;
|
|
}
|
|
}
|
|
|
|
private void UpdateBeePopulationAndHoney()
|
|
{
|
|
double currentTotalHours = Api.World.Calendar.TotalHours;
|
|
double hoursElapsed = currentTotalHours - lastUpdateTotalHours;
|
|
|
|
if (hoursElapsed <= 0)
|
|
return;
|
|
|
|
double hoursPerDay = Api.World.Calendar.HoursPerDay;
|
|
double daysElapsed;
|
|
|
|
var maxDaysSkip = 0.3;
|
|
var maxHoursSkip = maxDaysSkip * hoursPerDay;
|
|
var isCatchUp = hoursElapsed > maxHoursSkip;
|
|
var startingPopulation = BeePopulation;
|
|
var startingHoneyProgress = HoneyProgress;
|
|
|
|
if (isCatchUp)
|
|
{
|
|
Mod?.Logger.Notification($"{nameof(BlockEntityReusableBeehive)}.{nameof(UpdateBeePopulationAndHoney)} catchup detected.");
|
|
BroadcastUnloadDebug($"hive {Pos}: catch-up start elapsed={hoursElapsed:F2}h population={startingPopulation:F0} honeyProgress={startingHoneyProgress:F3}");
|
|
}
|
|
|
|
var framesChangedVisually = false;
|
|
var maxSteps = 10;
|
|
var steps = 0;
|
|
while (lastUpdateTotalHours < currentTotalHours)
|
|
{
|
|
steps++;
|
|
if (steps > maxSteps)
|
|
{
|
|
Mod?.Logger.Notification($"{nameof(BlockEntityReusableBeehive)}.{nameof(UpdateBeePopulationAndHoney)} catchup paused. {nameof(lastUpdateTotalHours)}={lastUpdateTotalHours}, {nameof(currentTotalHours)}={currentTotalHours}");
|
|
break;
|
|
}
|
|
|
|
var populationBeforeStep = BeePopulation;
|
|
|
|
if (lastUpdateTotalHours + maxHoursSkip < currentTotalHours)
|
|
{
|
|
lastUpdateTotalHours += maxHoursSkip;
|
|
daysElapsed = maxDaysSkip;
|
|
}
|
|
else
|
|
{
|
|
daysElapsed = (currentTotalHours - lastUpdateTotalHours) / hoursPerDay;
|
|
lastUpdateTotalHours = currentTotalHours;
|
|
}
|
|
|
|
var lastUpdateTotalDays = lastUpdateTotalHours / Api.World.Calendar.HoursPerDay;
|
|
var stats = CalculateStats(lastUpdateTotalDays);
|
|
|
|
UpdateBeePopulation(daysElapsed, stats);
|
|
framesChangedVisually |= UpdateHoneyProduction(daysElapsed, stats);
|
|
framesChangedVisually |= UpdateFeedConsumption(daysElapsed, stats);
|
|
|
|
ClearStaleIncomingSwarmReservation();
|
|
UpdateSwarming(lastUpdateTotalHours, daysElapsed * hoursPerDay, currentTotalHours);
|
|
|
|
if (isCatchUp)
|
|
{
|
|
var populationDelta = BeePopulation - populationBeforeStep;
|
|
if (Math.Abs(populationDelta) > 0.01)
|
|
BroadcastUnloadDebug($"hive {Pos}: step @hour={lastUpdateTotalHours:F2} deltaPopulation={populationDelta:F2} population={BeePopulation:F2}");
|
|
}
|
|
}
|
|
|
|
if (isCatchUp)
|
|
BroadcastUnloadDebug($"hive {Pos}: catch-up end population={BeePopulation:F2} (delta={(BeePopulation - startingPopulation):F2}) honeyProgress={HoneyProgress:F3}");
|
|
|
|
MarkDirty(framesChangedVisually);
|
|
}
|
|
|
|
private void UpdateSwarming(double simulationTotalHours, double hoursElapsed, double? catchUpTargetTotalHours)
|
|
{
|
|
if (!Config.Instance.EnableSwarms)
|
|
return;
|
|
|
|
if (activeSwarmPos != null)
|
|
{
|
|
if (Api.World.BlockAccessor.GetBlockEntity(activeSwarmPos) is BlockEntityBeeSwarm)
|
|
return;
|
|
|
|
if (catchUpTargetTotalHours.HasValue)
|
|
BroadcastUnloadDebug($"hive {Pos}: active swarm reference {activeSwarmPos} no longer exists during catch-up");
|
|
|
|
activeSwarmPos = null;
|
|
}
|
|
|
|
var hoursPerDay = Math.Max(1, Api.World.Calendar.HoursPerDay);
|
|
var simulationTotalDays = simulationTotalHours / hoursPerDay;
|
|
|
|
if (!CanBeginPreSwarm(simulationTotalDays))
|
|
{
|
|
if (PreSwarmProgress > 0)
|
|
{
|
|
PreSwarmProgress = 0;
|
|
MarkDirty(false);
|
|
}
|
|
return;
|
|
}
|
|
|
|
var stats = CalculateStats(simulationTotalDays);
|
|
var cfg = Config.Instance;
|
|
var ratePerHour = 1.0 / Math.Max(1, cfg.PreSwarmDurationHours);
|
|
|
|
if (stats.Components.Temperature >= cfg.TemperatureOptimal)
|
|
PreSwarmProgress += ratePerHour * hoursElapsed;
|
|
else
|
|
PreSwarmProgress -= ratePerHour * hoursElapsed;
|
|
|
|
PreSwarmProgress = Math.Clamp(PreSwarmProgress, 0, 1);
|
|
MarkDirty(false);
|
|
|
|
if (!IsReadyToStartSwarm(simulationTotalHours))
|
|
return;
|
|
|
|
var swarmPopulationPct = HasEligibleSwarmTargets() ? cfg.SwarmPopulationPercentage : cfg.SwarmPopulationPercentageWhenNoBeehivesAvailable;
|
|
|
|
if (swarmPopulationPct == 0)
|
|
{
|
|
var hoursPerDayForCooldown = Math.Max(1, Api.World.Calendar.HoursPerDay);
|
|
NextSwarmAllowedTotalDays = (simulationTotalHours / hoursPerDayForCooldown) + cfg.SwarmCooldownDays;
|
|
MarkDirty(false);
|
|
return;
|
|
}
|
|
|
|
var plannedSwarmPopulation = BeePopulation * (swarmPopulationPct / 100.0);
|
|
if (plannedSwarmPopulation <= 0)
|
|
return;
|
|
|
|
if (!TryFindSwarmSpawnPosition(out var swarmPos, out var attachmentSide))
|
|
return;
|
|
|
|
var swarmBlock = Api.World.GetBlock(new AssetLocation($"orekiwoofsbeehives:beeswarm-{attachmentSide}"));
|
|
if (swarmBlock == null)
|
|
return;
|
|
|
|
Api.World.BlockAccessor.SetBlock(swarmBlock.BlockId, swarmPos);
|
|
|
|
if (Api.World.BlockAccessor.GetBlockEntity(swarmPos) is not BlockEntityBeeSwarm swarmBe)
|
|
return;
|
|
|
|
var isCatchUpSpawn = catchUpTargetTotalHours.HasValue && catchUpTargetTotalHours.Value > simulationTotalHours;
|
|
if (isCatchUpSpawn)
|
|
BroadcastUnloadDebug($"hive {Pos}: spawned swarm {swarmPos} at simulatedHour={simulationTotalHours:F2}, plannedPopulation={plannedSwarmPopulation:F2}, attachment={attachmentSide}");
|
|
|
|
swarmBe.InitializeFromOrigin(
|
|
originHivePos: Pos,
|
|
plannedPopulation: plannedSwarmPopulation,
|
|
spawnTotalHours: simulationTotalHours,
|
|
buildingDurationHours: swarm_building_hours,
|
|
hangingDurationHours: swarm_hanging_hours
|
|
);
|
|
|
|
NextSwarmAllowedTotalDays = (simulationTotalHours / hoursPerDay) + cfg.SwarmCooldownDays;
|
|
activeSwarmPos = swarmPos;
|
|
PreSwarmProgress = 0;
|
|
|
|
if (catchUpTargetTotalHours.HasValue && catchUpTargetTotalHours.Value > simulationTotalHours)
|
|
{
|
|
swarmBe.FastForwardTo(catchUpTargetTotalHours.Value, debugAsCatchUp: true);
|
|
|
|
if (Api.World.BlockAccessor.GetBlockEntity(swarmPos) is BlockEntityBeeSwarm)
|
|
BroadcastUnloadDebug($"hive {Pos}: swarm {swarmPos} catch-up ended still active");
|
|
else
|
|
BroadcastUnloadDebug($"hive {Pos}: swarm {swarmPos} catch-up ended and swarm finished");
|
|
}
|
|
|
|
MarkDirty(false);
|
|
}
|
|
|
|
public bool IsReadyToStartSwarm(bool ignoreDayTime = false)
|
|
{
|
|
if (Api?.World == null)
|
|
return false;
|
|
|
|
return IsReadyToStartSwarm(Api.World.Calendar.TotalHours, ignoreDayTime);
|
|
}
|
|
|
|
private bool IsReadyToStartSwarm(double simulationTotalHours, bool ignoreDayTime = false)
|
|
{
|
|
if (Api?.World == null)
|
|
return false;
|
|
|
|
if (activeSwarmPos != null)
|
|
return false;
|
|
|
|
if (!Config.Instance.EnableSwarms || SwarmsDisabled)
|
|
return false;
|
|
|
|
var hoursPerDay = Math.Max(1, Api.World.Calendar.HoursPerDay);
|
|
var simulationTotalDays = simulationTotalHours / hoursPerDay;
|
|
if (simulationTotalDays < NextSwarmAllowedTotalDays)
|
|
return false;
|
|
|
|
var cfg = Config.Instance;
|
|
var requiredPopulation = cfg.MaxBeePopulation * (cfg.PopulationPercentRequirementForSwarm / 100.0);
|
|
if (BeePopulation <= requiredPopulation)
|
|
return false;
|
|
|
|
if (PreSwarmProgress < 1.0)
|
|
return false;
|
|
|
|
if (ignoreDayTime)
|
|
return true;
|
|
|
|
var dayHour = simulationTotalHours % hoursPerDay;
|
|
if (dayHour < 0)
|
|
dayHour += hoursPerDay;
|
|
|
|
if (dayHour is < 8 or > 12)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool CanBeginPreSwarm(double simulationTotalDays)
|
|
{
|
|
if (activeSwarmPos != null)
|
|
return false;
|
|
|
|
if (!Config.Instance.EnableSwarms || SwarmsDisabled)
|
|
return false;
|
|
|
|
if (simulationTotalDays < NextSwarmAllowedTotalDays)
|
|
return false;
|
|
|
|
var cfg = Config.Instance;
|
|
var requiredPopulation = cfg.MaxBeePopulation * (cfg.PopulationPercentRequirementForSwarm / 100.0);
|
|
if (BeePopulation <= requiredPopulation)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
public bool IsSwarmBuildingNearby()
|
|
{
|
|
if (Api?.World == null || activeSwarmPos == null)
|
|
return false;
|
|
|
|
if (Api.World.BlockAccessor.GetBlockEntity(activeSwarmPos) is not BlockEntityBeeSwarm swarm)
|
|
return false;
|
|
|
|
return swarm.SwarmState == SwarmState.BuildingSwarm;
|
|
}
|
|
|
|
public double TakeBeePopulationForSwarm(double requestedAmount)
|
|
{
|
|
var clampedAmount = Math.Max(0, requestedAmount);
|
|
if (clampedAmount <= 0 || BeePopulation <= 0)
|
|
return 0;
|
|
|
|
var taken = Math.Min(BeePopulation, clampedAmount);
|
|
BeePopulation -= taken;
|
|
MarkDirty(false);
|
|
return taken;
|
|
}
|
|
|
|
public double AddBeePopulationFromSwarm(double requestedAmount)
|
|
{
|
|
var clampedAmount = Math.Max(0, requestedAmount);
|
|
if (clampedAmount <= 0)
|
|
return 0;
|
|
|
|
var cfg = Config.Instance;
|
|
var room = Math.Max(0, cfg.MaxBeePopulation - BeePopulation);
|
|
var added = Math.Min(room, clampedAmount);
|
|
BeePopulation += added;
|
|
MarkDirty(false);
|
|
return added;
|
|
}
|
|
|
|
public void ClearActiveSwarm(BlockPos swarmPos)
|
|
{
|
|
if (activeSwarmPos == null)
|
|
return;
|
|
|
|
if (!activeSwarmPos.Equals(swarmPos))
|
|
return;
|
|
|
|
activeSwarmPos = null;
|
|
MarkDirty(false);
|
|
}
|
|
|
|
public void SetIncomingSwarm(BlockPos swarmPos)
|
|
{
|
|
incomingSwarmPos = swarmPos.Copy();
|
|
MarkDirty(false);
|
|
}
|
|
|
|
public void ClearIncomingSwarm(BlockPos swarmPos)
|
|
{
|
|
if (incomingSwarmPos == null)
|
|
return;
|
|
|
|
if (!incomingSwarmPos.Equals(swarmPos))
|
|
return;
|
|
|
|
incomingSwarmPos = null;
|
|
MarkDirty(false);
|
|
}
|
|
|
|
private bool HasEligibleSwarmTargets()
|
|
{
|
|
var cfg = Config.Instance;
|
|
var radius = Math.Max(1, cfg.BeehiveRadius);
|
|
var modSystem = Api.GetOrekiWoofsBeehives();
|
|
if (modSystem == null)
|
|
return false;
|
|
|
|
foreach (var pos in modSystem.BeehiveRegistry.BeehivePositions)
|
|
{
|
|
var targetPos = new BlockPos(pos.X, pos.Y, pos.Z);
|
|
if (targetPos.Equals(Pos))
|
|
continue;
|
|
|
|
if (!Overlaps.IsWithinSphericalRadius(Pos, pos, radius))
|
|
continue;
|
|
|
|
if (Api.World.BlockAccessor.GetBlockEntity(targetPos) is BlockEntityReusableBeehive hive && hive.OpenForIncomingSwarms)
|
|
return true;
|
|
}
|
|
|
|
foreach (var entry in modSystem.VanillaSkepRegistry.Entries)
|
|
{
|
|
if (!Overlaps.IsWithinSphericalRadius(Pos, entry.Key, radius))
|
|
continue;
|
|
|
|
var targetPos = new BlockPos(entry.Key.X, entry.Key.Y, entry.Key.Z);
|
|
var block = Api.World.BlockAccessor.GetBlock(targetPos);
|
|
if (block?.Code?.Path?.Contains("empty") == true)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool TryFindSwarmSpawnPosition(out BlockPos swarmPos, out string attachmentSide)
|
|
{
|
|
stopwatch.Restart();
|
|
swarmPos = Pos.Copy();
|
|
attachmentSide = "down";
|
|
|
|
var sideCandidatesWood = new List<SwarmSpawnCandidate>();
|
|
var sideCandidatesNonWood = new List<SwarmSpawnCandidate>();
|
|
var floorCandidates = new List<SwarmSpawnCandidate>();
|
|
var ceilingCandidates = new List<SwarmSpawnCandidate>();
|
|
|
|
const int horizontalSearchRadius = 10;
|
|
const int verticalSearchHalfHeight = 5;
|
|
var horizontalSearchRadiusSquared = horizontalSearchRadius * horizontalSearchRadius;
|
|
var ba = Api.World.BlockAccessor;
|
|
for (int dx = -horizontalSearchRadius; dx <= horizontalSearchRadius; dx++)
|
|
{
|
|
for (int dy = -verticalSearchHalfHeight; dy <= verticalSearchHalfHeight; dy++)
|
|
{
|
|
for (int dz = -horizontalSearchRadius; dz <= horizontalSearchRadius; dz++)
|
|
{
|
|
if ((dx * dx) + (dz * dz) > horizontalSearchRadiusSquared)
|
|
continue;
|
|
|
|
var basePos = Pos.AddCopy(dx, dy, dz);
|
|
var supportBlock = ba.GetBlock(basePos);
|
|
if (supportBlock == null || supportBlock.Id == 0)
|
|
continue;
|
|
var supportBias = GetSupportBias(supportBlock, basePos);
|
|
|
|
foreach (var facing in BlockFacing.HORIZONTALS)
|
|
{
|
|
if (!supportBlock.SideSolid[facing.Index])
|
|
continue;
|
|
|
|
var candidateSwarmPos = basePos.AddCopy(facing);
|
|
if (!IsEmptyForSwarm(candidateSwarmPos))
|
|
continue;
|
|
|
|
var candidateAttachmentSide = GetAttachmentSideCode(basePos, candidateSwarmPos);
|
|
var candidateWeight = CalculateCandidateWeight(candidateSwarmPos, candidateAttachmentSide, supportBias);
|
|
|
|
if (supportBlock.BlockMaterial == EnumBlockMaterial.Wood)
|
|
sideCandidatesWood.Add(new SwarmSpawnCandidate(candidateSwarmPos, candidateAttachmentSide, candidateWeight));
|
|
else
|
|
sideCandidatesNonWood.Add(new SwarmSpawnCandidate(candidateSwarmPos, candidateAttachmentSide, candidateWeight));
|
|
}
|
|
|
|
if (supportBlock.SideSolid[BlockFacing.UP.Index])
|
|
{
|
|
var floorCandidate = basePos.UpCopy();
|
|
if (IsEmptyForSwarm(floorCandidate))
|
|
floorCandidates.Add(new SwarmSpawnCandidate(floorCandidate, "down", CalculateCandidateWeight(floorCandidate, "down", supportBias)));
|
|
}
|
|
|
|
if (!supportBlock.SideSolid[BlockFacing.DOWN.Index])
|
|
continue;
|
|
|
|
var ceilingCandidate = basePos.DownCopy();
|
|
if (!IsEmptyForSwarm(ceilingCandidate))
|
|
continue;
|
|
|
|
ceilingCandidates.Add(new SwarmSpawnCandidate(ceilingCandidate, "up", CalculateCandidateWeight(ceilingCandidate, "up", supportBias)));
|
|
}
|
|
}
|
|
}
|
|
|
|
var selected = ((SelectBiasedCandidate(sideCandidatesWood)
|
|
?? SelectBiasedCandidate(sideCandidatesNonWood))
|
|
?? SelectBiasedCandidate(floorCandidates))
|
|
?? SelectBiasedCandidate(ceilingCandidates);
|
|
|
|
stopwatch.StopAndLogTime(this, 0.05);
|
|
|
|
if (selected == null)
|
|
return false;
|
|
|
|
swarmPos = selected.Value.Pos;
|
|
attachmentSide = selected.Value.AttachmentSide;
|
|
return true;
|
|
}
|
|
|
|
private static string GetAttachmentSideCode(BlockPos supportPos, BlockPos swarmPos)
|
|
{
|
|
var dx = supportPos.X - swarmPos.X;
|
|
var dy = supportPos.Y - swarmPos.Y;
|
|
var dz = supportPos.Z - swarmPos.Z;
|
|
|
|
if (dx < 0)
|
|
return "west";
|
|
if (dx > 0)
|
|
return "east";
|
|
if (dy < 0)
|
|
return "down";
|
|
if (dy > 0)
|
|
return "up";
|
|
if (dz < 0)
|
|
return "north";
|
|
return "south";
|
|
}
|
|
|
|
private bool IsEmptyForSwarm(BlockPos pos)
|
|
{
|
|
var block = Api.World.BlockAccessor.GetBlock(pos);
|
|
return block != null && (block.Id == 0 || block.Replaceable >= 6000);
|
|
}
|
|
|
|
private double GetSupportBias(Block supportBlock, BlockPos supportPos)
|
|
{
|
|
var bias = 1d;
|
|
var codePath = supportBlock.Code?.Path ?? string.Empty;
|
|
|
|
if (codePath.Contains("beehive", StringComparison.OrdinalIgnoreCase))
|
|
bias *= 0.15d;
|
|
|
|
if (codePath.Contains("log", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var leavesNearby = CountNearbyLeaves(supportPos, 4);
|
|
if (leavesNearby > 0)
|
|
bias *= 8d + leavesNearby;
|
|
}
|
|
|
|
return bias;
|
|
}
|
|
|
|
private int CountNearbyLeaves(BlockPos center, int radius)
|
|
{
|
|
int count = 0;
|
|
var radiusSq = radius * radius;
|
|
var ba = Api.World.BlockAccessor;
|
|
|
|
for (int dx = -radius; dx <= radius; dx++)
|
|
{
|
|
for (int dy = -radius; dy <= radius; dy++)
|
|
{
|
|
for (int dz = -radius; dz <= radius; dz++)
|
|
{
|
|
if ((dx * dx) + (dy * dy) + (dz * dz) > radiusSq)
|
|
continue;
|
|
|
|
var block = ba.GetBlock(center.AddCopy(dx, dy, dz));
|
|
var path = block?.Code?.Path;
|
|
if (path?.Contains("leaves", StringComparison.OrdinalIgnoreCase) == true)
|
|
count++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
private double CalculateCandidateWeight(BlockPos candidatePos, string attachmentSide, double supportBias)
|
|
{
|
|
var distance = candidatePos.DistanceTo(Pos);
|
|
var distanceWeight = 1d / (distance + 1d);
|
|
var attachmentWeight = attachmentSide switch
|
|
{
|
|
"down" => 0.8d,
|
|
"up" => 0.4d,
|
|
_ => 1d
|
|
};
|
|
|
|
return supportBias * distanceWeight * attachmentWeight;
|
|
}
|
|
|
|
private SwarmSpawnCandidate? SelectBiasedCandidate(List<SwarmSpawnCandidate> candidates)
|
|
{
|
|
if (candidates.Count == 0)
|
|
return null;
|
|
|
|
var totalWeight = 0d;
|
|
for (int i = 0; i < candidates.Count; i++)
|
|
totalWeight += Math.Max(0, candidates[i].Weight);
|
|
|
|
if (totalWeight <= 0)
|
|
return candidates[0];
|
|
|
|
var roll = Api.World.Rand.NextDouble() * totalWeight;
|
|
var running = 0d;
|
|
for (int i = 0; i < candidates.Count; i++)
|
|
{
|
|
var candidate = candidates[i];
|
|
running += Math.Max(0, candidate.Weight);
|
|
if (roll < running)
|
|
return candidate;
|
|
}
|
|
|
|
return candidates[^1];
|
|
}
|
|
|
|
private readonly record struct SwarmSpawnCandidate(BlockPos Pos, string AttachmentSide, double Weight);
|
|
|
|
private void UpdateBeePopulation(double daysElapsed, BeehiveStats stats)
|
|
{
|
|
if (BeePopulation <= 0)
|
|
{
|
|
BeePopulation = 0;
|
|
return;
|
|
}
|
|
|
|
var cfg = Config.Instance;
|
|
double change = stats.DailyNetPopulationChange * daysElapsed;
|
|
|
|
double newPopulation = BeePopulation + change;
|
|
BeePopulation = Math.Clamp(newPopulation, 0, cfg.MaxBeePopulation);
|
|
}
|
|
|
|
private BeehiveStats CalculateStats(double? totalDaysDate = null)
|
|
{
|
|
return BeehiveStats.Create(this, isInGreenhouse, totalDaysDate);
|
|
}
|
|
|
|
private void UpdateGreenhouseStatus()
|
|
{
|
|
if (roomRegistry != null)
|
|
isInGreenhouse = GetGreenhouseStatus(positionsToCheckGreenhouse, roomRegistry);
|
|
}
|
|
|
|
private static bool GetGreenhouseStatus(IEnumerable<BlockPos> positionsToCheckGreenhouse, RoomRegistry roomRegistry)
|
|
{
|
|
bool isGreenhouse = false;
|
|
foreach (var posToCheck in positionsToCheckGreenhouse)
|
|
{
|
|
var room = roomRegistry?.GetRoomForPosition(posToCheck);
|
|
isGreenhouse = room != null && (room.SkylightCount > room.NonSkylightCount && room.ExitCount == 0);
|
|
if (isGreenhouse)
|
|
break;
|
|
}
|
|
return isGreenhouse;
|
|
}
|
|
|
|
private bool UpdateHoneyProduction(double daysElapsed, BeehiveStats stats)
|
|
{
|
|
int emptyFrameSlot = GetFirstEmptyFrameSlot();
|
|
var hasNoEmptyFrames = emptyFrameSlot < 0;
|
|
if (hasNoEmptyFrames)
|
|
return false;
|
|
|
|
if (stats.FramesPerDay <= 0)
|
|
return false;
|
|
|
|
HoneyProgress += stats.FramesPerDay * daysElapsed;
|
|
|
|
var anyFilled = false;
|
|
var shouldFillFrame = HoneyProgress >= 1.0 && emptyFrameSlot >= 0;
|
|
while (shouldFillFrame)
|
|
{
|
|
FillFrame(emptyFrameSlot);
|
|
anyFilled = true;
|
|
HoneyProgress -= 1.0;
|
|
emptyFrameSlot = GetFirstEmptyFrameSlot();
|
|
shouldFillFrame = HoneyProgress >= 1.0 && emptyFrameSlot >= 0;
|
|
}
|
|
|
|
hasNoEmptyFrames = emptyFrameSlot < 0;
|
|
if (hasNoEmptyFrames)
|
|
HoneyProgress = 0;
|
|
|
|
return anyFilled;
|
|
}
|
|
|
|
private bool UpdateFeedConsumption(double daysElapsed, BeehiveStats stats)
|
|
{
|
|
if (stats.FeedConsumedPerDay <= 0 || BeePopulation <= 0)
|
|
return false;
|
|
|
|
var feedToConsume = stats.FeedConsumedPerDay * daysElapsed;
|
|
if (feedToConsume <= 0)
|
|
return false;
|
|
|
|
var anyReplaced = false;
|
|
for (int i = 0; i < inventory.Count && feedToConsume > 0; i++)
|
|
{
|
|
if (inventory[i].Empty)
|
|
continue;
|
|
|
|
var stack = inventory[i].Itemstack;
|
|
if (!IsFilledFeedFrame(stack))
|
|
continue;
|
|
|
|
var remaining = GetFeedRemaining(stack);
|
|
if (remaining <= feed_empty_threshold)
|
|
{
|
|
ReplaceWithEmptyFrame(i);
|
|
anyReplaced = true;
|
|
continue;
|
|
}
|
|
|
|
var consumed = Math.Min(remaining, feedToConsume);
|
|
remaining -= consumed;
|
|
feedToConsume -= consumed;
|
|
|
|
if (remaining <= feed_empty_threshold)
|
|
{
|
|
ReplaceWithEmptyFrame(i);
|
|
anyReplaced = true;
|
|
}
|
|
else
|
|
SetFeedRemaining(stack, remaining);
|
|
|
|
inventory[i].MarkDirty();
|
|
}
|
|
|
|
return anyReplaced;
|
|
}
|
|
|
|
private static bool IsFilledFeedFrame(ItemStack? stack)
|
|
{
|
|
return stack?.Block?.Code?.Path == "beehiveframe-filled-feed";
|
|
}
|
|
|
|
private static double GetFeedRemaining(ItemStack? stack)
|
|
{
|
|
if (stack?.Attributes == null)
|
|
return feed_full_amount;
|
|
|
|
if (!stack.Attributes.HasAttribute(feed_remaining_attribute))
|
|
return feed_full_amount;
|
|
|
|
return Math.Clamp(stack.Attributes.GetDouble(feed_remaining_attribute), 0, feed_full_amount);
|
|
}
|
|
|
|
public bool TryGetCurrentFeedStatus(out double remaining)
|
|
{
|
|
remaining = 0;
|
|
|
|
for (int i = 0; i < inventory.Count; i++)
|
|
{
|
|
if (inventory[i].Empty)
|
|
continue;
|
|
|
|
var stack = inventory[i].Itemstack;
|
|
if (!IsFilledFeedFrame(stack))
|
|
continue;
|
|
|
|
remaining = GetFeedRemaining(stack!);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static void SetFeedRemaining(ItemStack? stack, double remaining)
|
|
{
|
|
stack?.Attributes.SetDouble(feed_remaining_attribute, Math.Clamp(remaining, 0, feed_full_amount));
|
|
}
|
|
|
|
private void ReplaceWithEmptyFrame(int slotIndex)
|
|
{
|
|
var emptyFrameBlock = Api.World.GetBlock(new AssetLocation("orekiwoofsbeehives:beehiveframe-empty"));
|
|
if (emptyFrameBlock == null)
|
|
return;
|
|
|
|
inventory[slotIndex].Itemstack = new ItemStack(emptyFrameBlock, 1);
|
|
inventory[slotIndex].MarkDirty();
|
|
}
|
|
|
|
public int GetFirstEmptyFrameSlot()
|
|
{
|
|
for (int i = 0; i < inventory.Count; i++)
|
|
{
|
|
if (!inventory[i].Empty)
|
|
{
|
|
var itemStack = inventory[i].Itemstack;
|
|
if (itemStack?.Block?.Code?.Path == "beehiveframe-empty")
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
private void FillFrame(int slotIndex)
|
|
{
|
|
if (slotIndex < 0 || slotIndex >= inventory.Count)
|
|
return;
|
|
|
|
var emptyFrame = inventory[slotIndex].Itemstack;
|
|
if (emptyFrame?.Block?.Code?.Path != "beehiveframe-empty")
|
|
return;
|
|
|
|
Block? filledFrameBlock = Api.World.GetBlock(new AssetLocation("orekiwoofsbeehives:beehiveframe-filled"));
|
|
if (filledFrameBlock == null)
|
|
return;
|
|
|
|
var filledFrame = new ItemStack(filledFrameBlock, 1);
|
|
inventory[slotIndex].Itemstack = filledFrame;
|
|
inventory[slotIndex].MarkDirty();
|
|
}
|
|
|
|
public int CountFilledFrames()
|
|
{
|
|
int count = 0;
|
|
for (int i = 0; i < inventory.Count; i++)
|
|
{
|
|
if (!inventory[i].Empty)
|
|
{
|
|
var itemStack = inventory[i].Itemstack;
|
|
if (itemStack?.Block?.Code?.Path?.StartsWith("beehiveframe-filled") == true)
|
|
count++;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
public int CountEmptyFrames()
|
|
{
|
|
int count = 0;
|
|
for (int i = 0; i < inventory.Count; i++)
|
|
{
|
|
if (!inventory[i].Empty)
|
|
{
|
|
var itemStack = inventory[i].Itemstack;
|
|
if (itemStack?.Block?.Code?.Path == "beehiveframe-empty")
|
|
count++;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
public int CountTotalFrames()
|
|
{
|
|
int count = 0;
|
|
for (int i = 0; i < inventory.Count; i++)
|
|
{
|
|
if (!inventory[i].Empty)
|
|
{
|
|
var itemStack = inventory[i].Itemstack;
|
|
if (itemStack?.Block?.Code?.Path?.StartsWith("beehiveframe") == true)
|
|
count++;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
public override void ToTreeAttributes(ITreeAttribute tree)
|
|
{
|
|
base.ToTreeAttributes(tree);
|
|
tree.SetBool("isOpen", IsOpen);
|
|
tree.SetDouble("beePopulation", BeePopulation);
|
|
tree.SetInt("roamingbees_targetBeeParticleCount", GetTargetBeeParticleCount());
|
|
tree.SetInt("roamingbees_radius", Config.Instance.BeehiveRadius);
|
|
tree.SetDouble("honeyProgress", HoneyProgress);
|
|
tree.SetDouble("lastUpdateTotalHours", lastUpdateTotalHours);
|
|
tree.SetBool(nameof(wasFullyScanned), wasFullyScanned);
|
|
|
|
if (FlowersAround.HasValue)
|
|
tree.SetInt("flowersAround", FlowersAround.Value);
|
|
if (CropsAround.HasValue)
|
|
tree.SetInt("cropsAround", CropsAround.Value);
|
|
|
|
if (scanningProgress.HasValue)
|
|
tree.SetFloat(nameof(scanningProgress), scanningProgress.Value);
|
|
if (rescanningProgress.HasValue)
|
|
tree.SetFloat(nameof(rescanningProgress), rescanningProgress.Value);
|
|
|
|
if (activeSwarmPos != null)
|
|
tree.SetBlockPos("activeSwarmPos", activeSwarmPos);
|
|
|
|
if (incomingSwarmPos != null)
|
|
tree.SetBlockPos("incomingSwarmPos", incomingSwarmPos);
|
|
|
|
tree.SetDouble(nameof(NextSwarmAllowedTotalDays), NextSwarmAllowedTotalDays);
|
|
tree.SetDouble(nameof(PreSwarmProgress), PreSwarmProgress);
|
|
tree.SetBool(nameof(SwarmsDisabled), SwarmsDisabled);
|
|
}
|
|
|
|
public override void FromTreeAttributes(ITreeAttribute tree, IWorldAccessor worldAccessForResolve)
|
|
{
|
|
base.FromTreeAttributes(tree, worldAccessForResolve);
|
|
IsOpen = tree.GetBool("isOpen");
|
|
BeePopulation = tree.GetDouble("beePopulation");
|
|
HoneyProgress = tree.GetDouble("honeyProgress");
|
|
lastUpdateTotalHours = tree.GetDouble("lastUpdateTotalHours");
|
|
wasFullyScanned = tree.GetBool(nameof(wasFullyScanned));
|
|
|
|
if (tree.HasAttribute("flowersAround"))
|
|
FlowersAround = tree.GetInt("flowersAround");
|
|
if (tree.HasAttribute("cropsAround"))
|
|
CropsAround = tree.GetInt("cropsAround");
|
|
|
|
if (Api != null && Api.Side.IsClient())
|
|
{
|
|
TimeSinceLastSpawn = tree.GetFloat(nameof(TimeSinceLastSpawn));
|
|
if (tree.HasAttribute(nameof(scanningProgress)))
|
|
scanningProgress = tree.GetFloat(nameof(scanningProgress));
|
|
if (tree.HasAttribute(nameof(rescanningProgress)))
|
|
rescanningProgress = tree.GetFloat(nameof(rescanningProgress));
|
|
}
|
|
|
|
activeSwarmPos = tree.GetBlockPos("activeSwarmPos");
|
|
incomingSwarmPos = tree.GetBlockPos("incomingSwarmPos");
|
|
|
|
NextSwarmAllowedTotalDays = tree.GetDouble(nameof(NextSwarmAllowedTotalDays));
|
|
PreSwarmProgress = tree.GetDouble(nameof(PreSwarmProgress));
|
|
SwarmsDisabled = tree.GetBool(nameof(SwarmsDisabled));
|
|
}
|
|
|
|
private int GetTargetBeeParticleCount()
|
|
{
|
|
var cfg = Config.Instance;
|
|
if (cfg.BeehiveAlwaysSpawnNumberOfBees > 0)
|
|
return cfg.BeehiveAlwaysSpawnNumberOfBees;
|
|
|
|
if (cfg.BeesPerParticle <= 0)
|
|
return 0;
|
|
|
|
return (int)Math.Max(0, BeePopulation / cfg.BeesPerParticle);
|
|
}
|
|
|
|
public override void OnBlockRemoved()
|
|
{
|
|
base.OnBlockRemoved();
|
|
Api.GetPlantPositionRegistry()?.UnregisterBeehive(Pos);
|
|
Api.GetOrekiWoofsBeehives()?.BeehiveRegistry.Unregister(Pos);
|
|
}
|
|
|
|
public override void OnBlockUnloaded()
|
|
{
|
|
base.OnBlockUnloaded();
|
|
Api.GetPlantPositionRegistry()?.UnregisterBeehive(Pos);
|
|
Api.GetOrekiWoofsBeehives()?.BeehiveRegistry.Unregister(Pos);
|
|
}
|
|
|
|
private void BroadcastUnloadDebug(string message)
|
|
{
|
|
Api.GetOrekiWoofsBeehives()?.BroadcastUnloadDebug(message);
|
|
}
|
|
}
|