664 lines
24 KiB
C#
664 lines
24 KiB
C#
using OrekiWoofsBeehives.Utilities;
|
|
using OrekiWoofsBees.Common;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Text;
|
|
using Vintagestory.API.Common;
|
|
using Vintagestory.API.Datastructures;
|
|
using Vintagestory.API.MathTools;
|
|
using Vintagestory.API.Config;
|
|
|
|
namespace OrekiWoofsBeehives.BlockEntities;
|
|
|
|
public class BlockEntityBeeSwarm : BlockEntity
|
|
{
|
|
public double MigrationPhaseDurationHours { get; protected set; } = 5.0;
|
|
|
|
public BlockPos OriginHivePos { get; private set; } = new BlockPos(0);
|
|
public double Population { get; private set; }
|
|
public double SpawnDateTime { get; private set; }
|
|
public SwarmState SwarmState { get; private set; } = SwarmState.BuildingSwarm;
|
|
|
|
private double plannedPopulation;
|
|
private double stateStartTotalHours;
|
|
private double buildingDurationHours = 3.0;
|
|
private double hangingDurationHours = 5.0;
|
|
private double migrationStartPopulation;
|
|
private double transferredDuringMigration;
|
|
private int retryDay = -1;
|
|
private BlockPos? targetPos;
|
|
private bool targetIsVanillaSkep;
|
|
private string? targetPopulatedSkepCode;
|
|
private int? eligibleTargetsAtLastSelection;
|
|
private List<SwarmTargetCandidate>? targetHiveCandidates;
|
|
private long? candidateRefreshListenerId;
|
|
|
|
public override void Initialize(ICoreAPI api)
|
|
{
|
|
base.Initialize(api);
|
|
#if DEBUG
|
|
RegisterGameTickListener(OnGameTick, 500);
|
|
#else
|
|
RegisterGameTickListener(OnGameTick, 5_000);
|
|
#endif
|
|
|
|
if (api.Side != EnumAppSide.Server)
|
|
return;
|
|
|
|
if (SwarmState is SwarmState.BuildingSwarm or SwarmState.HangingOut)
|
|
StartCandidateRefresh();
|
|
|
|
FastForwardTo(api.World.Calendar.TotalHours, debugAsCatchUp: true);
|
|
}
|
|
|
|
public void InitializeFromOrigin(
|
|
BlockPos originHivePos,
|
|
double plannedPopulation,
|
|
double spawnTotalHours,
|
|
double buildingDurationHours,
|
|
double hangingDurationHours)
|
|
{
|
|
OriginHivePos = originHivePos.Copy();
|
|
this.plannedPopulation = Math.Max(0, plannedPopulation);
|
|
SpawnDateTime = spawnTotalHours;
|
|
stateStartTotalHours = spawnTotalHours;
|
|
this.buildingDurationHours = Math.Max(0.1, buildingDurationHours);
|
|
this.hangingDurationHours = Math.Max(0.1, hangingDurationHours);
|
|
SwarmState = SwarmState.BuildingSwarm;
|
|
Population = 0;
|
|
retryDay = -1;
|
|
targetPos = null;
|
|
targetIsVanillaSkep = false;
|
|
targetPopulatedSkepCode = null;
|
|
targetHiveCandidates = null;
|
|
migrationStartPopulation = 0;
|
|
transferredDuringMigration = 0;
|
|
MarkDirty(true);
|
|
}
|
|
|
|
private void OnGameTick(float dt)
|
|
{
|
|
if (Api.Side != EnumAppSide.Server)
|
|
return;
|
|
|
|
FastForwardTo(Api.World.Calendar.TotalHours, debugAsCatchUp: false);
|
|
}
|
|
|
|
private void OnCandidateRefreshTick(float dt)
|
|
{
|
|
if (Api.Side != EnumAppSide.Server)
|
|
return;
|
|
|
|
targetHiveCandidates = FindEligibleTargets();
|
|
eligibleTargetsAtLastSelection = targetHiveCandidates.Count;
|
|
MarkDirty(false);
|
|
}
|
|
|
|
private void StartCandidateRefresh()
|
|
{
|
|
StopCandidateRefresh();
|
|
candidateRefreshListenerId = RegisterGameTickListener(OnCandidateRefreshTick, 5_000);
|
|
}
|
|
|
|
private void StopCandidateRefresh()
|
|
{
|
|
if (candidateRefreshListenerId.HasValue)
|
|
{
|
|
UnregisterGameTickListener(candidateRefreshListenerId.Value);
|
|
candidateRefreshListenerId = null;
|
|
}
|
|
}
|
|
|
|
public void FastForwardTo(double nowHours)
|
|
{
|
|
FastForwardTo(nowHours, debugAsCatchUp: false);
|
|
}
|
|
|
|
public void FastForwardTo(double nowHours, bool debugAsCatchUp)
|
|
{
|
|
if (Api?.Side != EnumAppSide.Server)
|
|
return;
|
|
|
|
if (nowHours <= SpawnDateTime)
|
|
return;
|
|
|
|
var debugEnabled = debugAsCatchUp && Api.GetOrekiWoofsBeehives()?.DebugUnloadEnabled == true;
|
|
var initialState = SwarmState;
|
|
var initialPopulation = Population;
|
|
var initialTargetPos = targetPos?.Copy();
|
|
var initialTransferred = transferredDuringMigration;
|
|
|
|
if (debugEnabled)
|
|
BroadcastUnloadDebug($"swarm {Pos}: catch-up start from state={initialState}, population={initialPopulation:F2}, spawnHour={SpawnDateTime:F2}, targetHour={nowHours:F2}");
|
|
|
|
for (int i = 0; i < 12; i++)
|
|
{
|
|
if (Api.World.BlockAccessor.GetBlockEntity(Pos) is not BlockEntityBeeSwarm)
|
|
{
|
|
if (debugEnabled)
|
|
BroadcastUnloadDebug($"swarm {Pos}: removed during catch-up");
|
|
|
|
return;
|
|
}
|
|
|
|
var previousState = SwarmState;
|
|
var previousPopulation = Population;
|
|
var previousStateStartTotalHours = stateStartTotalHours;
|
|
var previousRetryDay = retryDay;
|
|
var previousTransferredDuringMigration = transferredDuringMigration;
|
|
var previousTargetPos = targetPos?.Copy();
|
|
|
|
switch (SwarmState)
|
|
{
|
|
case SwarmState.BuildingSwarm:
|
|
UpdateBuildingSwarm(nowHours);
|
|
break;
|
|
case SwarmState.HangingOut:
|
|
UpdateHangingOut(nowHours);
|
|
break;
|
|
case SwarmState.MigratingToNewHive:
|
|
UpdateMigratingToNewHive(nowHours);
|
|
break;
|
|
}
|
|
|
|
if (Api.World.BlockAccessor.GetBlockEntity(Pos) is not BlockEntityBeeSwarm)
|
|
{
|
|
if (debugEnabled)
|
|
BroadcastUnloadDebug($"swarm {Pos}: finished and removed during catch-up");
|
|
|
|
return;
|
|
}
|
|
|
|
var changed =
|
|
previousState != SwarmState ||
|
|
Math.Abs(previousPopulation - Population) > 0.0001 ||
|
|
previousStateStartTotalHours != stateStartTotalHours ||
|
|
previousRetryDay != retryDay ||
|
|
Math.Abs(previousTransferredDuringMigration - transferredDuringMigration) > 0.0001 ||
|
|
!Equals(previousTargetPos, targetPos);
|
|
|
|
if (debugEnabled)
|
|
{
|
|
if (previousState != SwarmState)
|
|
BroadcastUnloadDebug($"swarm {Pos}: state {previousState} -> {SwarmState} at hour={nowHours:F2}");
|
|
|
|
var movedNow = transferredDuringMigration - previousTransferredDuringMigration;
|
|
if (movedNow > 0.0001)
|
|
BroadcastUnloadDebug($"swarm {Pos}: moved {movedNow:F2} bees this catch-up step (remaining={Population:F2})");
|
|
}
|
|
|
|
if (!changed)
|
|
{
|
|
if (debugEnabled)
|
|
BroadcastUnloadDebug($"swarm {Pos}: catch-up reached stable state={SwarmState}, population={Population:F2}, transferred={transferredDuringMigration:F2}");
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (debugEnabled)
|
|
{
|
|
BroadcastUnloadDebug(
|
|
$"swarm {Pos}: catch-up loop limit reached; state {initialState}->{SwarmState}, population {initialPopulation:F2}->{Population:F2}, transferred {initialTransferred:F2}->{transferredDuringMigration:F2}, target {initialTargetPos}->{targetPos}");
|
|
}
|
|
}
|
|
|
|
private void UpdateBuildingSwarm(double nowHours)
|
|
{
|
|
var elapsedHours = Math.Max(0, nowHours - stateStartTotalHours);
|
|
var progress = Math.Clamp(elapsedHours / buildingDurationHours, 0, 1);
|
|
var desiredPopulationInSwarm = plannedPopulation * progress;
|
|
var toMoveNow = desiredPopulationInSwarm - Population;
|
|
|
|
if (toMoveNow > 0)
|
|
{
|
|
var sourceHive = GetOriginHive();
|
|
if (sourceHive != null)
|
|
{
|
|
var moved = sourceHive.TakeBeePopulationForSwarm(toMoveNow);
|
|
Population += moved;
|
|
MarkDirty(false);
|
|
}
|
|
}
|
|
|
|
if (progress < 1)
|
|
return;
|
|
|
|
SwarmState = SwarmState.HangingOut;
|
|
stateStartTotalHours = nowHours;
|
|
StartCandidateRefresh();
|
|
MarkDirty(false);
|
|
}
|
|
|
|
private void UpdateHangingOut(double nowHours)
|
|
{
|
|
if (retryDay >= 0)
|
|
{
|
|
if (CanRetryInCurrentWindow(nowHours))
|
|
TryStartMigration(nowHours);
|
|
return;
|
|
}
|
|
|
|
var elapsedHours = Math.Max(0, nowHours - stateStartTotalHours);
|
|
if (elapsedHours < hangingDurationHours)
|
|
return;
|
|
|
|
TryStartMigration(nowHours);
|
|
}
|
|
|
|
private bool CanRetryInCurrentWindow(double nowHours)
|
|
{
|
|
var currentDay = GetCurrentDayIndex(nowHours);
|
|
if (currentDay < retryDay)
|
|
return false;
|
|
|
|
var hourOfDay = Api.World.Calendar.HourOfDay;
|
|
return hourOfDay is >= 8 and <= 12;
|
|
}
|
|
|
|
private void TryStartMigration(double nowHours)
|
|
{
|
|
targetHiveCandidates = FindEligibleTargets();
|
|
eligibleTargetsAtLastSelection = targetHiveCandidates.Count;
|
|
|
|
if (targetHiveCandidates.Count == 0)
|
|
{
|
|
var currentDay = GetCurrentDayIndex(nowHours);
|
|
if (retryDay < 0)
|
|
{
|
|
retryDay = currentDay + 1;
|
|
MarkDirty(false);
|
|
return;
|
|
}
|
|
|
|
Disperse();
|
|
return;
|
|
}
|
|
|
|
foreach (var candidate in targetHiveCandidates)
|
|
{
|
|
if (candidate.IsVanillaSkep)
|
|
{
|
|
if (TryConvertSkepToPopulated(candidate.Pos, candidate.PopulatedSkepCode!))
|
|
{
|
|
StartMigrating(nowHours, candidate.Pos, true, candidate.PopulatedSkepCode);
|
|
return;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
StartMigrating(nowHours, candidate.Pos, false, null);
|
|
return;
|
|
}
|
|
|
|
Disperse();
|
|
}
|
|
|
|
private void StartMigrating(double nowHours, BlockPos targetPos, bool targetIsVanillaSkep, string? targetPopulatedSkepCode)
|
|
{
|
|
StopCandidateRefresh();
|
|
SwarmState = SwarmState.MigratingToNewHive;
|
|
stateStartTotalHours = nowHours;
|
|
migrationStartPopulation = Population;
|
|
transferredDuringMigration = 0;
|
|
this.targetPos = targetPos.Copy();
|
|
this.targetIsVanillaSkep = targetIsVanillaSkep;
|
|
this.targetPopulatedSkepCode = targetPopulatedSkepCode;
|
|
retryDay = -1;
|
|
|
|
if (!targetIsVanillaSkep && Api.World.BlockAccessor.GetBlockEntity(targetPos) is BlockEntityReusableBeehive targetHive)
|
|
targetHive.SetIncomingSwarm(Pos);
|
|
|
|
MarkDirty(false);
|
|
}
|
|
|
|
private void UpdateMigratingToNewHive(double nowHours)
|
|
{
|
|
if (targetPos == null)
|
|
{
|
|
Disperse();
|
|
return;
|
|
}
|
|
|
|
var elapsedHours = Math.Max(0, nowHours - stateStartTotalHours);
|
|
var progress = Math.Clamp(elapsedHours / MigrationPhaseDurationHours, 0, 1);
|
|
var desiredTransferred = migrationStartPopulation * progress;
|
|
var toTransferNow = desiredTransferred - transferredDuringMigration;
|
|
|
|
if (toTransferNow > 0)
|
|
{
|
|
var moved = targetIsVanillaSkep
|
|
? Math.Min(Population, Math.Max(0, toTransferNow))
|
|
: MoveToTargetHive(toTransferNow);
|
|
|
|
transferredDuringMigration += moved;
|
|
Population = Math.Max(0, Population - moved);
|
|
MarkDirty(false);
|
|
}
|
|
|
|
if (progress < 1)
|
|
return;
|
|
|
|
RemoveSwarmBlock();
|
|
}
|
|
|
|
private double MoveToTargetHive(double amount)
|
|
{
|
|
if (targetPos == null)
|
|
return 0;
|
|
|
|
if (Api.World.BlockAccessor.GetBlockEntity(targetPos) is not BlockEntityReusableBeehive targetHive)
|
|
return 0;
|
|
|
|
return targetHive.AddBeePopulationFromSwarm(amount);
|
|
}
|
|
|
|
private readonly List<SwarmTargetCandidate> _findEligibleTargetsList = [];
|
|
private readonly BlockPos _findEligibleTargetsBlockPos = new(0);
|
|
private List<SwarmTargetCandidate> FindEligibleTargets()
|
|
{
|
|
var candidates = _findEligibleTargetsList;
|
|
candidates.Clear();
|
|
var cfg = Config.Instance;
|
|
var radius = Math.Max(1, cfg.BeehiveRadius);
|
|
|
|
var modSystem = Api.GetOrekiWoofsBeehives();
|
|
if (modSystem != null)
|
|
{
|
|
foreach (var pos in modSystem.BeehiveRegistry.BeehivePositions)
|
|
{
|
|
var targetPos = _findEligibleTargetsBlockPos.Set(pos.X, pos.Y, pos.Z);
|
|
if (targetPos.Equals(OriginHivePos) || targetPos.Equals(Pos))
|
|
continue;
|
|
|
|
if (!Overlaps.IsWithinSphericalRadius(Pos, pos, radius))
|
|
continue;
|
|
|
|
if (Api.World.BlockAccessor.GetBlockEntity(targetPos) is not BlockEntityReusableBeehive hive)
|
|
continue;
|
|
|
|
if (!hive.OpenForIncomingSwarms)
|
|
continue;
|
|
|
|
var score = CalculateBeehiveTargetScore(hive, targetPos, radius);
|
|
candidates.Add(new SwarmTargetCandidate(targetPos, false, score, null));
|
|
}
|
|
}
|
|
|
|
var skepRegistry = modSystem?.VanillaSkepRegistry;
|
|
if (skepRegistry != null)
|
|
{
|
|
foreach (var entry in skepRegistry.Entries)
|
|
{
|
|
var targetPos = new BlockPos(entry.Key.X, entry.Key.Y, entry.Key.Z);
|
|
if (!Overlaps.IsWithinSphericalRadius(Pos, entry.Key, radius))
|
|
continue;
|
|
|
|
if (targetPos.Equals(OriginHivePos) || targetPos.Equals(Pos))
|
|
continue;
|
|
|
|
var block = Api.World.BlockAccessor.GetBlock(targetPos);
|
|
if (block?.Code?.Path?.Contains("empty") != true)
|
|
continue;
|
|
|
|
var score = CalculateSkepTargetScore(targetPos, radius);
|
|
candidates.Add(new SwarmTargetCandidate(targetPos, true, score, entry.Value));
|
|
}
|
|
}
|
|
|
|
candidates.Sort((a, b) => b.Score.CompareTo(a.Score));
|
|
return candidates;
|
|
}
|
|
|
|
private bool TryConvertSkepToPopulated(BlockPos pos, string populatedCode)
|
|
{
|
|
var block = Api.World.GetBlock(new AssetLocation(populatedCode));
|
|
if (block == null || block.Id == 0)
|
|
return false;
|
|
|
|
Api.World.BlockAccessor.ExchangeBlock(block.BlockId, pos);
|
|
return true;
|
|
}
|
|
|
|
private double CalculateBeehiveTargetScore(BlockEntityReusableBeehive hive, BlockPos targetPos, int radius)
|
|
{
|
|
var distanceFactor = GetDistanceFactor(targetPos, radius);
|
|
var flowerFactor = GetBeehiveFlowerFactor(hive);
|
|
var frameFactor = hive.CountFilledFrames() > 0 ? 2.0 : 1.0;
|
|
return distanceFactor * flowerFactor * frameFactor + 10;
|
|
}
|
|
|
|
private static double GetBeehiveFlowerFactor(BlockEntityReusableBeehive hive)
|
|
{
|
|
var cfg = Config.Instance;
|
|
var maxFlowers = Math.Max(1, cfg.MaxFlowersForHoneyProduction);
|
|
var flowers = hive.FlowersAround.GetValueOrDefault(0);
|
|
var crops = hive.CropsAround.GetValueOrDefault(0);
|
|
var effectiveFlowers = Math.Min(maxFlowers, flowers + crops * 0.25);
|
|
var normalized = effectiveFlowers / maxFlowers;
|
|
return Math.Max(0.05, normalized);
|
|
}
|
|
|
|
private double CalculateSkepTargetScore(BlockPos targetPos, int radius)
|
|
{
|
|
var distanceFactor = GetDistanceFactor(targetPos, radius);
|
|
var flowerFactor = GetSkepFlowerFactor(targetPos);
|
|
return distanceFactor * flowerFactor;
|
|
}
|
|
|
|
private double GetSkepFlowerFactor(BlockPos targetPos)
|
|
{
|
|
var registry = Api.GetPlantPositionRegistry();
|
|
if (registry == null)
|
|
return 0.05;
|
|
|
|
var (flowers, crops, _, _) = registry.GetPlantCountsNearPosition(targetPos, Config.Instance.BeehiveRadius);
|
|
var maxFlowers = Math.Max(1, Config.Instance.MaxFlowersForHoneyProduction);
|
|
var effectiveFlowers = Math.Min(maxFlowers, flowers + crops * 0.25);
|
|
var normalized = effectiveFlowers / maxFlowers;
|
|
return Math.Max(0.05, normalized);
|
|
}
|
|
|
|
private double GetDistanceFactor(BlockPos targetPos, int radius)
|
|
{
|
|
var distance = targetPos.DistanceTo(Pos);
|
|
var normalized = Math.Clamp(1.0 - (distance / radius), 0.01, 1.0);
|
|
return normalized;
|
|
}
|
|
|
|
public override void GetBlockInfo(IPlayer forPlayer, StringBuilder dsc)
|
|
{
|
|
dsc.AppendLine(GetPhaseStatusLine());
|
|
dsc.AppendLine(Lang.Get("orekiwoofsbeehives:beeswarm-info-population", Population.ToString("N0")));
|
|
|
|
if (SwarmState is SwarmState.MigratingToNewHive)
|
|
return;
|
|
|
|
var eligibleTargets = targetHiveCandidates?.Count ?? eligibleTargetsAtLastSelection;
|
|
|
|
if (!eligibleTargets.HasValue)
|
|
return;
|
|
dsc.AppendLine(Lang.Get("orekiwoofsbeehives:beeswarm-info-eligible-targets", eligibleTargets.Value));
|
|
}
|
|
|
|
private string GetPhaseStatusLine()
|
|
{
|
|
var hoursLeft = GetHoursLeftInCurrentPhase();
|
|
var hoursLeftText = hoursLeft < 1
|
|
? Lang.Get("orekiwoofsbeehives:beeswarm-less-than-hour-left")
|
|
: Lang.Get("orekiwoofsbeehives:beeswarm-hours-left", Math.Ceiling(hoursLeft).ToString("F0"));
|
|
|
|
return SwarmState switch
|
|
{
|
|
SwarmState.BuildingSwarm => Lang.Get("orekiwoofsbeehives:beeswarm-phase-forming", hoursLeftText),
|
|
SwarmState.HangingOut => Lang.Get("orekiwoofsbeehives:beeswarm-phase-scouting", hoursLeftText),
|
|
SwarmState.MigratingToNewHive => Lang.Get("orekiwoofsbeehives:beeswarm-phase-moving", hoursLeftText),
|
|
_ => Lang.Get("orekiwoofsbeehives:beeswarm-phase-scouting", hoursLeftText),
|
|
};
|
|
}
|
|
|
|
private double GetHoursLeftInCurrentPhase()
|
|
{
|
|
var now = Api.World.Calendar.TotalHours;
|
|
|
|
if (SwarmState == SwarmState.BuildingSwarm)
|
|
return Math.Max(0, buildingDurationHours - (now - stateStartTotalHours));
|
|
|
|
if (SwarmState == SwarmState.HangingOut)
|
|
{
|
|
if (retryDay >= 0)
|
|
{
|
|
var nextPickHour = retryDay * Api.World.Calendar.HoursPerDay + 8;
|
|
return Math.Max(0, nextPickHour - now);
|
|
}
|
|
|
|
return Math.Max(0, hangingDurationHours - (now - stateStartTotalHours));
|
|
}
|
|
|
|
if (SwarmState == SwarmState.MigratingToNewHive)
|
|
return Math.Max(0, MigrationPhaseDurationHours - (now - stateStartTotalHours));
|
|
|
|
return 0;
|
|
}
|
|
|
|
private int GetCurrentDayIndex(double nowHours)
|
|
{
|
|
var hpd = Math.Max(1, Api.World.Calendar.HoursPerDay);
|
|
return (int)Math.Floor(nowHours / hpd);
|
|
}
|
|
|
|
private BlockEntityReusableBeehive? GetOriginHive()
|
|
{
|
|
return Api.World.BlockAccessor.GetBlockEntity(OriginHivePos) as BlockEntityReusableBeehive;
|
|
}
|
|
|
|
private void Disperse()
|
|
{
|
|
StopCandidateRefresh();
|
|
var originHive = GetOriginHive();
|
|
if (originHive != null && Population > 0)
|
|
{
|
|
var returnPercent = Math.Clamp(Config.Instance.SwarmReturnToOriginOnFailedMigrationPercent, 0, 100);
|
|
if (returnPercent > 0)
|
|
{
|
|
var returnAmount = Population * returnPercent / 100.0;
|
|
originHive.AddBeePopulationFromSwarm(returnAmount);
|
|
}
|
|
}
|
|
|
|
RemoveSwarmBlock();
|
|
}
|
|
|
|
private void RemoveSwarmBlock()
|
|
{
|
|
NotifyTargetHiveSwarmFinished();
|
|
NotifyOriginHiveSwarmFinished();
|
|
Api.World.BlockAccessor.SetBlock(0, Pos);
|
|
}
|
|
|
|
private void NotifyOriginHiveSwarmFinished()
|
|
{
|
|
var originHive = GetOriginHive();
|
|
originHive?.ClearActiveSwarm(Pos);
|
|
}
|
|
|
|
private void NotifyTargetHiveSwarmFinished()
|
|
{
|
|
if (targetIsVanillaSkep || targetPos == null)
|
|
return;
|
|
|
|
if (Api.World.BlockAccessor.GetBlockEntity(targetPos) is BlockEntityReusableBeehive targetHive)
|
|
targetHive.ClearIncomingSwarm(Pos);
|
|
}
|
|
|
|
public override void OnBlockRemoved()
|
|
{
|
|
NotifyTargetHiveSwarmFinished();
|
|
NotifyOriginHiveSwarmFinished();
|
|
base.OnBlockRemoved();
|
|
}
|
|
|
|
public override void ToTreeAttributes(ITreeAttribute tree)
|
|
{
|
|
base.ToTreeAttributes(tree);
|
|
|
|
tree.SetBlockPos("originPos", OriginHivePos);
|
|
tree.SetDouble("population", Population);
|
|
tree.SetDouble("plannedPopulation", plannedPopulation);
|
|
tree.SetDouble("spawnDateTime", SpawnDateTime);
|
|
tree.SetDouble("stateStartTotalHours", stateStartTotalHours);
|
|
tree.SetDouble(nameof(MigrationPhaseDurationHours), MigrationPhaseDurationHours);
|
|
tree.SetDouble("buildingDurationHours", buildingDurationHours);
|
|
tree.SetDouble("hangingDurationHours", hangingDurationHours);
|
|
tree.SetInt("swarmState", (int)SwarmState);
|
|
tree.SetDouble("migrationStartPopulation", migrationStartPopulation);
|
|
tree.SetDouble("transferredDuringMigration", transferredDuringMigration);
|
|
tree.SetInt("retryDay", retryDay);
|
|
tree.SetBool("targetIsVanillaSkep", targetIsVanillaSkep);
|
|
tree.SetString("targetPopulatedSkepCode", targetPopulatedSkepCode ?? string.Empty);
|
|
if (eligibleTargetsAtLastSelection.HasValue)
|
|
tree.SetInt("eligibleTargetsAtLastSelection", eligibleTargetsAtLastSelection.Value);
|
|
|
|
// for roamingbees
|
|
tree.SetInt("roamingbees_swarm_state", (int)SwarmState);
|
|
tree.SetBlockPos("roamingbees_swarm_originHivePos", OriginHivePos);
|
|
if (targetPos is not null)
|
|
tree.SetBlockPos("roamingbees_swarm_targetHivePos", targetPos);
|
|
|
|
tree["roamingbees_swarm_candidateHives"] = BuildCandidateHivesTreeArray();
|
|
|
|
if (targetPos != null)
|
|
tree.SetBlockPos("targetPos", targetPos);
|
|
}
|
|
|
|
private TreeArrayAttribute BuildCandidateHivesTreeArray()
|
|
{
|
|
if (targetHiveCandidates == null)
|
|
return new TreeArrayAttribute([]);
|
|
|
|
var count = Math.Min(targetHiveCandidates.Count, 10);
|
|
var entries = new TreeAttribute[count];
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
var entry = new TreeAttribute();
|
|
entry.SetBlockPos("pos", targetHiveCandidates[i].Pos);
|
|
entries[i] = entry;
|
|
}
|
|
return new TreeArrayAttribute(entries);
|
|
}
|
|
|
|
public override void FromTreeAttributes(ITreeAttribute tree, IWorldAccessor worldAccessForResolve)
|
|
{
|
|
base.FromTreeAttributes(tree, worldAccessForResolve);
|
|
|
|
OriginHivePos = tree.GetBlockPos("originPos");
|
|
Population = tree.GetDouble("population");
|
|
plannedPopulation = tree.GetDouble("plannedPopulation");
|
|
SpawnDateTime = tree.GetDouble("spawnDateTime");
|
|
stateStartTotalHours = tree.GetDouble("stateStartTotalHours");
|
|
if (tree.HasAttribute(nameof(MigrationPhaseDurationHours)))
|
|
MigrationPhaseDurationHours = tree.GetDouble(nameof(MigrationPhaseDurationHours));
|
|
buildingDurationHours = tree.GetDouble("buildingDurationHours");
|
|
hangingDurationHours = tree.GetDouble("hangingDurationHours");
|
|
SwarmState = (SwarmState)tree.GetInt("swarmState");
|
|
migrationStartPopulation = tree.GetDouble("migrationStartPopulation");
|
|
transferredDuringMigration = tree.GetDouble("transferredDuringMigration");
|
|
retryDay = tree.GetInt("retryDay");
|
|
targetIsVanillaSkep = tree.GetBool("targetIsVanillaSkep");
|
|
targetPopulatedSkepCode = tree.GetString("targetPopulatedSkepCode");
|
|
eligibleTargetsAtLastSelection = tree.HasAttribute("eligibleTargetsAtLastSelection")
|
|
? tree.GetInt("eligibleTargetsAtLastSelection")
|
|
: null;
|
|
|
|
targetPos = tree.GetBlockPos("targetPos");
|
|
}
|
|
|
|
private void BroadcastUnloadDebug(string message)
|
|
{
|
|
Api.GetOrekiWoofsBeehives()?.BroadcastUnloadDebug(message);
|
|
}
|
|
|
|
private readonly record struct SwarmTargetCandidate(BlockPos Pos, bool IsVanillaSkep, double Score, string? PopulatedSkepCode);
|
|
}
|