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

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);
}