using System; using System.Collections.Generic; using System.Linq; using CultMath; using MessagePack; #nullable enable namespace GameCult.Aetheria.State.Verse { public enum AetheriaRuntimeGravityInfluenceKind { Planet, GasGiant, Sun } /// /// Horizontal X/Z viewport boundary for callers projecting the world into a two-dimensional surface. /// public readonly struct AetheriaRuntimeXzRect { public AetheriaRuntimeXzRect(double minX, double minZ, double maxX, double maxZ) { MinX = Math.Min(minX, maxX); MinZ = Math.Min(minZ, maxZ); MaxX = Math.Max(minX, maxX); MaxZ = Math.Max(minZ, maxZ); } public double MinX { get; } public double MinZ { get; } public double MaxX { get; } public double MaxZ { get; } public rect ToAetheriaXyRect() { return new rect( (float)MinX, (float)MinZ, (float)MaxX, (float)MaxZ); } } [MessagePackObject(true)] public readonly struct AetheriaRuntimeExponentialCurve { public AetheriaRuntimeExponentialCurve(double exponent, double multiplier, double constant) { Exponent = exponent; Multiplier = multiplier; Constant = constant; } public double Exponent { get; } public double Multiplier { get; } public double Constant { get; } public double Evaluate(double value) { return Multiplier * Math.Pow(value, Exponent) + Constant; } } [MessagePackObject(true)] public readonly struct AetheriaRuntimeDaemonRenderSettings { public static AetheriaRuntimeDaemonRenderSettings AetheriaDefault { get; } = new AetheriaRuntimeDaemonRenderSettings( new AetheriaRuntimeExponentialCurve(3.0, 0.000000001, 0.0), new AetheriaRuntimeExponentialLerp(4.0, 0.01, 10.0), new AetheriaRuntimeExponentialLerp(2.0, 5.0, 720.0), 50.0, 273.0, 330.0, 0.25, 0.1, 256.0, heatstrokePhasingFloor: 0.0, heatstrokePhasingFrequency: 5.0, minimapZoomLevels: new[] { 250.0, 500.0, 1000.0, 2000.0, 4000.0 }, defaultMinimapZoom: 3, wormholeDistanceRatio: 0.75, defaultViewDistance: 4096.0, minimapIconScale: 0.125, minimapAsteroidSize: 3.0, bodyIconSizeCurve: new AetheriaRuntimeExponentialCurve(0.15, 10.0, 25.0), minimapZoneGravityRange: 0.45, asteroidVerticalOffset: -10.0, planetRotationSpeed: 0.1, zoneBoundaryPower: 2.0, zoneBoundaryDepth: 64.0, asteroidMeshCount: 4, bodyRadiusCurve: new AetheriaRuntimeExponentialCurve(0.25, 3.0, 0.0), lightRadiusCurve: new AetheriaRuntimeExponentialCurve(0.25, 300.0, 0.0), gravityWaveFrequencyCurve: new AetheriaRuntimeExponentialCurve(0.45, 0.2, 0.0)); public AetheriaRuntimeDaemonRenderSettings( AetheriaRuntimeExponentialCurve temperatureEmissionCurve, AetheriaRuntimeExponentialLerp lockIndicatorFrequency, AetheriaRuntimeExponentialLerp lockSpinSpeed, double convergenceMinimumDistance, double hypothermiaTemperature, double heatstrokeTemperature, double severeHeatstrokeRiskThreshold, double targetDetectionInfoThreshold, double lockIndicatorNoiseAmplitude, double heatstrokePhasingFloor = 0.0, double heatstrokePhasingFrequency = 5.0, double targetSpottedBlinkFrequency = 20.0, double targetSpottedBlinkOffset = -0.25, IReadOnlyList? minimapZoomLevels = null, int defaultMinimapZoom = 0, double wormholeDistanceRatio = 1.0, double defaultViewDistance = 0.0, double minimapIconScale = 0.0, double minimapAsteroidSize = 0.0, AetheriaRuntimeExponentialCurve bodyIconSizeCurve = default, double minimapZoneGravityRange = 0.0, double asteroidVerticalOffset = 0.0, double planetRotationSpeed = 0.0, double zoneBoundaryPower = 0.0, double zoneBoundaryDepth = 0.0, int asteroidMeshCount = int.MaxValue, AetheriaRuntimeExponentialCurve bodyRadiusCurve = default, AetheriaRuntimeExponentialCurve lightRadiusCurve = default, AetheriaRuntimeExponentialCurve gravityWaveFrequencyCurve = default) { TemperatureEmissionCurve = temperatureEmissionCurve; LockIndicatorFrequency = lockIndicatorFrequency; LockSpinSpeed = lockSpinSpeed; ConvergenceMinimumDistance = convergenceMinimumDistance; HypothermiaTemperature = hypothermiaTemperature; HeatstrokeTemperature = heatstrokeTemperature; SevereHeatstrokeRiskThreshold = severeHeatstrokeRiskThreshold; TargetDetectionInfoThreshold = targetDetectionInfoThreshold; LockIndicatorNoiseAmplitude = lockIndicatorNoiseAmplitude; HeatstrokePhasingFloor = heatstrokePhasingFloor; HeatstrokePhasingFrequency = heatstrokePhasingFrequency; TargetSpottedBlinkFrequency = targetSpottedBlinkFrequency; TargetSpottedBlinkOffset = targetSpottedBlinkOffset; MinimapZoomLevels = CopyPositiveMinimapZoomLevels(minimapZoomLevels); DefaultMinimapZoom = defaultMinimapZoom; WormholeDistanceRatio = Math.Max(0.0, wormholeDistanceRatio); DefaultViewDistance = Math.Max(0.0, defaultViewDistance); MinimapIconScale = Math.Max(0.0, minimapIconScale); MinimapAsteroidSize = Math.Max(0.0, minimapAsteroidSize); BodyIconSizeCurve = bodyIconSizeCurve; MinimapZoneGravityRange = Math.Max(0.0, minimapZoneGravityRange); AsteroidVerticalOffset = asteroidVerticalOffset; PlanetRotationSpeed = planetRotationSpeed; ZoneBoundaryPower = zoneBoundaryPower; ZoneBoundaryDepth = zoneBoundaryDepth; AsteroidMeshCount = Math.Max(0, asteroidMeshCount); BodyRadiusCurve = bodyRadiusCurve; LightRadiusCurve = lightRadiusCurve; GravityWaveFrequencyCurve = gravityWaveFrequencyCurve; } public AetheriaRuntimeExponentialCurve TemperatureEmissionCurve { get; } public AetheriaRuntimeExponentialLerp LockIndicatorFrequency { get; } public AetheriaRuntimeExponentialLerp LockSpinSpeed { get; } public double ConvergenceMinimumDistance { get; } public double HypothermiaTemperature { get; } public double HeatstrokeTemperature { get; } public double SevereHeatstrokeRiskThreshold { get; } public double TargetDetectionInfoThreshold { get; } public double LockIndicatorNoiseAmplitude { get; } public double HeatstrokePhasingFloor { get; } public double HeatstrokePhasingFrequency { get; } public double TargetSpottedBlinkFrequency { get; } public double TargetSpottedBlinkOffset { get; } public IReadOnlyList MinimapZoomLevels { get; } public int DefaultMinimapZoom { get; } public double WormholeDistanceRatio { get; } public double DefaultViewDistance { get; } public double MinimapIconScale { get; } public double MinimapAsteroidSize { get; } public AetheriaRuntimeExponentialCurve BodyIconSizeCurve { get; } public double MinimapZoneGravityRange { get; } public double AsteroidVerticalOffset { get; } public double PlanetRotationSpeed { get; } public double ZoneBoundaryPower { get; } public double ZoneBoundaryDepth { get; } public int AsteroidMeshCount { get; } public AetheriaRuntimeExponentialCurve BodyRadiusCurve { get; } public AetheriaRuntimeExponentialCurve LightRadiusCurve { get; } public AetheriaRuntimeExponentialCurve GravityWaveFrequencyCurve { get; } public int ResolveDefaultMinimapZoomIndex() { var levels = ResolveMinimapZoomLevels(); return ClampIndex(DefaultMinimapZoom, levels.Count); } public int ResolveNextMinimapZoomIndex(int currentIndex) { var levels = ResolveMinimapZoomLevels(); if (levels.Count == 0) return 0; return currentIndex < 0 ? ResolveDefaultMinimapZoomIndex() : (currentIndex + 1) % levels.Count; } public double ResolveMinimapDistance(int zoomIndex) { var levels = ResolveMinimapZoomLevels(); return levels[ClampIndex(zoomIndex, levels.Count)]; } public double ResolveDefaultMinimapDistance() { return ResolveMinimapDistance(ResolveDefaultMinimapZoomIndex()); } public double ResolveMinimapIconSize(double minimapDistance) { return Math.Max(0.0, minimapDistance) * MinimapIconScale; } public double ResolveBodyIconSize(double mass) { return BodyIconSizeCurve.Evaluate(Math.Max(0.0, mass)); } public double ResolveBodyRadius(double mass) { return BodyRadiusCurve.Evaluate(Math.Max(0.0, mass)); } public double ResolveLightRadius(double mass) { return LightRadiusCurve.Evaluate(Math.Max(0.0, mass)); } public double ResolveGravityWaveFrequency(double mass) { return GravityWaveFrequencyCurve.Evaluate(Math.Max(0.0, mass)); } public double NormalizeThermalRisk(double temperature) { var range = HeatstrokeTemperature - HypothermiaTemperature; if (range <= 0) return temperature >= HeatstrokeTemperature ? 1.0 : 0.0; return Math.Max(0.0, Math.Min(1.0, (temperature - HypothermiaTemperature) / range)); } public double NormalizeHeatstrokePost(double heatstroke) { if (SevereHeatstrokeRiskThreshold <= 0) return heatstroke > 0 ? 1.0 : 0.0; return Saturate(heatstroke / SevereHeatstrokeRiskThreshold); } public double NormalizeSevereHeatstrokePost(double heatstroke) { var range = 1.0 - SevereHeatstrokeRiskThreshold; if (range <= 0) return heatstroke >= 1.0 ? 1.0 : 0.0; return Saturate((heatstroke - SevereHeatstrokeRiskThreshold) / range); } public double ResolveSevereHeatstrokePostWeight(double heatstroke, double timeSeconds) { var normalized = NormalizeSevereHeatstrokePost(heatstroke); return normalized + normalized * (1.0 - normalized) * Math.Max(HeatstrokePhasingFloor, Math.Sin(timeSeconds * HeatstrokePhasingFrequency)); } public double NormalizeDetectionProgress(double infoGathered) { return TargetDetectionInfoThreshold <= 0 ? (infoGathered > 0 ? 1.0 : 0.0) : Saturate(infoGathered / TargetDetectionInfoThreshold); } public bool ResolveTargetSpottedFillEnabled(double infoGathered, double timeSeconds) { return !(infoGathered > TargetDetectionInfoThreshold) || Math.Sin(TargetSpottedBlinkFrequency * timeSeconds) + TargetSpottedBlinkOffset > 0; } public double NormalizeTargetVisibilityFill(double infoGathered) { var denominator = 1.0 - TargetDetectionInfoThreshold; var normalized = denominator <= 0 ? (infoGathered >= 1.0 ? 1.0 : 0.0) : (infoGathered - TargetDetectionInfoThreshold) / denominator; return Lerp(0.25, 0.75, normalized); } public double NormalizeVisibilityToTargetFill(double infoGathered) { return Lerp(0.25, 0.75, NormalizeDetectionProgress(infoGathered)); } public double NormalizeTargetStatusFill(double normalizedValue) { return Lerp(0.25, 0.75, normalizedValue); } public double ResolveLockIndicatorNoiseAmplitude(double lockProgress) { return LockIndicatorNoiseAmplitude * (1.0 - Saturate(lockProgress)); } public double ResolveLockIndicatorNoiseFrequency(double lockProgress) { return LockIndicatorFrequency.Evaluate(lockProgress); } public double ResolveLockSpinSpeed(double lockProgress) { return LockSpinSpeed.Evaluate(lockProgress); } private static double Saturate(double value) { return Math.Max(0.0, Math.Min(1.0, value)); } private static double Lerp(double minimum, double maximum, double value) { return minimum + Saturate(value) * (maximum - minimum); } private IReadOnlyList ResolveMinimapZoomLevels() { return MinimapZoomLevels != null && MinimapZoomLevels.Count > 0 ? MinimapZoomLevels : DefaultMinimapZoomLevels; } private static int ClampIndex(int index, int count) { if (count <= 1) return 0; return Math.Max(0, Math.Min(count - 1, index)); } private static IReadOnlyList CopyPositiveMinimapZoomLevels(IReadOnlyList? levels) { if (levels == null || levels.Count == 0) return DefaultMinimapZoomLevels; var copy = new List(levels.Count); for (var index = 0; index < levels.Count; index++) { var level = levels[index]; if (level > 0 && !double.IsNaN(level) && !double.IsInfinity(level)) copy.Add(level); } return copy.Count > 0 ? copy.ToArray() : DefaultMinimapZoomLevels; } private static readonly IReadOnlyList DefaultMinimapZoomLevels = new[] { 1000.0 }; } public readonly struct AetheriaRuntimeGravityInfluenceBrush { public AetheriaRuntimeGravityInfluenceBrush( string bodyKey, string orbitKey, AetheriaRuntimeGravityInfluenceKind kind, double centerX, double centerZ, double radius, double gravityDepth, double gravityDepthExponent, double waveRadius, double waveDepth, double waveSpeed) { BodyKey = bodyKey ?? ""; OrbitKey = orbitKey ?? ""; Kind = kind; CenterX = centerX; CenterZ = centerZ; Radius = Math.Max(0, radius); GravityDepth = gravityDepth; GravityDepthExponent = gravityDepthExponent; WaveRadius = Math.Max(0, waveRadius); WaveDepth = waveDepth; WaveSpeed = waveSpeed; } public string BodyKey { get; } public string OrbitKey { get; } public AetheriaRuntimeGravityInfluenceKind Kind { get; } public double CenterX { get; } public double CenterZ { get; } public double Radius { get; } public double GravityDepth { get; } public double GravityDepthExponent { get; } public double WaveRadius { get; } public double WaveDepth { get; } public double WaveSpeed { get; } } public readonly struct AetheriaRuntimeDaemonBodyPose { public AetheriaRuntimeDaemonBodyPose( string bodyKey, string orbitKey, string parentOrbitKey, string kind, double centerX, double centerZ, double parentCenterX, double parentCenterZ, double gravityWaveSpeed) { BodyKey = bodyKey ?? ""; OrbitKey = orbitKey ?? ""; ParentOrbitKey = parentOrbitKey ?? ""; Kind = kind ?? ""; CenterX = centerX; CenterZ = centerZ; ParentCenterX = parentCenterX; ParentCenterZ = parentCenterZ; GravityWaveSpeed = gravityWaveSpeed; } public string BodyKey { get; } public string OrbitKey { get; } public string ParentOrbitKey { get; } public string Kind { get; } public double CenterX { get; } public double CenterZ { get; } public double ParentCenterX { get; } public double ParentCenterZ { get; } public double GravityWaveSpeed { get; } } public readonly struct AetheriaRuntimeDaemonBodyView { public AetheriaRuntimeDaemonBodyView( AetheriaRuntimeBodySnapshotCommit body, AetheriaRuntimeDaemonBodyPose pose, bool isAsteroidBelt) { Body = body ?? throw new ArgumentNullException(nameof(body)); Pose = pose; IsAsteroidBelt = isAsteroidBelt; } public AetheriaRuntimeBodySnapshotCommit Body { get; } public AetheriaRuntimeDaemonBodyPose Pose { get; } public bool IsAsteroidBelt { get; } } public readonly struct AetheriaRuntimeDaemonAsteroidBeltPose { public AetheriaRuntimeDaemonAsteroidBeltPose( string bodyKey, string orbitKey, double centerX, double centerZ, double radius, int asteroidCount) { BodyKey = bodyKey ?? ""; OrbitKey = orbitKey ?? ""; CenterX = centerX; CenterZ = centerZ; Radius = Math.Max(0, radius); AsteroidCount = Math.Max(0, asteroidCount); } public string BodyKey { get; } public string OrbitKey { get; } public double CenterX { get; } public double CenterZ { get; } public double Radius { get; } public int AsteroidCount { get; } } public readonly struct AetheriaRuntimeDaemonAsteroidInstancePose { public AetheriaRuntimeDaemonAsteroidInstancePose( string bodyKey, int asteroidIndex, double positionX, double positionZ, double rotation, double size) { BodyKey = bodyKey ?? ""; AsteroidIndex = Math.Max(0, asteroidIndex); PositionX = positionX; PositionZ = positionZ; Rotation = rotation; Size = Math.Max(0, size); } public string BodyKey { get; } public int AsteroidIndex { get; } public double PositionX { get; } public double PositionZ { get; } public double Rotation { get; } public double Size { get; } } public readonly struct AetheriaRuntimeDaemonCompassMarker { public AetheriaRuntimeDaemonCompassMarker( int targetEntityIndex, double positionX, double positionZ, double deltaX, double deltaZ, double distance, double infoGathered, bool hostile) { TargetEntityIndex = targetEntityIndex; PositionX = positionX; PositionZ = positionZ; DeltaX = deltaX; DeltaZ = deltaZ; Distance = Math.Max(0, distance); InfoGathered = infoGathered; Hostile = hostile; } public int TargetEntityIndex { get; } public double PositionX { get; } public double PositionZ { get; } public double DeltaX { get; } public double DeltaZ { get; } public double Distance { get; } public double InfoGathered { get; } public bool Hostile { get; } } public readonly struct AetheriaRuntimeDaemonEntityContact { public AetheriaRuntimeDaemonEntityContact( int observerEntityIndex, int targetEntityIndex, double targetPositionX, double targetPositionZ, double deltaX, double deltaZ, double distance, double infoGathered, bool hostile, bool visible) { ObserverEntityIndex = observerEntityIndex; TargetEntityIndex = targetEntityIndex; TargetPositionX = targetPositionX; TargetPositionZ = targetPositionZ; DeltaX = deltaX; DeltaZ = deltaZ; Distance = Math.Max(0, distance); InfoGathered = infoGathered; Hostile = hostile; Visible = visible; } public int ObserverEntityIndex { get; } public int TargetEntityIndex { get; } public double TargetPositionX { get; } public double TargetPositionZ { get; } public double DeltaX { get; } public double DeltaZ { get; } public double Distance { get; } public double InfoGathered { get; } public bool Hostile { get; } public bool Visible { get; } } public readonly struct AetheriaRuntimeDaemonEntityTarget { public AetheriaRuntimeDaemonEntityTarget( int observerEntityIndex, int targetEntityIndex, double targetPositionX, double targetPositionZ, double deltaX, double deltaZ, double distance) { ObserverEntityIndex = observerEntityIndex; TargetEntityIndex = targetEntityIndex; TargetPositionX = targetPositionX; TargetPositionZ = targetPositionZ; DeltaX = deltaX; DeltaZ = deltaZ; Distance = Math.Max(0, distance); } public int ObserverEntityIndex { get; } public int TargetEntityIndex { get; } public double TargetPositionX { get; } public double TargetPositionZ { get; } public double DeltaX { get; } public double DeltaZ { get; } public double Distance { get; } } public readonly struct AetheriaRuntimeDaemonObjectViewportEntity { public AetheriaRuntimeDaemonObjectViewportEntity( int entityIndex, string entityKey, string displayName, double3 position, double2 xy, double2 direction, double2 velocity, bool controlled) { EntityIndex = entityIndex; EntityKey = entityKey ?? ""; DisplayName = displayName ?? ""; Position = position; Xy = xy; Direction = direction; Velocity = velocity; Controlled = controlled; } public int EntityIndex { get; } public string EntityKey { get; } public string DisplayName { get; } public double3 Position { get; } public double2 Xy { get; } public double2 Direction { get; } public double2 Velocity { get; } public bool Controlled { get; } } public readonly struct AetheriaRuntimeDaemonWormholeExit { public AetheriaRuntimeDaemonWormholeExit( int targetZoneIndex, double directionX, double directionZ, double positionX, double positionZ) { TargetZoneIndex = targetZoneIndex; DirectionX = directionX; DirectionZ = directionZ; PositionX = positionX; PositionZ = positionZ; } public int TargetZoneIndex { get; } public double DirectionX { get; } public double DirectionZ { get; } public double PositionX { get; } public double PositionZ { get; } } public readonly struct AetheriaRuntimeGravityTerrainBand { public AetheriaRuntimeGravityTerrainBand(double startDepth, double depthRange) { StartDepth = startDepth; DepthRange = depthRange; } public double StartDepth { get; } public double DepthRange { get; } } public static class AetheriaRuntimeDaemonRenderQueries { public static double ResolveZoneRenderRadius( AetheriaRuntimeZoneSnapshotCommit? zone, double fallbackRadius) { if (zone != null && zone.GravityTerrainRadius > 0) return zone.GravityTerrainRadius; return Math.Max(0, fallbackRadius); } public static AetheriaRuntimeGravityInfluenceBrush[] QueryGravityInfluences( AetheriaRuntimeZoneSnapshotCommit? zone, AetheriaRuntimeXzRect viewport) { return QueryGravityInfluences(zone, viewport.ToAetheriaXyRect()); } public static AetheriaRuntimeGravityInfluenceBrush[] QueryGravityInfluences( AetheriaRuntimeZoneSnapshotCommit? zone, rect viewport) { var brushes = new List(); QueryGravityInfluences(zone, viewport, brushes); return brushes.Count == 0 ? Array.Empty() : brushes.ToArray(); } public static int QueryGravityInfluences( AetheriaRuntimeZoneSnapshotCommit? zone, AetheriaRuntimeXzRect viewport, List brushes) { return QueryGravityInfluences(zone, viewport.ToAetheriaXyRect(), brushes); } public static int QueryGravityInfluences( AetheriaRuntimeZoneSnapshotCommit? zone, rect viewport, List brushes) { if (brushes == null) throw new ArgumentNullException(nameof(brushes)); brushes.Clear(); if (zone == null) return 0; var orbitPositions = BuildOrbitPositions(zone); foreach (var body in zone.Bodies ?? Array.Empty()) { if (body == null || !TryResolveBodyCenter(body, orbitPositions, out var center)) continue; var gravityRadius = ResolveGravityRadius(body); var waveRadius = ResolveWaveRadius(body); var radius = Math.Max(gravityRadius, waveRadius); if (!IntersectsCircle(viewport, center.x, center.z, radius)) continue; brushes.Add(new AetheriaRuntimeGravityInfluenceBrush( body.BodyKey, body.OrbitKey, GravityKind(body), center.x, center.z, radius, ResolveGravityDepth(body), body.GravityDepthExponent, waveRadius, ResolveWaveDepth(body), ResolveWaveSpeed(body))); } return brushes.Count; } public static AetheriaRuntimeDaemonBodyPose[] QueryBodyPoses( AetheriaRuntimeZoneSnapshotCommit? zone) { var poses = new List(); QueryBodyPoses(zone, poses); return poses.Count == 0 ? Array.Empty() : poses.ToArray(); } public static int QueryBodyPoses( AetheriaRuntimeZoneSnapshotCommit? zone, List poses) { if (poses == null) throw new ArgumentNullException(nameof(poses)); poses.Clear(); if (zone == null) return 0; var orbitPositions = BuildOrbitPositions(zone); var orbits = BuildOrbitMap(zone); foreach (var body in zone.Bodies ?? Array.Empty()) { if (body == null || !TryResolveBodyCenter(body, orbitPositions, out var center)) continue; var orbitKey = body.OrbitKey ?? ""; var parentOrbitKey = orbits.TryGetValue(orbitKey, out var orbit) ? orbit.ParentOrbitKey ?? "" : ""; var parentCenter = orbitPositions.TryGetValue(parentOrbitKey, out var parent) ? parent : new AetheriaRuntimeXzPoint(0, 0); poses.Add(new AetheriaRuntimeDaemonBodyPose( body.BodyKey, orbitKey, parentOrbitKey, body.Kind, center.x, center.z, parentCenter.x, parentCenter.z, ResolveWaveSpeed(body))); } return poses.Count; } public static AetheriaRuntimeDaemonBodyView[] QueryBodyViews( AetheriaRuntimeZoneSnapshotCommit? zone) { var views = new List(); QueryBodyViews(zone, views); return views.Count == 0 ? Array.Empty() : views.ToArray(); } public static AetheriaRuntimeDaemonBodyView[] QueryBodyViews( AetheriaRuntimeZoneSnapshotCommit? zone, AetheriaRuntimeXzRect viewport) { return QueryBodyViews(zone, viewport.ToAetheriaXyRect()); } public static AetheriaRuntimeDaemonBodyView[] QueryBodyViews( AetheriaRuntimeZoneSnapshotCommit? zone, rect viewport) { var views = new List(); QueryBodyViews(zone, viewport, views); return views.Count == 0 ? Array.Empty() : views.ToArray(); } public static int QueryBodyViews( AetheriaRuntimeZoneSnapshotCommit? zone, List views) { return QueryBodyViews(zone, null, views); } public static int QueryBodyViews( AetheriaRuntimeZoneSnapshotCommit? zone, AetheriaRuntimeXzRect viewport, List views) { return QueryBodyViews(zone, viewport.ToAetheriaXyRect(), views); } public static int QueryBodyViews( AetheriaRuntimeZoneSnapshotCommit? zone, rect viewport, List views) { return QueryBodyViews(zone, (rect?)viewport, views); } private static int QueryBodyViews( AetheriaRuntimeZoneSnapshotCommit? zone, rect? viewport, List views) { if (views == null) throw new ArgumentNullException(nameof(views)); views.Clear(); if (zone == null) return 0; var orbitPositions = BuildOrbitPositions(zone); var orbits = BuildOrbitMap(zone); foreach (var body in zone.Bodies ?? Array.Empty()) { if (body == null || !TryResolveBodyCenter(body, orbitPositions, out var center)) continue; if (viewport.HasValue && !IntersectsCircle( viewport.Value, center.x, center.z, Math.Max(ResolveGravityRadius(body), ResolveWaveRadius(body)))) { continue; } var orbitKey = body.OrbitKey ?? ""; var parentOrbitKey = orbits.TryGetValue(orbitKey, out var orbit) ? orbit.ParentOrbitKey ?? "" : ""; var parentCenter = orbitPositions.TryGetValue(parentOrbitKey, out var parent) ? parent : new AetheriaRuntimeXzPoint(0, 0); var pose = new AetheriaRuntimeDaemonBodyPose( body.BodyKey, orbitKey, parentOrbitKey, body.Kind, center.x, center.z, parentCenter.x, parentCenter.z, ResolveWaveSpeed(body)); views.Add(new AetheriaRuntimeDaemonBodyView( body, pose, string.Equals(body.Kind, "asteroid_belt", StringComparison.OrdinalIgnoreCase))); } return views.Count; } public static AetheriaRuntimeDaemonAsteroidBeltPose[] QueryAsteroidBeltPoses( AetheriaRuntimeZoneSnapshotCommit? zone) { var poses = new List(); QueryAsteroidBeltPoses(zone, poses); return poses.Count == 0 ? Array.Empty() : poses.ToArray(); } public static int QueryAsteroidBeltPoses( AetheriaRuntimeZoneSnapshotCommit? zone, List poses) { if (poses == null) throw new ArgumentNullException(nameof(poses)); poses.Clear(); if (zone == null) return 0; var orbitPositions = BuildOrbitPositions(zone); var orbits = BuildOrbitMap(zone); foreach (var body in zone.Bodies ?? Array.Empty()) { if (body == null || !string.Equals(body.Kind, "asteroid_belt", StringComparison.OrdinalIgnoreCase) || !TryResolveAsteroidBeltCenter(body, orbitPositions, orbits, out var center)) continue; poses.Add(new AetheriaRuntimeDaemonAsteroidBeltPose( body.BodyKey, body.OrbitKey, center.x, center.z, ResolveAsteroidBeltRadius(body), body.Asteroids?.Count ?? 0)); } return poses.Count; } public static AetheriaRuntimeDaemonAsteroidInstancePose[] QueryAsteroidInstancePoses( AetheriaRuntimeZoneSnapshotCommit? zone, string bodyKey, double simulationTimeSeconds) { var poses = new List(); QueryAsteroidInstancePoses(zone, bodyKey, simulationTimeSeconds, poses); return poses.Count == 0 ? Array.Empty() : poses.ToArray(); } public static int QueryAsteroidInstancePoses( AetheriaRuntimeZoneSnapshotCommit? zone, string bodyKey, double simulationTimeSeconds, List poses) { if (poses == null) throw new ArgumentNullException(nameof(poses)); poses.Clear(); if (zone == null) return 0; var orbitPositions = BuildOrbitPositions(zone); var orbits = BuildOrbitMap(zone); foreach (var body in zone.Bodies ?? Array.Empty()) { if (body == null || !string.Equals(body.BodyKey ?? "", bodyKey ?? "", StringComparison.Ordinal) || !string.Equals(body.Kind, "asteroid_belt", StringComparison.OrdinalIgnoreCase) || !TryResolveAsteroidBeltCenter(body, orbitPositions, orbits, out var center)) continue; var asteroids = body.Asteroids ?? Array.Empty(); for (var index = 0; index < asteroids.Count; index++) { var asteroid = asteroids[index]; if (asteroid == null) continue; var phase = Fraction(asteroid.Phase); var x = center.x + Math.Cos(phase * Math.PI * 2.0) * asteroid.Distance; var z = center.z + Math.Sin(phase * Math.PI * 2.0) * asteroid.Distance; var size = asteroid.RespawnTimer > 0 ? 0 : Math.Max(0, asteroid.Size - asteroid.Damage); var rotation = simulationTimeSeconds * asteroid.RotationSpeed; poses.Add(new AetheriaRuntimeDaemonAsteroidInstancePose( body.BodyKey ?? "", index, x, z, rotation, size)); } } return poses.Count; } public static AetheriaRuntimeDaemonCompassMarker[] QueryCompassMarkers( AetheriaRuntimeZoneSnapshotCommit? zone, int observerEntityIndex, double minimumInfoGathered, double minimumDistance) { var markers = new List(); QueryCompassMarkers(zone, observerEntityIndex, minimumInfoGathered, minimumDistance, markers); return markers.Count == 0 ? Array.Empty() : markers.ToArray(); } public static int QueryCompassMarkers( AetheriaRuntimeZoneSnapshotCommit? zone, int observerEntityIndex, double minimumInfoGathered, double minimumDistance, List markers) { if (markers == null) throw new ArgumentNullException(nameof(markers)); markers.Clear(); if (zone == null || observerEntityIndex < 0) return 0; var entities = BuildEntityMap(zone); if (!entities.TryGetValue(observerEntityIndex, out var observer)) return 0; var requiredDistance = Math.Max(0, minimumDistance); foreach (var contact in observer.Contacts ?? Array.Empty()) { if (contact == null || !contact.Visible || contact.InfoGathered <= minimumInfoGathered || !entities.TryGetValue(contact.TargetEntityIndex, out var target)) { continue; } var deltaX = target.PositionX - observer.PositionX; var deltaZ = target.PositionZ - observer.PositionZ; var distance = Math.Sqrt(deltaX * deltaX + deltaZ * deltaZ); if (distance <= requiredDistance) continue; markers.Add(new AetheriaRuntimeDaemonCompassMarker( target.EntityIndex, target.PositionX, target.PositionZ, deltaX, deltaZ, distance, contact.InfoGathered, contact.Hostile)); } return markers.Count; } public static int[] QueryVisibleEntityIndices( AetheriaRuntimeZoneSnapshotCommit? zone, int observerEntityIndex, double minimumInfoGathered) { var entityIndices = new List(); QueryVisibleEntityIndices(zone, observerEntityIndex, minimumInfoGathered, entityIndices); return entityIndices.Count == 0 ? Array.Empty() : entityIndices.ToArray(); } public static int QueryVisibleEntityIndices( AetheriaRuntimeZoneSnapshotCommit? zone, int observerEntityIndex, double minimumInfoGathered, List entityIndices) { if (entityIndices == null) throw new ArgumentNullException(nameof(entityIndices)); entityIndices.Clear(); if (zone == null || observerEntityIndex < 0) return 0; var entities = BuildEntityMap(zone); if (!entities.TryGetValue(observerEntityIndex, out var observer)) return 0; entityIndices.Add(observer.EntityIndex); foreach (var contact in observer.Contacts ?? Array.Empty()) { if (contact != null && contact.Visible && contact.InfoGathered > minimumInfoGathered && entities.ContainsKey(contact.TargetEntityIndex)) { entityIndices.Add(contact.TargetEntityIndex); } } return entityIndices.Count; } public static int[] QueryPresentationEntityIndices( AetheriaRuntimeRunCheckpointCommit? run, AetheriaRuntimeZoneSnapshotCommit? zone, int observerEntityIndex, double minimumInfoGathered, AetheriaRuntimeXzRect viewport) { return QueryPresentationEntityIndices(run, zone, observerEntityIndex, minimumInfoGathered, viewport.ToAetheriaXyRect()); } public static int[] QueryPresentationEntityIndices( AetheriaRuntimeRunCheckpointCommit? run, AetheriaRuntimeZoneSnapshotCommit? zone, int observerEntityIndex, double minimumInfoGathered, rect viewport) { var currentEntityIndex = TryParseEntityIndex(run?.CurrentEntityKey); return QueryPresentationEntityIndices( currentEntityIndex, zone, observerEntityIndex, minimumInfoGathered, viewport); } public static int QueryPresentationEntityIndices( AetheriaRuntimeRunCheckpointCommit? run, AetheriaRuntimeZoneSnapshotCommit? zone, int observerEntityIndex, double minimumInfoGathered, AetheriaRuntimeXzRect viewport, List entityIndices) { return QueryPresentationEntityIndices(run, zone, observerEntityIndex, minimumInfoGathered, viewport.ToAetheriaXyRect(), entityIndices); } public static int QueryPresentationEntityIndices( AetheriaRuntimeRunCheckpointCommit? run, AetheriaRuntimeZoneSnapshotCommit? zone, int observerEntityIndex, double minimumInfoGathered, rect viewport, List entityIndices) { var currentEntityIndex = TryParseEntityIndex(run?.CurrentEntityKey); return QueryPresentationEntityIndices( currentEntityIndex, zone, observerEntityIndex, minimumInfoGathered, viewport, entityIndices); } public static int[] QueryPresentationEntityIndices( string? currentEntityKey, AetheriaRuntimeZoneSnapshotCommit? zone, int observerEntityIndex, double minimumInfoGathered, AetheriaRuntimeXzRect viewport) { return QueryPresentationEntityIndices(currentEntityKey, zone, observerEntityIndex, minimumInfoGathered, viewport.ToAetheriaXyRect()); } public static int[] QueryPresentationEntityIndices( string? currentEntityKey, AetheriaRuntimeZoneSnapshotCommit? zone, int observerEntityIndex, double minimumInfoGathered, rect viewport) { var entityIndices = new List(); var currentEntityIndex = TryParseEntityIndex(currentEntityKey); QueryPresentationEntityIndices( currentEntityIndex, zone, observerEntityIndex, minimumInfoGathered, viewport, entityIndices); return entityIndices.Count == 0 ? Array.Empty() : entityIndices.ToArray(); } private static int[] QueryPresentationEntityIndices( int currentEntityIndex, AetheriaRuntimeZoneSnapshotCommit? zone, int observerEntityIndex, double minimumInfoGathered, rect viewport) { var entityIndices = new List(); QueryPresentationEntityIndices( currentEntityIndex, zone, observerEntityIndex, minimumInfoGathered, viewport, entityIndices); return entityIndices.Count == 0 ? Array.Empty() : entityIndices.ToArray(); } public static int QueryPresentationEntityIndices( string? currentEntityKey, AetheriaRuntimeZoneSnapshotCommit? zone, int observerEntityIndex, double minimumInfoGathered, AetheriaRuntimeXzRect viewport, List entityIndices) { return QueryPresentationEntityIndices(currentEntityKey, zone, observerEntityIndex, minimumInfoGathered, viewport.ToAetheriaXyRect(), entityIndices); } public static int QueryPresentationEntityIndices( string? currentEntityKey, AetheriaRuntimeZoneSnapshotCommit? zone, int observerEntityIndex, double minimumInfoGathered, rect viewport, List entityIndices) { var currentEntityIndex = TryParseEntityIndex(currentEntityKey); return QueryPresentationEntityIndices( currentEntityIndex, zone, observerEntityIndex, minimumInfoGathered, viewport, entityIndices); } private static int QueryPresentationEntityIndices( int currentEntityIndex, AetheriaRuntimeZoneSnapshotCommit? zone, int observerEntityIndex, double minimumInfoGathered, rect viewport, List entityIndices) { if (entityIndices == null) throw new ArgumentNullException(nameof(entityIndices)); entityIndices.Clear(); if (zone == null) return 0; var selected = new HashSet(); if (currentEntityIndex >= 0) selected.Add(currentEntityIndex); var entities = BuildEntityMap(zone); if (observerEntityIndex >= 0) { if (entities.TryGetValue(observerEntityIndex, out var observer)) { selected.Add(observer.EntityIndex); foreach (var contact in observer.Contacts ?? Array.Empty()) { if (contact != null && contact.Visible && contact.InfoGathered > minimumInfoGathered && entities.ContainsKey(contact.TargetEntityIndex)) { selected.Add(contact.TargetEntityIndex); } } } } foreach (var entity in zone.Entities ?? Array.Empty()) { if (entity != null && entity.EntityIndex >= 0 && ContainsPoint(viewport, entity.PositionX, entity.PositionZ)) { selected.Add(entity.EntityIndex); } } foreach (var entityIndex in selected.OrderBy(index => index)) entityIndices.Add(entityIndex); return entityIndices.Count; } public static int[] QueryObjectsViewportEntityIndices( AetheriaRuntimeZoneSnapshotCommit? zone, IReadOnlyList? controlledEntityIndices, double minimumInfoGathered, rect viewport) { var entityIndices = new List(); QueryObjectsViewportEntityIndices(zone, controlledEntityIndices, minimumInfoGathered, viewport, entityIndices); return entityIndices.Count == 0 ? Array.Empty() : entityIndices.ToArray(); } public static int QueryObjectsViewportEntityIndices( AetheriaRuntimeZoneSnapshotCommit? zone, IReadOnlyList? controlledEntityIndices, double minimumInfoGathered, rect viewport, List entityIndices) { if (entityIndices == null) throw new ArgumentNullException(nameof(entityIndices)); entityIndices.Clear(); if (zone == null || controlledEntityIndices == null || controlledEntityIndices.Count == 0) return 0; var entities = BuildEntityMap(zone); var visible = new HashSet(); foreach (var observerEntityIndex in controlledEntityIndices) { if (observerEntityIndex < 0 || !entities.TryGetValue(observerEntityIndex, out var observer)) { continue; } visible.Add(observer.EntityIndex); foreach (var contact in observer.Contacts ?? Array.Empty()) { if (contact != null && contact.Visible && contact.InfoGathered > minimumInfoGathered && entities.ContainsKey(contact.TargetEntityIndex)) { visible.Add(contact.TargetEntityIndex); } } } foreach (var entityIndex in visible.OrderBy(index => index)) { if (entities.TryGetValue(entityIndex, out var entity) && viewport.Contains(new float2((float)entity.PositionX, (float)entity.PositionZ))) { entityIndices.Add(entityIndex); } } return entityIndices.Count; } public static int[] QueryObjectsViewportEntityIndices( AetheriaRuntimeZoneSnapshotCommit? zone, IReadOnlyList? controlledEntityIndices, double minimumInfoGathered, AetheriaRuntimeXzRect unityViewport) { return QueryObjectsViewportEntityIndices( zone, controlledEntityIndices, minimumInfoGathered, ToRuntimeRect(unityViewport)); } public static int QueryObjectsViewportEntityIndices( AetheriaRuntimeZoneSnapshotCommit? zone, IReadOnlyList? controlledEntityIndices, double minimumInfoGathered, AetheriaRuntimeXzRect unityViewport, List entityIndices) { return QueryObjectsViewportEntityIndices( zone, controlledEntityIndices, minimumInfoGathered, ToRuntimeRect(unityViewport), entityIndices); } public static AetheriaRuntimeDaemonObjectViewportEntity[] QueryObjectsViewport( AetheriaRuntimeZoneSnapshotCommit? zone, IReadOnlyList? controlledEntityIndices, double minimumInfoGathered, rect viewport) { var entities = new List(); QueryObjectsViewport(zone, controlledEntityIndices, minimumInfoGathered, viewport, entities); return entities.Count == 0 ? Array.Empty() : entities.ToArray(); } public static int QueryObjectsViewport( AetheriaRuntimeZoneSnapshotCommit? zone, IReadOnlyList? controlledEntityIndices, double minimumInfoGathered, rect viewport, List entities) { if (entities == null) throw new ArgumentNullException(nameof(entities)); entities.Clear(); var entityIndices = new List(); QueryObjectsViewportEntityIndices(zone, controlledEntityIndices, minimumInfoGathered, viewport, entityIndices); if (zone == null || entityIndices.Count == 0) return 0; var entityMap = BuildEntityMap(zone); var controlled = new HashSet(controlledEntityIndices ?? Array.Empty()); foreach (var entityIndex in entityIndices) { if (entityMap.TryGetValue(entityIndex, out var entity)) entities.Add(BuildObjectViewportEntity(entity, controlled.Contains(entityIndex))); } return entities.Count; } public static AetheriaRuntimeDaemonObjectViewportEntity[] QueryObjectsViewport( AetheriaRuntimeZoneSnapshotCommit? zone, IReadOnlyList? controlledEntityIndices, double minimumInfoGathered, AetheriaRuntimeXzRect unityViewport) { return QueryObjectsViewport( zone, controlledEntityIndices, minimumInfoGathered, ToRuntimeRect(unityViewport)); } public static int QueryObjectsViewport( AetheriaRuntimeZoneSnapshotCommit? zone, IReadOnlyList? controlledEntityIndices, double minimumInfoGathered, AetheriaRuntimeXzRect unityViewport, List entities) { return QueryObjectsViewport( zone, controlledEntityIndices, minimumInfoGathered, ToRuntimeRect(unityViewport), entities); } public static bool TryQueryEntityContact( AetheriaRuntimeZoneSnapshotCommit? zone, int observerEntityIndex, int targetEntityIndex, out AetheriaRuntimeDaemonEntityContact entityContact) { entityContact = default; if (zone == null || observerEntityIndex < 0 || targetEntityIndex < 0) return false; var entities = BuildEntityMap(zone); if (!entities.TryGetValue(observerEntityIndex, out var observer) || !entities.TryGetValue(targetEntityIndex, out var target)) { return false; } foreach (var contact in observer.Contacts ?? Array.Empty()) { if (contact == null || contact.TargetEntityIndex != targetEntityIndex) continue; entityContact = BuildEntityContact(observer, target, contact); return true; } return false; } public static AetheriaRuntimeDaemonEntityContact[] QueryEntityContacts( AetheriaRuntimeZoneSnapshotCommit? zone, int observerEntityIndex, double minimumInfoGathered, bool visibleOnly) { var contacts = new List(); QueryEntityContacts(zone, observerEntityIndex, minimumInfoGathered, visibleOnly, contacts); return contacts.Count == 0 ? Array.Empty() : contacts.ToArray(); } public static int QueryEntityContacts( AetheriaRuntimeZoneSnapshotCommit? zone, int observerEntityIndex, double minimumInfoGathered, bool visibleOnly, List contacts) { if (contacts == null) throw new ArgumentNullException(nameof(contacts)); contacts.Clear(); if (zone == null || observerEntityIndex < 0) return 0; var entities = BuildEntityMap(zone); if (!entities.TryGetValue(observerEntityIndex, out var observer)) return 0; foreach (var contact in observer.Contacts ?? Array.Empty()) { if (contact == null || contact.InfoGathered <= minimumInfoGathered || visibleOnly && !contact.Visible || !entities.TryGetValue(contact.TargetEntityIndex, out var target)) { continue; } contacts.Add(BuildEntityContact(observer, target, contact)); } return contacts.Count; } public static bool TryQueryEntityTarget( AetheriaRuntimeZoneSnapshotCommit? zone, int observerEntityIndex, out AetheriaRuntimeDaemonEntityTarget entityTarget) { entityTarget = default; if (zone == null || observerEntityIndex < 0) return false; var entities = BuildEntityMap(zone); if (!entities.TryGetValue(observerEntityIndex, out var observer) || observer.TargetEntityIndex < 0 || !entities.TryGetValue(observer.TargetEntityIndex, out var target)) { return false; } entityTarget = BuildEntityTarget(observer, target); return true; } public static AetheriaRuntimeDaemonWormholeExit[] QueryWormholeExits( AetheriaRuntimeRunCheckpointCommit? run, AetheriaRuntimeZoneSnapshotCommit? zone, double zoneRadius, double wormholeDistanceRatio) { var exits = new List(); QueryWormholeExits(run, zone, zoneRadius, wormholeDistanceRatio, exits); return exits.Count == 0 ? Array.Empty() : exits.ToArray(); } public static int QueryWormholeExits( AetheriaRuntimeRunCheckpointCommit? run, AetheriaRuntimeZoneSnapshotCommit? zone, double zoneRadius, double wormholeDistanceRatio, List exits) { if (exits == null) throw new ArgumentNullException(nameof(exits)); exits.Clear(); if (run == null || zone == null) return 0; var zones = BuildZoneMap(run); var distance = Math.Max(0, zoneRadius) * Math.Max(0, wormholeDistanceRatio); foreach (var targetZoneIndex in zone.AdjacentZoneIndices ?? Array.Empty()) { if (!zones.TryGetValue(targetZoneIndex, out var target)) continue; var deltaX = target.PositionX - zone.PositionX; var deltaZ = target.PositionY - zone.PositionY; var magnitude = Math.Sqrt(deltaX * deltaX + deltaZ * deltaZ); if (magnitude <= 0) continue; var directionX = deltaX / magnitude; var directionZ = deltaZ / magnitude; exits.Add(new AetheriaRuntimeDaemonWormholeExit( target.ZoneIndex, directionX, directionZ, directionX * distance, directionZ * distance)); } return exits.Count; } public static int QueryWormholeExits( AetheriaRuntimeZoneSnapshotCommit? zone, IReadOnlyList? adjacentZones, double zoneRadius, double wormholeDistanceRatio, List exits) { if (exits == null) throw new ArgumentNullException(nameof(exits)); exits.Clear(); if (zone == null || adjacentZones == null || adjacentZones.Count == 0) return 0; var adjacentByIndex = adjacentZones .Where(candidate => candidate != null && candidate.ZoneIndex >= 0) .ToDictionary(candidate => candidate.ZoneIndex); var distance = Math.Max(0, zoneRadius) * Math.Max(0, wormholeDistanceRatio); foreach (var targetZoneIndex in zone.AdjacentZoneIndices ?? Array.Empty()) { if (!adjacentByIndex.TryGetValue(targetZoneIndex, out var target)) continue; var deltaX = target.X - zone.PositionX; var deltaZ = target.Y - zone.PositionY; var magnitude = Math.Sqrt(deltaX * deltaX + deltaZ * deltaZ); if (magnitude <= 0) continue; var directionX = deltaX / magnitude; var directionZ = deltaZ / magnitude; exits.Add(new AetheriaRuntimeDaemonWormholeExit( target.ZoneIndex, directionX, directionZ, directionX * distance, directionZ * distance)); } return exits.Count; } public static AetheriaRuntimeDaemonRenderGroupDocument[] QueryRenderGroups( AetheriaRuntimeDaemonSoaViewIndex? index, double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { var groups = new List(); QueryRenderGroups(index, minX, minY, minZ, maxX, maxY, maxZ, groups); return groups.Count == 0 ? Array.Empty() : groups.ToArray(); } public static int QueryRenderGroups( AetheriaRuntimeDaemonSoaViewIndex? index, double minX, double minY, double minZ, double maxX, double maxY, double maxZ, List groups) { if (groups == null) throw new ArgumentNullException(nameof(groups)); groups.Clear(); if (index == null) return 0; Normalize(ref minX, ref maxX); Normalize(ref minY, ref maxY); Normalize(ref minZ, ref maxZ); foreach (var group in index.RenderGroups) { if (group == null || !IntersectsBounds(group, minX, minY, minZ, maxX, maxY, maxZ)) continue; groups.Add(group); } return groups.Count; } public static double EvaluateGravityTerrainHeight( AetheriaRuntimeZoneSnapshotCommit? zone, double positionX, double positionZ, double simulationTimeSeconds) { if (zone == null) return 0; var height = 0.0; if (zone.GravityTerrainRadius > 0 && zone.GravityTerrainDepth != 0) { var distance = Math.Sqrt(positionX * positionX + positionZ * positionZ); height -= PowerPulse( distance / (zone.GravityTerrainRadius * 2.0), Math.Max(0.0001, zone.GravityTerrainDepthExponent)) * zone.GravityTerrainDepth; } foreach (var brush in QueryGravityInfluences( zone, new rect((float)positionX, (float)positionZ, (float)positionX, (float)positionZ))) { var dx = positionX - brush.CenterX; var dz = positionZ - brush.CenterZ; var distance = Math.Sqrt(dx * dx + dz * dz); if (brush.Radius > 0 && distance < brush.Radius && brush.GravityDepth != 0) { height -= PowerPulse( distance / brush.Radius, Math.Max(0.0001, brush.GravityDepthExponent)) * brush.GravityDepth; } if (brush.WaveRadius > 0 && distance < brush.WaveRadius && brush.WaveDepth != 0) { height -= RadialWaves( distance / brush.WaveRadius, 8.0, 1.25, zone.GravityTerrainWaveFrequency, simulationTimeSeconds * brush.WaveSpeed) * brush.WaveDepth; } } return height; } public static AetheriaRuntimeGravityTerrainBand QueryGravityTerrainBand( AetheriaRuntimeZoneSnapshotCommit? zone, double minimapGravityRange, double maxDepth) { if (zone == null) return new AetheriaRuntimeGravityTerrainBand(0, Math.Max(0, maxDepth)); var startDepth = PowerPulse( minimapGravityRange, Math.Max(0.0001, zone.GravityTerrainDepthExponent)) * zone.GravityTerrainDepth; return new AetheriaRuntimeGravityTerrainBand( startDepth, zone.GravityTerrainDepth - startDepth + maxDepth); } private static Dictionary BuildEntityMap(AetheriaRuntimeZoneSnapshotCommit zone) { var entities = new Dictionary(); foreach (var entity in zone.Entities ?? Array.Empty()) { if (entity != null && entity.EntityIndex >= 0) entities[entity.EntityIndex] = entity; } return entities; } private static AetheriaRuntimeDaemonEntityContact BuildEntityContact( AetheriaRuntimeEntitySnapshotCommit observer, AetheriaRuntimeEntitySnapshotCommit target, AetheriaRuntimeEntityContactCommit contact) { var deltaX = target.PositionX - observer.PositionX; var deltaZ = target.PositionZ - observer.PositionZ; var distance = Math.Sqrt(deltaX * deltaX + deltaZ * deltaZ); return new AetheriaRuntimeDaemonEntityContact( observer.EntityIndex, target.EntityIndex, target.PositionX, target.PositionZ, deltaX, deltaZ, distance, contact.InfoGathered, contact.Hostile, contact.Visible); } private static AetheriaRuntimeDaemonObjectViewportEntity BuildObjectViewportEntity( AetheriaRuntimeEntitySnapshotCommit entity, bool controlled) { var position = new double3(entity.PositionX, entity.PositionY, entity.PositionZ); var xy = new double2(entity.PositionX, entity.PositionZ); var direction = new double2(entity.DirectionX, entity.DirectionY); var velocity = new double2(entity.VelocityX, entity.VelocityY); return new AetheriaRuntimeDaemonObjectViewportEntity( entity.EntityIndex, entity.Name, entity.Name, position, xy, direction, velocity, controlled); } private static AetheriaRuntimeDaemonEntityTarget BuildEntityTarget( AetheriaRuntimeEntitySnapshotCommit observer, AetheriaRuntimeEntitySnapshotCommit target) { var deltaX = target.PositionX - observer.PositionX; var deltaZ = target.PositionZ - observer.PositionZ; var distance = Math.Sqrt(deltaX * deltaX + deltaZ * deltaZ); return new AetheriaRuntimeDaemonEntityTarget( observer.EntityIndex, target.EntityIndex, target.PositionX, target.PositionZ, deltaX, deltaZ, distance); } private static Dictionary BuildZoneMap(AetheriaRuntimeRunCheckpointCommit run) { var zones = new Dictionary(); foreach (var zone in run.Zones ?? Array.Empty()) { if (zone != null && zone.ZoneIndex >= 0) zones[zone.ZoneIndex] = zone; } return zones; } private static Dictionary BuildOrbitPositions(AetheriaRuntimeZoneSnapshotCommit zone) { var source = BuildOrbitMap(zone); var positions = new Dictionary(StringComparer.Ordinal) { [""] = new AetheriaRuntimeXzPoint(0, 0) }; foreach (var orbitKey in source.Keys) ResolveOrbitPosition(orbitKey, source, positions); return positions; } private static Dictionary BuildOrbitMap( AetheriaRuntimeZoneSnapshotCommit zone) { var source = new Dictionary(StringComparer.Ordinal); foreach (var orbit in zone.Orbits ?? Array.Empty()) { if (orbit != null && !string.IsNullOrWhiteSpace(orbit.OrbitKey)) source[orbit.OrbitKey] = orbit; } return source; } private static AetheriaRuntimeXzPoint ResolveOrbitPosition( string orbitKey, Dictionary source, Dictionary positions) { if (positions.TryGetValue(orbitKey ?? "", out var cached)) return cached; if (!source.TryGetValue(orbitKey ?? "", out var orbit)) return new AetheriaRuntimeXzPoint(0, 0); var parent = ResolveOrbitPosition(orbit.ParentOrbitKey ?? "", source, positions); var position = new AetheriaRuntimeXzPoint( parent.x + orbit.FixedPositionX + Math.Cos(orbit.Phase * Math.PI * 2) * orbit.Distance, parent.z + orbit.FixedPositionY + Math.Sin(orbit.Phase * Math.PI * 2) * orbit.Distance); positions[orbitKey ?? ""] = position; return position; } private static bool IntersectsCircle(rect viewport, double centerX, double centerY, double radius) { if (radius <= 0) return false; var nearestX = Clamp(centerX, viewport.min.x, viewport.max.x); var nearestZ = Clamp(centerY, viewport.min.y, viewport.max.y); var dx = centerX - nearestX; var dz = centerY - nearestZ; return dx * dx + dz * dz <= radius * radius; } private static bool IntersectsCircle(AetheriaRuntimeXzRect rect, double centerX, double centerZ, double radius) { return IntersectsCircle(rect.ToAetheriaXyRect(), centerX, centerZ, radius); } private static bool ContainsPoint(AetheriaRuntimeXzRect rect, double x, double z) { return ContainsPoint(rect.ToAetheriaXyRect(), x, z); } private static bool ContainsPoint(rect viewport, double x, double y) { return viewport.Contains(new float2((float)x, (float)y)); } private static rect ToRuntimeRect(AetheriaRuntimeXzRect unityViewport) { return unityViewport.ToAetheriaXyRect(); } private static int TryParseEntityIndex(string? key) { if (string.IsNullOrWhiteSpace(key)) return -1; var marker = ".entity."; var markerIndex = key.LastIndexOf(marker, StringComparison.Ordinal); if (markerIndex < 0) return -1; var start = markerIndex + marker.Length; var end = start; while (end < key.Length && char.IsDigit(key[end])) end++; return int.TryParse(key.Substring(start, end - start), out var value) ? value : -1; } private static bool IntersectsBounds( AetheriaRuntimeDaemonRenderGroupDocument group, double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { var halfX = group.BoundsSizeX * 0.5; var halfY = group.BoundsSizeY * 0.5; var halfZ = group.BoundsSizeZ * 0.5; return group.BoundsCenterX + halfX >= minX && group.BoundsCenterX - halfX <= maxX && group.BoundsCenterY + halfY >= minY && group.BoundsCenterY - halfY <= maxY && group.BoundsCenterZ + halfZ >= minZ && group.BoundsCenterZ - halfZ <= maxZ; } private static AetheriaRuntimeGravityInfluenceKind GravityKind(AetheriaRuntimeBodySnapshotCommit body) { return (body.Kind ?? "").ToLowerInvariant() switch { "sun" => AetheriaRuntimeGravityInfluenceKind.Sun, "gas_giant" => AetheriaRuntimeGravityInfluenceKind.GasGiant, _ => AetheriaRuntimeGravityInfluenceKind.Planet }; } private static bool TryResolveBodyCenter( AetheriaRuntimeBodySnapshotCommit body, Dictionary orbitPositions, out AetheriaRuntimeXzPoint center) { if (IsFinite(body.GravityInfluenceCenterX) && IsFinite(body.GravityInfluenceCenterZ)) { center = new AetheriaRuntimeXzPoint(body.GravityInfluenceCenterX, body.GravityInfluenceCenterZ); return true; } return orbitPositions.TryGetValue(body.OrbitKey ?? "", out center); } private static bool TryResolveAsteroidBeltCenter( AetheriaRuntimeBodySnapshotCommit body, Dictionary orbitPositions, Dictionary orbits, out AetheriaRuntimeXzPoint center) { if (orbits.TryGetValue(body.OrbitKey ?? "", out var orbit) && orbitPositions.TryGetValue(orbit.ParentOrbitKey ?? "", out center)) { return true; } return TryResolveBodyCenter(body, orbitPositions, out center); } private static double ResolveGravityRadius(AetheriaRuntimeBodySnapshotCommit body) { return body.GravityInfluenceRadius > 0 && IsFinite(body.GravityInfluenceRadius) ? body.GravityInfluenceRadius : 0; } private static double ResolveGravityDepth(AetheriaRuntimeBodySnapshotCommit body) { return body.GravityWellDepth; } private static double ResolveWaveRadius(AetheriaRuntimeBodySnapshotCommit body) { return string.Equals(body.Kind, "gas_giant", StringComparison.OrdinalIgnoreCase) || string.Equals(body.Kind, "sun", StringComparison.OrdinalIgnoreCase) ? Math.Max(0, body.GravityWaveRadius) : 0; } private static double ResolveWaveDepth(AetheriaRuntimeBodySnapshotCommit body) { return body.GravityWaveDepth; } private static double ResolveWaveSpeed(AetheriaRuntimeBodySnapshotCommit body) { return body.GravityWaveSpeed; } private static double ResolveAsteroidBeltRadius(AetheriaRuntimeBodySnapshotCommit body) { var radius = 0.0; foreach (var asteroid in body.Asteroids ?? Array.Empty()) { if (asteroid != null && asteroid.Distance > radius) radius = asteroid.Distance; } return radius; } private static double Clamp(double value, double min, double max) { return value < min ? min : value > max ? max : value; } private static double PowerPulse(double x, double exponent) { x *= 2.0; x = Clamp(x, -1.0, 1.0); return Math.Pow((x + 1.0) * (1.0 - x), exponent); } private static double Fraction(double value) { return value - Math.Floor(value); } private static double RadialWaves( double x, double maskExponent, double sineExponent, double frequency, double phase) { return PowerPulse(x, maskExponent) * Math.Cos(Math.Pow(x * 2.0, sineExponent) * frequency + phase); } private static bool IsFinite(double value) { return !double.IsNaN(value) && !double.IsInfinity(value); } private static void Normalize(ref double a, ref double b) { if (a > b) (a, b) = (b, a); } private readonly struct AetheriaRuntimeXzPoint { public AetheriaRuntimeXzPoint(double x, double z) { this.x = x; this.z = z; } public readonly double x; public readonly double z; } } }