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 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(); 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; 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.MaxTemperatureGrowth) 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(); var sideCandidatesNonWood = new List(); var floorCandidates = new List(); var ceilingCandidates = new List(); 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 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 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); } }