Private
Public Access
1
0
Files
OrekiWoofsBeehives/OrekiWoofsBeehives/BlockEntities/BlockEntityReusableBeehive.cs

1063 lines
35 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
{
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 StringBuilder infoStringBuilder = new();
private readonly InventoryGeneric inventory;
private bool isOpen;
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;
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 BlockEntityReusableBeehive()
{
inventory = new InventoryGeneric(8, null, null);
}
public bool IsOpen
{
get => isOpen;
set
{
if (isOpen == value)
return;
isOpen = value;
if (Api?.World != null)
Api.World.BlockAccessor.MarkBlockDirty(Pos);
}
}
public override void Initialize(ICoreAPI api)
{
base.Initialize(api);
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);
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(true);
}
}
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(true);
}
}
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)
{
var stopwatch = Stopwatch.StartNew();
UpdateGreenhouseStatus();
UpdateStatsText();
if (Api.Side == EnumAppSide.Client)
return;
UpdateFlowersAndCrops();
UpdateBeePopulationAndHoney();
stopwatch.Stop();
if (stopwatch.Elapsed.TotalSeconds > 0.2)
{
var modSystem = Api.GetOrekiWoofsBeehives();
modSystem?.Mod?.Logger.Warning($"{nameof(BlockEntityReusableBeehive)} {nameof(OnGameTick)} took {stopwatch.Elapsed.TotalSeconds:F2}s");
}
}
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)
BroadcastUnloadDebug($"hive {Pos}: catch-up start elapsed={hoursElapsed:F2}h population={startingPopulation:F0} honeyProgress={startingHoneyProgress:F3}");
var framesChangedVisually = false;
while (lastUpdateTotalHours < currentTotalHours)
{
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 (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)
{
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);
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);
}
}