551 lines
21 KiB
C#
551 lines
21 KiB
C#
using OrekiWoofsBees.Common;
|
|
using RoamingBees.Particles;
|
|
using RoamingBees.Particles.Catchup;
|
|
using RoamingBees.Utilities;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Numerics;
|
|
using Vintagestory.API.Client;
|
|
using Vintagestory.API.Common;
|
|
using Vintagestory.API.Datastructures;
|
|
using Vintagestory.API.MathTools;
|
|
using Vintagestory.GameContent;
|
|
|
|
namespace RoamingBees.Behaviors;
|
|
|
|
public class BlockEntityBehaviorRoamingBees(BlockEntity blockEntity) : BlockEntityBehavior(blockEntity), IBeeSpawnHandler, IBeeSpawnCatchup, IModEntity
|
|
{
|
|
private const float spawn_cooldown_seconds = 1.5f;
|
|
private const string flower_count_attribute = "roamingbees_flowerCount";
|
|
private const string crop_count_attribute = "roamingbees_cropCount";
|
|
private const string initial_scan_progress_attribute = "roamingbees_initialScanProgress";
|
|
private const string rescan_progress_attribute = "roamingbees_rescanProgress";
|
|
|
|
private static readonly Random random = new();
|
|
|
|
private readonly List<InternalBeeParticle> activeBees = [];
|
|
private readonly TreeAttribute entityAttributeSnapshot = new();
|
|
|
|
private string facingVariantKey = "side";
|
|
private Dictionary<string, Vector3>? entrancePositions;
|
|
private Dictionary<string, Vector3>? frontDirections;
|
|
private RoamingBeesModSystem? modSystem;
|
|
private IPlantPositionRegistry? plantPositionRegistry;
|
|
private float rainfall;
|
|
private float temperature;
|
|
private bool initialized;
|
|
|
|
private ClimateCondition? climate;
|
|
private Vec3d? windVec;
|
|
private int? registeredRadius;
|
|
private int onParticleTickStopwatchTriggerCount;
|
|
private int spawnStopwatchTriggerCount;
|
|
private readonly Stopwatch plantInfoStopwatch = new();
|
|
private readonly Stopwatch particleStopwatch = new();
|
|
private readonly Stopwatch serverSpawnStopwatch = new();
|
|
private readonly Stopwatch handleSpawnStopwatch = new();
|
|
private readonly List<StructVec3i> flowerPositions = [];
|
|
private readonly List<StructVec3i> cropPositions = [];
|
|
private readonly List<Vector3> relativePlantPositions = [];
|
|
|
|
public const string TARGET_BEE_PARTICLE_COUNT_ATTRIBUTE = "roamingbees_targetBeeParticleCount";
|
|
public const string RADIUS_ATTRIBUTE = "roamingbees_radius";
|
|
public const string NETWORK_CHANNEL_NAME = "bee particle spawns";
|
|
|
|
public int ActiveBeesCount => activeBees.Count;
|
|
public IEnumerable<BeeSpawnPacket> ActiveBeesPackets => activeBees.Select(x => x.SpawnPacket);
|
|
public float TimeSinceLastSpawn { get; private set; } = (float)(random.NextDouble() * spawn_cooldown_seconds);
|
|
public int TargetParticleCount { get; private set; }
|
|
public int ScanRadius => GetRadiusFromBlockEntity();
|
|
public int FlowerCount { get; private set; }
|
|
public int CropCount { get; private set; }
|
|
public float InitialScanProgress { get; private set; }
|
|
public float RescanProgress { get; private set; }
|
|
|
|
public Mod? Mod { get; private set; }
|
|
|
|
public override void Initialize(ICoreAPI api, JsonObject properties)
|
|
{
|
|
if (initialized)
|
|
return;
|
|
|
|
base.Initialize(api, properties);
|
|
initialized = true;
|
|
|
|
facingVariantKey = properties["facingVariantKey"].AsString("side");
|
|
entrancePositions = VectorParsing.ParseVector3Map(properties["entrancePositions"]);
|
|
frontDirections = VectorParsing.ParseVector3Map(properties["frontDirections"]);
|
|
|
|
modSystem = api.ModLoader.GetModSystem<RoamingBeesModSystem>();
|
|
Mod = modSystem?.Mod;
|
|
modSystem?.BeeSpawnPacketDistributor?.Register(Blockentity.Pos, this);
|
|
modSystem?.ClientChannel?.SendPacket(new BeeCatchupRequestPacket { HivePosition = Pos });
|
|
if (modSystem?.Mod.Info.Version.Contains("dev") == true && modSystem?.ClientChannel != null)
|
|
modSystem?.Mod.Logger.Event($"{nameof(BlockEntityBehaviorRoamingBees)} sent {nameof(BeeCatchupRequestPacket)} Pos: {Pos}");
|
|
|
|
plantPositionRegistry = api.GetPlantPositionRegistry();
|
|
var radius = GetRadiusFromBlockEntity();
|
|
plantPositionRegistry?.RegisterBeehive(Blockentity.Pos, radius);
|
|
registeredRadius = radius;
|
|
|
|
var updateFrequency = 20;
|
|
Blockentity.RegisterGameTickListener(OnParticleTick, updateFrequency);
|
|
Blockentity.RegisterGameTickListener(UpdateClimateInfo, 10000);
|
|
Blockentity.RegisterGameTickListener(DecreaseStopwatchCounts, 60000);
|
|
if (api.Side.IsServer())
|
|
{
|
|
Blockentity.RegisterGameTickListener(OnSpawnTick, 500);
|
|
Blockentity.RegisterGameTickListener(OnUpdatePlantInfoTick, 5000);
|
|
}
|
|
}
|
|
|
|
private void DecreaseStopwatchCounts(float dt)
|
|
{
|
|
if (spawnStopwatchTriggerCount > 10)
|
|
modSystem?.Mod.Logger.Warning($"{nameof(spawnStopwatchTriggerCount)} is {spawnStopwatchTriggerCount}");
|
|
spawnStopwatchTriggerCount = Math.Max(0, spawnStopwatchTriggerCount - 1);
|
|
|
|
if (onParticleTickStopwatchTriggerCount > 10)
|
|
modSystem?.Mod.Logger.Warning($"{nameof(onParticleTickStopwatchTriggerCount)} is {onParticleTickStopwatchTriggerCount}");
|
|
onParticleTickStopwatchTriggerCount = Math.Max(0, onParticleTickStopwatchTriggerCount - 1);
|
|
}
|
|
|
|
private void UpdateClimateInfo(float dt)
|
|
{
|
|
windVec = Api.World.BlockAccessor.GetWindSpeedAt(Blockentity.Pos);
|
|
climate = Api.World.BlockAccessor.GetClimateAt(Blockentity.Pos, EnumGetClimateMode.NowValues);
|
|
}
|
|
|
|
private readonly TreeAttribute _tempTreeAttribute = new();
|
|
private int GetRadiusFromBlockEntity()
|
|
{
|
|
var radius = properties[RADIUS_ATTRIBUTE].AsInt(10);
|
|
_tempTreeAttribute.Clear();
|
|
Blockentity.ToTreeAttributes(_tempTreeAttribute);
|
|
radius = _tempTreeAttribute.GetAsInt(RADIUS_ATTRIBUTE, radius);
|
|
return radius;
|
|
}
|
|
|
|
private int GetTargetBeeParticleCountFromBlockEntity()
|
|
{
|
|
var targetBeeParticleCount = properties[TARGET_BEE_PARTICLE_COUNT_ATTRIBUTE].AsInt(10);
|
|
targetBeeParticleCount = entityAttributeSnapshot.GetInt(TARGET_BEE_PARTICLE_COUNT_ATTRIBUTE, targetBeeParticleCount);
|
|
return targetBeeParticleCount;
|
|
}
|
|
|
|
public override void OnBlockRemoved()
|
|
{
|
|
Clear();
|
|
base.OnBlockRemoved();
|
|
}
|
|
|
|
public override void OnBlockUnloaded()
|
|
{
|
|
Clear();
|
|
base.OnBlockUnloaded();
|
|
}
|
|
|
|
public override void ToTreeAttributes(ITreeAttribute tree)
|
|
{
|
|
base.ToTreeAttributes(tree);
|
|
|
|
tree.SetInt(flower_count_attribute, FlowerCount);
|
|
tree.SetInt(crop_count_attribute, CropCount);
|
|
tree.SetFloat(initial_scan_progress_attribute, InitialScanProgress);
|
|
tree.SetFloat(rescan_progress_attribute, RescanProgress);
|
|
}
|
|
|
|
public override void FromTreeAttributes(ITreeAttribute tree, IWorldAccessor worldAccessForResolve)
|
|
{
|
|
base.FromTreeAttributes(tree, worldAccessForResolve);
|
|
|
|
if (Api is null || Api.Side == EnumAppSide.Server)
|
|
return;
|
|
FlowerCount = tree.GetInt(flower_count_attribute);
|
|
CropCount = tree.GetInt(crop_count_attribute);
|
|
InitialScanProgress = tree.GetFloat(initial_scan_progress_attribute);
|
|
RescanProgress = tree.GetFloat(rescan_progress_attribute);
|
|
}
|
|
|
|
public void HandleBeeParticleSpawn(BeeSpawnPacket packet, bool catchup = false)
|
|
{
|
|
handleSpawnStopwatch.Restart();
|
|
if (Blockentity.Pos != packet.HivePosition)
|
|
return;
|
|
|
|
if (packet.Path is null)
|
|
{
|
|
modSystem?.Mod.Logger.Warning("Received packet with Path == null");
|
|
return;
|
|
}
|
|
|
|
if (packet.Path.Length == 0)
|
|
{
|
|
modSystem?.Mod.Logger.Warning("Received packet with Path.Length == 0");
|
|
return;
|
|
}
|
|
|
|
if (catchup)
|
|
{
|
|
var totalDelta = Math.Abs(Api.World.Calendar.ElapsedSeconds - packet.TimeElapsedSeconds);
|
|
if (totalDelta > 240)
|
|
{
|
|
Mod?.Logger.Notification($"{nameof(BlockEntityBehaviorBeeSwarm)} {nameof(HandleBeeParticleSpawn)} totalDelta: {totalDelta}s, discarding");
|
|
return;
|
|
}
|
|
}
|
|
|
|
var facing = packet.Facing ?? GetFacing();
|
|
Vector3 entrancePosition = GetEntrancePosition(facing);
|
|
var bee = new InternalBeeParticle(entrancePosition, GetFrontDirection(facing), [.. packet.Path.Select(x => x.ToPoint())], BeeRole.Forager, entrancePosition, packet);
|
|
activeBees.Add(bee);
|
|
if (catchup)
|
|
{
|
|
var totalDelta = Math.Abs(Api.World.Calendar.ElapsedSeconds - packet.TimeElapsedSeconds);
|
|
if (modSystem?.Mod.Info.Version.Contains("dev") == true)
|
|
modSystem.Mod.Logger.Notification($"HandleBeeParticleSpawn catchup totalDelta: {totalDelta}s");
|
|
|
|
for (var i = 0; i < totalDelta / 0.1f; i++)
|
|
bee.Step(0.1f, 0.1f); // todo
|
|
}
|
|
handleSpawnStopwatch.StopAndLogTime(this, 0.05);
|
|
}
|
|
|
|
public void Clear()
|
|
{
|
|
if (modSystem is null)
|
|
return;
|
|
|
|
modSystem.GlobalActiveBees -= activeBees.Count;
|
|
modSystem.BeeSpawnPacketDistributor?.Unregister(Blockentity.Pos);
|
|
plantPositionRegistry?.UnregisterBeehive(Blockentity.Pos);
|
|
|
|
activeBees.Clear();
|
|
}
|
|
|
|
private void OnUpdatePlantInfoTick(float dt)
|
|
{
|
|
if (!ShouldTick())
|
|
return;
|
|
|
|
plantInfoStopwatch.Restart();
|
|
UpdatePlantInfo();
|
|
UpdateRadius();
|
|
|
|
var note = $"global bees: {modSystem?.GlobalActiveBees}, registered hives: {modSystem?.BeeSpawnPacketDistributor?.SpawnHandlersCount}, radius: {Config.Instance.BeeRoamingRadius}";
|
|
plantInfoStopwatch.StopAndLogTime(this, 0.1, note);
|
|
}
|
|
|
|
private void UpdateRadius()
|
|
{
|
|
var radius = GetRadiusFromBlockEntity();
|
|
if (registeredRadius == radius)
|
|
return;
|
|
|
|
var plantPositionRegistry = Api.GetPlantPositionRegistry();
|
|
try
|
|
{
|
|
plantPositionRegistry?.UpdateBeehiveRadius(Pos, radius);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Mod?.Logger.Warning("Couldn't update radius");
|
|
Mod?.Logger.Warning(e);
|
|
}
|
|
registeredRadius = radius;
|
|
}
|
|
|
|
private bool ShouldTick()
|
|
{
|
|
if (Block is BlockSkep && !Config.Instance.EnableOnVanillaSkeps)
|
|
return false;
|
|
|
|
var isFgc = Block?.Code?.Domain == "fromgoldencombs";
|
|
var path = Block?.Code?.Path ?? string.Empty;
|
|
var isFgcLangstroth = isFgc && path.Contains("langstrothstack", StringComparison.OrdinalIgnoreCase);
|
|
var isFgcCeramic = isFgc && path.Contains("ceramicbroodpot", StringComparison.OrdinalIgnoreCase);
|
|
|
|
if (isFgcLangstroth)
|
|
{
|
|
if (!Config.Instance.EnableOnFgcLangstroth)
|
|
return false;
|
|
if (!HasRequiredLangstrothBase())
|
|
return false;
|
|
}
|
|
|
|
if (isFgcCeramic && !Config.Instance.EnableOnFgcCeramic)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
private void OnParticleTick(float dt)
|
|
{
|
|
if (!ShouldTick())
|
|
return;
|
|
|
|
var isFgc = Block?.Code?.Domain == "fromgoldencombs";
|
|
var path = Block?.Code?.Path ?? string.Empty;
|
|
var isFgcLangstroth = isFgc && path.Contains("langstrothstack", StringComparison.OrdinalIgnoreCase);
|
|
var isFgcCeramic = isFgc && path.Contains("ceramicbroodpot", StringComparison.OrdinalIgnoreCase);
|
|
|
|
particleStopwatch.Restart();
|
|
|
|
entityAttributeSnapshot.Clear();
|
|
Blockentity.ToTreeAttributes(entityAttributeSnapshot);
|
|
|
|
var targetBeeParticleCount = GetTargetBeeParticleCountFromBlockEntity();
|
|
TargetParticleCount = Math.Max(0, targetBeeParticleCount);
|
|
|
|
if (TargetParticleCount <= 0)
|
|
return;
|
|
|
|
if (Block is BlockSkep)
|
|
TargetParticleCount = Config.Instance.RoamingBeesPerVanillaSkep;
|
|
if (isFgcLangstroth)
|
|
TargetParticleCount = Config.Instance.RoamingBeesPerFgcLangstroth;
|
|
if (isFgcCeramic)
|
|
TargetParticleCount = Config.Instance.RoamingBeesPerFgcCeramic;
|
|
|
|
Update(dt);
|
|
if (particleStopwatch.StopAndLogTime(this, 0.1, note: $"global bees: {modSystem?.GlobalActiveBees}, registered hives: {modSystem?.BeeSpawnPacketDistributor?.SpawnHandlersCount}"))
|
|
onParticleTickStopwatchTriggerCount++;
|
|
}
|
|
|
|
private void Update(float dt)
|
|
{
|
|
if (dt > 10f)
|
|
return;
|
|
|
|
if (climate is null || windVec is null)
|
|
return;
|
|
|
|
dt = Math.Min(0.5f, dt);
|
|
var windSpeed = (float)Math.Min(windVec.Length(), 1.0);
|
|
rainfall = climate?.Rainfall ?? 0f;
|
|
temperature = climate?.Temperature ?? 20f;
|
|
|
|
// iterate from end to easily remove from list
|
|
var end = activeBees.Count - 1;
|
|
end -= onParticleTickStopwatchTriggerCount * 10;
|
|
|
|
for (int i = end; i >= 0; i--)
|
|
{
|
|
var bee = activeBees[i];
|
|
|
|
if (Api.Side == EnumAppSide.Client)
|
|
{
|
|
bee.Step(dt, windSpeed);
|
|
if (Config.Instance.ReceiveParticles && Api.World is IClientWorldAccessor clientWorld)
|
|
BeeVisualParticleRenderer.Spawn(clientWorld, Blockentity.Pos, bee);
|
|
}
|
|
if (Api.Side == EnumAppSide.Server)
|
|
bee.Step(dt, windSpeed);
|
|
|
|
if (bee.ShouldBeDespawned && modSystem != null)
|
|
{
|
|
activeBees.RemoveAt(i);
|
|
modSystem.GlobalActiveBees--;
|
|
}
|
|
}
|
|
|
|
TimeSinceLastSpawn += dt;
|
|
}
|
|
|
|
private void OnSpawnTick(float dt)
|
|
{
|
|
if (!ShouldTick())
|
|
return;
|
|
|
|
if (climate is null || windVec is null)
|
|
return;
|
|
|
|
serverSpawnStopwatch.Restart();
|
|
TrySpawnNewBee(TargetParticleCount);
|
|
if (serverSpawnStopwatch.StopAndLogTime(this, 0.1))
|
|
spawnStopwatchTriggerCount++;
|
|
}
|
|
|
|
private void TrySpawnNewBee(int targetParticleCount)
|
|
{
|
|
int desiredParticleCount = targetParticleCount;
|
|
if (activeBees.Count >= desiredParticleCount)
|
|
return;
|
|
|
|
if (TimeSinceLastSpawn < spawn_cooldown_seconds)
|
|
return;
|
|
|
|
if (relativePlantPositions.Count == 0)
|
|
return;
|
|
|
|
var cfg = Config.Instance;
|
|
|
|
var sunPos = Api.World.Calendar.GetSunPosition(new Vec3d(Blockentity.Pos.X, Blockentity.Pos.Y, Blockentity.Pos.Z), Api.World.Calendar.TotalDays);
|
|
float sunAltitudeDegrees = MathF.Asin(sunPos.Y) * 180f / GameMath.PI;
|
|
if (sunAltitudeDegrees <= cfg.SunAltitudeMinDegrees)
|
|
return;
|
|
|
|
if (rainfall >= cfg.RainfallSpawnStopThreshold)
|
|
return;
|
|
|
|
int maxGlobal = cfg.MaxGlobalRoamingBees;
|
|
if (modSystem?.ServerChannel is null)
|
|
return;
|
|
|
|
if (modSystem.GlobalActiveBees >= maxGlobal)
|
|
return;
|
|
|
|
float sunAltitudeModifier = 1 - Math.Clamp((sunAltitudeDegrees - cfg.SunAltitudeMinDegrees) / cfg.SunAltitudeRangeDegrees, 0f, 1f);
|
|
float sunAltitudeCooldownPenalty = sunAltitudeModifier * cfg.MaxSunAltitudeCooldownPenalty;
|
|
|
|
float rainfallFactor = rainfall / cfg.RainfallSpawnStopThreshold;
|
|
float rainfallCooldownPenalty = rainfallFactor * cfg.MaxRainfallCooldownPenalty;
|
|
float spawnCooldownSeconds = rainfallCooldownPenalty + sunAltitudeCooldownPenalty;
|
|
|
|
float temperatureRange = cfg.MaxTemperatureParticleSpawn - cfg.MinTemperatureParticleSpawn;
|
|
float temperatureFactor = Math.Clamp((cfg.MaxTemperatureParticleSpawn - temperature) / temperatureRange, 0f, 1f);
|
|
|
|
var randomChance = random.NextDouble() > temperatureFactor && random.NextDouble() > sunAltitudeModifier && random.NextDouble() > 0.3f;
|
|
if (!randomChance)
|
|
{
|
|
var noise = random.NextDouble() * 2;
|
|
TimeSinceLastSpawn = -(float)(random.NextDouble() * spawnCooldownSeconds + noise);
|
|
return;
|
|
}
|
|
|
|
var facing = GetFacing();
|
|
var entrancePos = GetEntrancePosition(facing);
|
|
var frontDirection = GetFrontDirection(facing);
|
|
var path = BeePathGeneration.GeneratePath(Api.World.BlockAccessor, Blockentity.Pos, entrancePos, frontDirection, relativePlantPositions);
|
|
if (path is null || path.Length == 0)
|
|
return;
|
|
|
|
var message = new BeeSpawnPacket
|
|
{
|
|
HivePosition = Blockentity.Pos,
|
|
Path = [.. path.Select(x => BeePlannedPathPointContract.FromPoint(x))],
|
|
Facing = facing,
|
|
Role = BeeRole.Forager,
|
|
TimeElapsedSeconds = Api.World.Calendar.ElapsedSeconds,
|
|
EntrancePosition = entrancePos,
|
|
DespawnPosition = entrancePos,
|
|
};
|
|
activeBees.Add(new InternalBeeParticle(entrancePos, frontDirection, path, BeeRole.Forager, entrancePos, message));
|
|
modSystem.GlobalActiveBees++;
|
|
TimeSinceLastSpawn = 0f;
|
|
|
|
modSystem.ServerChannel.BroadcastPacket(message);
|
|
}
|
|
|
|
private bool HasRequiredLangstrothBase()
|
|
{
|
|
if (Blockentity is not BlockEntityDisplay blockEntityDisplay)
|
|
return false;
|
|
|
|
return blockEntityDisplay.Inventory.Any(x => x.Itemstack?.Block?.Code?.Path?.Contains("langstrothbase", StringComparison.OrdinalIgnoreCase) == true);
|
|
}
|
|
|
|
public Vector3 GetEntrancePosition() => GetEntrancePosition(GetFacing());
|
|
public Vector3 GetFrontDirection() => GetFrontDirection(GetFacing());
|
|
|
|
private Vector3 GetEntrancePosition(string facing)
|
|
{
|
|
if (entrancePositions != null)
|
|
{
|
|
if (entrancePositions.TryGetValue(facing, out var entrance))
|
|
return entrance;
|
|
if (entrancePositions.TryGetValue("*", out entrance))
|
|
return entrance;
|
|
}
|
|
|
|
return facing switch
|
|
{
|
|
"north" => new Vector3(0.5f, 0.2f, 0.9f),
|
|
"east" => new Vector3(0.1f, 0.2f, 0.5f),
|
|
"south" => new Vector3(0.5f, 0.2f, 0.1f),
|
|
"west" => new Vector3(0.9f, 0.2f, 0.5f),
|
|
_ => new Vector3(0.5f, 0.2f, 0.9f)
|
|
};
|
|
}
|
|
|
|
private void UpdatePlantInfo()
|
|
{
|
|
relativePlantPositions.Clear();
|
|
|
|
int radius = Config.Instance.BeeRoamingRadius;
|
|
if (radius <= 0)
|
|
{
|
|
radius = entityAttributeSnapshot.GetInt(RADIUS_ATTRIBUTE, 10);
|
|
if (radius <= 0)
|
|
return;
|
|
}
|
|
|
|
var plantRegistry = Api.GetPlantPositionRegistry();
|
|
if (plantRegistry is null)
|
|
return;
|
|
|
|
var (initialScanProgress, rescanProgress) = plantRegistry.GetPlantsNearPosition(Blockentity.Pos, radius, flowerPositions, cropPositions);
|
|
FlowerCount = flowerPositions.Count;
|
|
CropCount = cropPositions.Count;
|
|
InitialScanProgress = initialScanProgress;
|
|
RescanProgress = rescanProgress;
|
|
|
|
foreach (var pos in flowerPositions)
|
|
{
|
|
float relX = pos.X - Blockentity.Pos.X + 0.5f;
|
|
float relY = pos.Y - Blockentity.Pos.Y + 0.5f;
|
|
float relZ = pos.Z - Blockentity.Pos.Z + 0.5f;
|
|
relativePlantPositions.Add(new Vector3(relX, relY, relZ));
|
|
}
|
|
|
|
foreach (var pos in cropPositions)
|
|
{
|
|
float relX = pos.X - Blockentity.Pos.X + 0.5f;
|
|
float relY = pos.Y - Blockentity.Pos.Y + 0.5f;
|
|
float relZ = pos.Z - Blockentity.Pos.Z + 0.5f;
|
|
relativePlantPositions.Add(new Vector3(relX, relY, relZ));
|
|
}
|
|
}
|
|
|
|
private Vector3 GetFrontDirection(string facing)
|
|
{
|
|
if (frontDirections != null)
|
|
{
|
|
if (frontDirections.TryGetValue(facing, out var frontDirection))
|
|
return frontDirection;
|
|
if (frontDirections.TryGetValue("*", out frontDirection))
|
|
return frontDirection;
|
|
}
|
|
|
|
return facing switch
|
|
{
|
|
"north" => new Vector3(0f, 0f, 1f),
|
|
"east" => new Vector3(-1f, 0f, 0f),
|
|
"south" => new Vector3(0f, 0f, -1f),
|
|
"west" => new Vector3(1f, 0f, 0f),
|
|
_ => new Vector3(0f, 0f, 1f)
|
|
};
|
|
}
|
|
|
|
private string GetFacing()
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(facingVariantKey))
|
|
{
|
|
var block = Blockentity.Block;
|
|
if (block?.Variant?.ContainsKey(facingVariantKey) == true)
|
|
return block.Variant[facingVariantKey];
|
|
}
|
|
|
|
// omnidirectional block, randomly pick from available entrance directions
|
|
if (entrancePositions is { Count: > 0 })
|
|
{
|
|
var keys = entrancePositions.Keys.Where(k => k != "*").ToList();
|
|
if (keys.Count > 0)
|
|
return keys[random.Next(keys.Count)];
|
|
}
|
|
|
|
return "north";
|
|
}
|
|
}
|