2026-06-21 14:52:31 +01:00
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
2026-06-25 18:09:33 +01:00
|
|
|
using System.Linq;
|
|
|
|
|
using CultMath;
|
2026-07-05 15:12:29 +01:00
|
|
|
using MessagePack;
|
2026-06-21 14:52:31 +01:00
|
|
|
|
|
|
|
|
#nullable enable
|
|
|
|
|
|
|
|
|
|
namespace GameCult.Aetheria.State.Verse
|
|
|
|
|
{
|
|
|
|
|
public enum AetheriaRuntimeGravityInfluenceKind
|
|
|
|
|
{
|
|
|
|
|
Planet,
|
|
|
|
|
GasGiant,
|
|
|
|
|
Sun
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-25 18:09:33 +01:00
|
|
|
/// <summary>
|
2026-07-06 01:29:53 +01:00
|
|
|
/// Horizontal X/Z viewport boundary for callers projecting the world into a two-dimensional surface.
|
2026-06-25 18:09:33 +01:00
|
|
|
/// </summary>
|
2026-06-21 14:52:31 +01:00
|
|
|
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; }
|
2026-06-25 18:09:33 +01:00
|
|
|
|
|
|
|
|
public rect ToAetheriaXyRect()
|
|
|
|
|
{
|
|
|
|
|
return new rect(
|
|
|
|
|
(float)MinX,
|
|
|
|
|
(float)MinZ,
|
|
|
|
|
(float)MaxX,
|
|
|
|
|
(float)MaxZ);
|
|
|
|
|
}
|
2026-06-21 14:52:31 +01:00
|
|
|
}
|
|
|
|
|
|
2026-07-05 15:12:29 +01:00
|
|
|
[MessagePackObject(true)]
|
2026-06-21 20:29:03 +01:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-05 15:12:29 +01:00
|
|
|
[MessagePackObject(true)]
|
2026-06-21 20:29:03 +01:00
|
|
|
public readonly struct AetheriaRuntimeDaemonRenderSettings
|
|
|
|
|
{
|
2026-07-05 15:12:29 +01:00
|
|
|
public static AetheriaRuntimeDaemonRenderSettings AetheriaDefault { get; } =
|
|
|
|
|
new AetheriaRuntimeDaemonRenderSettings(
|
2026-07-05 15:23:39 +01:00
|
|
|
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,
|
2026-07-05 15:12:29 +01:00
|
|
|
bodyIconSizeCurve: new AetheriaRuntimeExponentialCurve(0.15, 10.0, 25.0),
|
2026-07-05 15:23:39 +01:00
|
|
|
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));
|
2026-07-05 15:12:29 +01:00
|
|
|
|
2026-06-21 20:29:03 +01:00
|
|
|
public AetheriaRuntimeDaemonRenderSettings(
|
|
|
|
|
AetheriaRuntimeExponentialCurve temperatureEmissionCurve,
|
2026-06-21 23:21:29 +01:00
|
|
|
AetheriaRuntimeExponentialLerp lockIndicatorFrequency,
|
|
|
|
|
AetheriaRuntimeExponentialLerp lockSpinSpeed,
|
2026-06-21 23:17:00 +01:00
|
|
|
double convergenceMinimumDistance,
|
|
|
|
|
double hypothermiaTemperature,
|
2026-06-21 23:21:29 +01:00
|
|
|
double heatstrokeTemperature,
|
|
|
|
|
double severeHeatstrokeRiskThreshold,
|
|
|
|
|
double targetDetectionInfoThreshold,
|
2026-06-21 23:38:05 +01:00
|
|
|
double lockIndicatorNoiseAmplitude,
|
|
|
|
|
double heatstrokePhasingFloor = 0.0,
|
2026-06-21 23:42:01 +01:00
|
|
|
double heatstrokePhasingFrequency = 5.0,
|
|
|
|
|
double targetSpottedBlinkFrequency = 20.0,
|
2026-06-21 23:48:40 +01:00
|
|
|
double targetSpottedBlinkOffset = -0.25,
|
|
|
|
|
IReadOnlyList<double>? minimapZoomLevels = null,
|
2026-06-21 23:54:03 +01:00
|
|
|
int defaultMinimapZoom = 0,
|
2026-06-21 23:59:35 +01:00
|
|
|
double wormholeDistanceRatio = 1.0,
|
|
|
|
|
double defaultViewDistance = 0.0,
|
2026-06-22 00:03:48 +01:00
|
|
|
double minimapIconScale = 0.0,
|
2026-06-22 00:07:34 +01:00
|
|
|
double minimapAsteroidSize = 0.0,
|
2026-06-22 00:11:55 +01:00
|
|
|
AetheriaRuntimeExponentialCurve bodyIconSizeCurve = default,
|
2026-06-22 00:26:49 +01:00
|
|
|
double minimapZoneGravityRange = 0.0,
|
|
|
|
|
double asteroidVerticalOffset = 0.0,
|
|
|
|
|
double planetRotationSpeed = 0.0,
|
|
|
|
|
double zoneBoundaryPower = 0.0,
|
2026-06-22 00:31:17 +01:00
|
|
|
double zoneBoundaryDepth = 0.0,
|
|
|
|
|
int asteroidMeshCount = int.MaxValue,
|
|
|
|
|
AetheriaRuntimeExponentialCurve bodyRadiusCurve = default,
|
|
|
|
|
AetheriaRuntimeExponentialCurve lightRadiusCurve = default,
|
|
|
|
|
AetheriaRuntimeExponentialCurve gravityWaveFrequencyCurve = default)
|
2026-06-21 20:29:03 +01:00
|
|
|
{
|
|
|
|
|
TemperatureEmissionCurve = temperatureEmissionCurve;
|
2026-06-21 23:21:29 +01:00
|
|
|
LockIndicatorFrequency = lockIndicatorFrequency;
|
|
|
|
|
LockSpinSpeed = lockSpinSpeed;
|
2026-06-21 20:29:03 +01:00
|
|
|
ConvergenceMinimumDistance = convergenceMinimumDistance;
|
2026-06-21 23:17:00 +01:00
|
|
|
HypothermiaTemperature = hypothermiaTemperature;
|
|
|
|
|
HeatstrokeTemperature = heatstrokeTemperature;
|
2026-06-21 23:21:29 +01:00
|
|
|
SevereHeatstrokeRiskThreshold = severeHeatstrokeRiskThreshold;
|
|
|
|
|
TargetDetectionInfoThreshold = targetDetectionInfoThreshold;
|
|
|
|
|
LockIndicatorNoiseAmplitude = lockIndicatorNoiseAmplitude;
|
2026-06-21 23:38:05 +01:00
|
|
|
HeatstrokePhasingFloor = heatstrokePhasingFloor;
|
|
|
|
|
HeatstrokePhasingFrequency = heatstrokePhasingFrequency;
|
2026-06-21 23:42:01 +01:00
|
|
|
TargetSpottedBlinkFrequency = targetSpottedBlinkFrequency;
|
|
|
|
|
TargetSpottedBlinkOffset = targetSpottedBlinkOffset;
|
2026-06-21 23:48:40 +01:00
|
|
|
MinimapZoomLevels = CopyPositiveMinimapZoomLevels(minimapZoomLevels);
|
|
|
|
|
DefaultMinimapZoom = defaultMinimapZoom;
|
2026-06-21 23:54:03 +01:00
|
|
|
WormholeDistanceRatio = Math.Max(0.0, wormholeDistanceRatio);
|
2026-06-21 23:59:35 +01:00
|
|
|
DefaultViewDistance = Math.Max(0.0, defaultViewDistance);
|
|
|
|
|
MinimapIconScale = Math.Max(0.0, minimapIconScale);
|
2026-06-22 00:03:48 +01:00
|
|
|
MinimapAsteroidSize = Math.Max(0.0, minimapAsteroidSize);
|
2026-06-22 00:07:34 +01:00
|
|
|
BodyIconSizeCurve = bodyIconSizeCurve;
|
2026-06-22 00:11:55 +01:00
|
|
|
MinimapZoneGravityRange = Math.Max(0.0, minimapZoneGravityRange);
|
2026-06-22 00:26:49 +01:00
|
|
|
AsteroidVerticalOffset = asteroidVerticalOffset;
|
|
|
|
|
PlanetRotationSpeed = planetRotationSpeed;
|
|
|
|
|
ZoneBoundaryPower = zoneBoundaryPower;
|
|
|
|
|
ZoneBoundaryDepth = zoneBoundaryDepth;
|
2026-06-22 00:31:17 +01:00
|
|
|
AsteroidMeshCount = Math.Max(0, asteroidMeshCount);
|
|
|
|
|
BodyRadiusCurve = bodyRadiusCurve;
|
|
|
|
|
LightRadiusCurve = lightRadiusCurve;
|
|
|
|
|
GravityWaveFrequencyCurve = gravityWaveFrequencyCurve;
|
2026-06-21 20:29:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public AetheriaRuntimeExponentialCurve TemperatureEmissionCurve { get; }
|
2026-06-21 23:21:29 +01:00
|
|
|
public AetheriaRuntimeExponentialLerp LockIndicatorFrequency { get; }
|
|
|
|
|
public AetheriaRuntimeExponentialLerp LockSpinSpeed { get; }
|
2026-06-21 20:29:03 +01:00
|
|
|
public double ConvergenceMinimumDistance { get; }
|
2026-06-21 23:17:00 +01:00
|
|
|
public double HypothermiaTemperature { get; }
|
|
|
|
|
public double HeatstrokeTemperature { get; }
|
2026-06-21 23:21:29 +01:00
|
|
|
public double SevereHeatstrokeRiskThreshold { get; }
|
|
|
|
|
public double TargetDetectionInfoThreshold { get; }
|
|
|
|
|
public double LockIndicatorNoiseAmplitude { get; }
|
2026-06-21 23:38:05 +01:00
|
|
|
public double HeatstrokePhasingFloor { get; }
|
|
|
|
|
public double HeatstrokePhasingFrequency { get; }
|
2026-06-21 23:42:01 +01:00
|
|
|
public double TargetSpottedBlinkFrequency { get; }
|
|
|
|
|
public double TargetSpottedBlinkOffset { get; }
|
2026-06-21 23:48:40 +01:00
|
|
|
public IReadOnlyList<double> MinimapZoomLevels { get; }
|
|
|
|
|
public int DefaultMinimapZoom { get; }
|
2026-06-21 23:54:03 +01:00
|
|
|
public double WormholeDistanceRatio { get; }
|
2026-06-21 23:59:35 +01:00
|
|
|
public double DefaultViewDistance { get; }
|
|
|
|
|
public double MinimapIconScale { get; }
|
2026-06-22 00:03:48 +01:00
|
|
|
public double MinimapAsteroidSize { get; }
|
2026-06-22 00:07:34 +01:00
|
|
|
public AetheriaRuntimeExponentialCurve BodyIconSizeCurve { get; }
|
2026-06-22 00:11:55 +01:00
|
|
|
public double MinimapZoneGravityRange { get; }
|
2026-06-22 00:26:49 +01:00
|
|
|
public double AsteroidVerticalOffset { get; }
|
|
|
|
|
public double PlanetRotationSpeed { get; }
|
|
|
|
|
public double ZoneBoundaryPower { get; }
|
|
|
|
|
public double ZoneBoundaryDepth { get; }
|
2026-06-22 00:31:17 +01:00
|
|
|
public int AsteroidMeshCount { get; }
|
|
|
|
|
public AetheriaRuntimeExponentialCurve BodyRadiusCurve { get; }
|
|
|
|
|
public AetheriaRuntimeExponentialCurve LightRadiusCurve { get; }
|
|
|
|
|
public AetheriaRuntimeExponentialCurve GravityWaveFrequencyCurve { get; }
|
2026-06-21 23:48:40 +01:00
|
|
|
|
|
|
|
|
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());
|
|
|
|
|
}
|
2026-06-21 23:17:00 +01:00
|
|
|
|
2026-06-21 23:59:35 +01:00
|
|
|
public double ResolveMinimapIconSize(double minimapDistance)
|
|
|
|
|
{
|
|
|
|
|
return Math.Max(0.0, minimapDistance) * MinimapIconScale;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 00:07:34 +01:00
|
|
|
public double ResolveBodyIconSize(double mass)
|
|
|
|
|
{
|
|
|
|
|
return BodyIconSizeCurve.Evaluate(Math.Max(0.0, mass));
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 00:31:17 +01:00
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 23:17:00 +01:00
|
|
|
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));
|
|
|
|
|
}
|
2026-06-21 23:21:29 +01:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 23:38:05 +01:00
|
|
|
public double ResolveSevereHeatstrokePostWeight(double heatstroke, double timeSeconds)
|
|
|
|
|
{
|
|
|
|
|
var normalized = NormalizeSevereHeatstrokePost(heatstroke);
|
|
|
|
|
return normalized + normalized * (1.0 - normalized) *
|
|
|
|
|
Math.Max(HeatstrokePhasingFloor, Math.Sin(timeSeconds * HeatstrokePhasingFrequency));
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 23:21:29 +01:00
|
|
|
public double NormalizeDetectionProgress(double infoGathered)
|
|
|
|
|
{
|
|
|
|
|
return TargetDetectionInfoThreshold <= 0
|
|
|
|
|
? (infoGathered > 0 ? 1.0 : 0.0)
|
|
|
|
|
: Saturate(infoGathered / TargetDetectionInfoThreshold);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 23:42:01 +01:00
|
|
|
public bool ResolveTargetSpottedFillEnabled(double infoGathered, double timeSeconds)
|
|
|
|
|
{
|
|
|
|
|
return !(infoGathered > TargetDetectionInfoThreshold) ||
|
|
|
|
|
Math.Sin(TargetSpottedBlinkFrequency * timeSeconds) + TargetSpottedBlinkOffset > 0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 23:21:29 +01:00
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 23:24:14 +01:00
|
|
|
public double NormalizeTargetStatusFill(double normalizedValue)
|
|
|
|
|
{
|
|
|
|
|
return Lerp(0.25, 0.75, normalizedValue);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 23:21:29 +01:00
|
|
|
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);
|
|
|
|
|
}
|
2026-06-21 23:48:40 +01:00
|
|
|
|
|
|
|
|
private IReadOnlyList<double> 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<double> CopyPositiveMinimapZoomLevels(IReadOnlyList<double>? levels)
|
|
|
|
|
{
|
|
|
|
|
if (levels == null || levels.Count == 0)
|
|
|
|
|
return DefaultMinimapZoomLevels;
|
|
|
|
|
|
|
|
|
|
var copy = new List<double>(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<double> DefaultMinimapZoomLevels = new[] { 1000.0 };
|
2026-06-21 20:29:03 +01:00
|
|
|
}
|
|
|
|
|
|
2026-06-21 14:52:31 +01:00
|
|
|
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; }
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 15:20:29 +01:00
|
|
|
public readonly struct AetheriaRuntimeDaemonBodyPose
|
|
|
|
|
{
|
|
|
|
|
public AetheriaRuntimeDaemonBodyPose(
|
|
|
|
|
string bodyKey,
|
|
|
|
|
string orbitKey,
|
|
|
|
|
string parentOrbitKey,
|
|
|
|
|
string kind,
|
|
|
|
|
double centerX,
|
|
|
|
|
double centerZ,
|
|
|
|
|
double parentCenterX,
|
2026-06-21 15:25:23 +01:00
|
|
|
double parentCenterZ,
|
|
|
|
|
double gravityWaveSpeed)
|
2026-06-21 15:20:29 +01:00
|
|
|
{
|
|
|
|
|
BodyKey = bodyKey ?? "";
|
|
|
|
|
OrbitKey = orbitKey ?? "";
|
|
|
|
|
ParentOrbitKey = parentOrbitKey ?? "";
|
|
|
|
|
Kind = kind ?? "";
|
|
|
|
|
CenterX = centerX;
|
|
|
|
|
CenterZ = centerZ;
|
|
|
|
|
ParentCenterX = parentCenterX;
|
|
|
|
|
ParentCenterZ = parentCenterZ;
|
2026-06-21 15:25:23 +01:00
|
|
|
GravityWaveSpeed = gravityWaveSpeed;
|
2026-06-21 15:20:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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; }
|
2026-06-21 15:25:23 +01:00
|
|
|
public double GravityWaveSpeed { get; }
|
2026-06-21 15:20:29 +01:00
|
|
|
}
|
|
|
|
|
|
2026-06-21 23:01:34 +01:00
|
|
|
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; }
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 15:30:55 +01:00
|
|
|
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; }
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 15:47:39 +01:00
|
|
|
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; }
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 15:57:11 +01:00
|
|
|
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; }
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 17:30:14 +01:00
|
|
|
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; }
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 17:47:14 +01:00
|
|
|
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; }
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-25 18:09:33 +01:00
|
|
|
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; }
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 16:07:13 +01:00
|
|
|
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; }
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 15:50:22 +01:00
|
|
|
public readonly struct AetheriaRuntimeGravityTerrainBand
|
|
|
|
|
{
|
|
|
|
|
public AetheriaRuntimeGravityTerrainBand(double startDepth, double depthRange)
|
|
|
|
|
{
|
|
|
|
|
StartDepth = startDepth;
|
|
|
|
|
DepthRange = depthRange;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public double StartDepth { get; }
|
|
|
|
|
public double DepthRange { get; }
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 14:52:31 +01:00
|
|
|
public static class AetheriaRuntimeDaemonRenderQueries
|
|
|
|
|
{
|
2026-06-21 16:40:22 +01:00
|
|
|
public static double ResolveZoneRenderRadius(
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
|
|
|
|
double fallbackRadius)
|
|
|
|
|
{
|
|
|
|
|
if (zone != null && zone.GravityTerrainRadius > 0)
|
|
|
|
|
return zone.GravityTerrainRadius;
|
|
|
|
|
|
|
|
|
|
return Math.Max(0, fallbackRadius);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 14:52:31 +01:00
|
|
|
public static AetheriaRuntimeGravityInfluenceBrush[] QueryGravityInfluences(
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
|
|
|
|
AetheriaRuntimeXzRect viewport)
|
2026-06-25 18:09:33 +01:00
|
|
|
{
|
|
|
|
|
return QueryGravityInfluences(zone, viewport.ToAetheriaXyRect());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static AetheriaRuntimeGravityInfluenceBrush[] QueryGravityInfluences(
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
|
|
|
|
rect viewport)
|
2026-06-21 14:52:31 +01:00
|
|
|
{
|
|
|
|
|
var brushes = new List<AetheriaRuntimeGravityInfluenceBrush>();
|
|
|
|
|
QueryGravityInfluences(zone, viewport, brushes);
|
|
|
|
|
return brushes.Count == 0 ? Array.Empty<AetheriaRuntimeGravityInfluenceBrush>() : brushes.ToArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static int QueryGravityInfluences(
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
|
|
|
|
AetheriaRuntimeXzRect viewport,
|
|
|
|
|
List<AetheriaRuntimeGravityInfluenceBrush> brushes)
|
2026-06-25 18:09:33 +01:00
|
|
|
{
|
|
|
|
|
return QueryGravityInfluences(zone, viewport.ToAetheriaXyRect(), brushes);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static int QueryGravityInfluences(
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
|
|
|
|
rect viewport,
|
|
|
|
|
List<AetheriaRuntimeGravityInfluenceBrush> brushes)
|
2026-06-21 14:52:31 +01:00
|
|
|
{
|
|
|
|
|
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<AetheriaRuntimeBodySnapshotCommit>())
|
|
|
|
|
{
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 15:20:29 +01:00
|
|
|
public static AetheriaRuntimeDaemonBodyPose[] QueryBodyPoses(
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone)
|
|
|
|
|
{
|
|
|
|
|
var poses = new List<AetheriaRuntimeDaemonBodyPose>();
|
|
|
|
|
QueryBodyPoses(zone, poses);
|
|
|
|
|
return poses.Count == 0 ? Array.Empty<AetheriaRuntimeDaemonBodyPose>() : poses.ToArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static int QueryBodyPoses(
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
|
|
|
|
List<AetheriaRuntimeDaemonBodyPose> 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<AetheriaRuntimeBodySnapshotCommit>())
|
|
|
|
|
{
|
|
|
|
|
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,
|
2026-06-21 15:25:23 +01:00
|
|
|
parentCenter.z,
|
|
|
|
|
ResolveWaveSpeed(body)));
|
2026-06-21 15:20:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return poses.Count;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 23:01:34 +01:00
|
|
|
public static AetheriaRuntimeDaemonBodyView[] QueryBodyViews(
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone)
|
|
|
|
|
{
|
|
|
|
|
var views = new List<AetheriaRuntimeDaemonBodyView>();
|
|
|
|
|
QueryBodyViews(zone, views);
|
|
|
|
|
return views.Count == 0 ? Array.Empty<AetheriaRuntimeDaemonBodyView>() : views.ToArray();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 14:55:17 +01:00
|
|
|
public static AetheriaRuntimeDaemonBodyView[] QueryBodyViews(
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
|
|
|
|
AetheriaRuntimeXzRect viewport)
|
2026-06-25 18:09:33 +01:00
|
|
|
{
|
|
|
|
|
return QueryBodyViews(zone, viewport.ToAetheriaXyRect());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static AetheriaRuntimeDaemonBodyView[] QueryBodyViews(
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
|
|
|
|
rect viewport)
|
2026-06-22 14:55:17 +01:00
|
|
|
{
|
|
|
|
|
var views = new List<AetheriaRuntimeDaemonBodyView>();
|
|
|
|
|
QueryBodyViews(zone, viewport, views);
|
|
|
|
|
return views.Count == 0 ? Array.Empty<AetheriaRuntimeDaemonBodyView>() : views.ToArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static int QueryBodyViews(
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
|
|
|
|
List<AetheriaRuntimeDaemonBodyView> views)
|
|
|
|
|
{
|
|
|
|
|
return QueryBodyViews(zone, null, views);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 23:01:34 +01:00
|
|
|
public static int QueryBodyViews(
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
2026-06-22 14:55:17 +01:00
|
|
|
AetheriaRuntimeXzRect viewport,
|
|
|
|
|
List<AetheriaRuntimeDaemonBodyView> views)
|
|
|
|
|
{
|
2026-06-25 18:09:33 +01:00
|
|
|
return QueryBodyViews(zone, viewport.ToAetheriaXyRect(), views);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static int QueryBodyViews(
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
|
|
|
|
rect viewport,
|
|
|
|
|
List<AetheriaRuntimeDaemonBodyView> views)
|
|
|
|
|
{
|
|
|
|
|
return QueryBodyViews(zone, (rect?)viewport, views);
|
2026-06-22 14:55:17 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static int QueryBodyViews(
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
2026-06-25 18:09:33 +01:00
|
|
|
rect? viewport,
|
2026-06-21 23:01:34 +01:00
|
|
|
List<AetheriaRuntimeDaemonBodyView> 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<AetheriaRuntimeBodySnapshotCommit>())
|
|
|
|
|
{
|
|
|
|
|
if (body == null || !TryResolveBodyCenter(body, orbitPositions, out var center))
|
|
|
|
|
continue;
|
|
|
|
|
|
2026-06-22 14:55:17 +01:00
|
|
|
if (viewport.HasValue &&
|
|
|
|
|
!IntersectsCircle(
|
|
|
|
|
viewport.Value,
|
|
|
|
|
center.x,
|
|
|
|
|
center.z,
|
|
|
|
|
Math.Max(ResolveGravityRadius(body), ResolveWaveRadius(body))))
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 23:01:34 +01:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 15:30:55 +01:00
|
|
|
public static AetheriaRuntimeDaemonAsteroidBeltPose[] QueryAsteroidBeltPoses(
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone)
|
|
|
|
|
{
|
|
|
|
|
var poses = new List<AetheriaRuntimeDaemonAsteroidBeltPose>();
|
|
|
|
|
QueryAsteroidBeltPoses(zone, poses);
|
|
|
|
|
return poses.Count == 0 ? Array.Empty<AetheriaRuntimeDaemonAsteroidBeltPose>() : poses.ToArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static int QueryAsteroidBeltPoses(
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
|
|
|
|
List<AetheriaRuntimeDaemonAsteroidBeltPose> poses)
|
|
|
|
|
{
|
|
|
|
|
if (poses == null) throw new ArgumentNullException(nameof(poses));
|
|
|
|
|
poses.Clear();
|
|
|
|
|
if (zone == null)
|
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
|
|
var orbitPositions = BuildOrbitPositions(zone);
|
2026-06-21 15:47:39 +01:00
|
|
|
var orbits = BuildOrbitMap(zone);
|
2026-06-21 15:30:55 +01:00
|
|
|
foreach (var body in zone.Bodies ?? Array.Empty<AetheriaRuntimeBodySnapshotCommit>())
|
|
|
|
|
{
|
|
|
|
|
if (body == null ||
|
|
|
|
|
!string.Equals(body.Kind, "asteroid_belt", StringComparison.OrdinalIgnoreCase) ||
|
2026-06-21 15:47:39 +01:00
|
|
|
!TryResolveAsteroidBeltCenter(body, orbitPositions, orbits, out var center))
|
2026-06-21 15:30:55 +01:00
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
poses.Add(new AetheriaRuntimeDaemonAsteroidBeltPose(
|
|
|
|
|
body.BodyKey,
|
|
|
|
|
body.OrbitKey,
|
|
|
|
|
center.x,
|
|
|
|
|
center.z,
|
|
|
|
|
ResolveAsteroidBeltRadius(body),
|
|
|
|
|
body.Asteroids?.Count ?? 0));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return poses.Count;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 15:47:39 +01:00
|
|
|
public static AetheriaRuntimeDaemonAsteroidInstancePose[] QueryAsteroidInstancePoses(
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
|
|
|
|
string bodyKey,
|
|
|
|
|
double simulationTimeSeconds)
|
|
|
|
|
{
|
|
|
|
|
var poses = new List<AetheriaRuntimeDaemonAsteroidInstancePose>();
|
|
|
|
|
QueryAsteroidInstancePoses(zone, bodyKey, simulationTimeSeconds, poses);
|
|
|
|
|
return poses.Count == 0 ? Array.Empty<AetheriaRuntimeDaemonAsteroidInstancePose>() : poses.ToArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static int QueryAsteroidInstancePoses(
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
|
|
|
|
string bodyKey,
|
|
|
|
|
double simulationTimeSeconds,
|
|
|
|
|
List<AetheriaRuntimeDaemonAsteroidInstancePose> 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<AetheriaRuntimeBodySnapshotCommit>())
|
|
|
|
|
{
|
|
|
|
|
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<AetheriaRuntimeAsteroidCommit>();
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 15:57:11 +01:00
|
|
|
public static AetheriaRuntimeDaemonCompassMarker[] QueryCompassMarkers(
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
|
|
|
|
int observerEntityIndex,
|
|
|
|
|
double minimumInfoGathered,
|
|
|
|
|
double minimumDistance)
|
|
|
|
|
{
|
|
|
|
|
var markers = new List<AetheriaRuntimeDaemonCompassMarker>();
|
|
|
|
|
QueryCompassMarkers(zone, observerEntityIndex, minimumInfoGathered, minimumDistance, markers);
|
|
|
|
|
return markers.Count == 0 ? Array.Empty<AetheriaRuntimeDaemonCompassMarker>() : markers.ToArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static int QueryCompassMarkers(
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
|
|
|
|
int observerEntityIndex,
|
|
|
|
|
double minimumInfoGathered,
|
|
|
|
|
double minimumDistance,
|
|
|
|
|
List<AetheriaRuntimeDaemonCompassMarker> 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<AetheriaRuntimeEntityContactCommit>())
|
|
|
|
|
{
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 16:02:03 +01:00
|
|
|
public static int[] QueryVisibleEntityIndices(
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
|
|
|
|
int observerEntityIndex,
|
|
|
|
|
double minimumInfoGathered)
|
|
|
|
|
{
|
|
|
|
|
var entityIndices = new List<int>();
|
|
|
|
|
QueryVisibleEntityIndices(zone, observerEntityIndex, minimumInfoGathered, entityIndices);
|
|
|
|
|
return entityIndices.Count == 0 ? Array.Empty<int>() : entityIndices.ToArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static int QueryVisibleEntityIndices(
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
|
|
|
|
int observerEntityIndex,
|
|
|
|
|
double minimumInfoGathered,
|
|
|
|
|
List<int> 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<AetheriaRuntimeEntityContactCommit>())
|
|
|
|
|
{
|
|
|
|
|
if (contact != null &&
|
|
|
|
|
contact.Visible &&
|
|
|
|
|
contact.InfoGathered > minimumInfoGathered &&
|
|
|
|
|
entities.ContainsKey(contact.TargetEntityIndex))
|
|
|
|
|
{
|
|
|
|
|
entityIndices.Add(contact.TargetEntityIndex);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return entityIndices.Count;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 15:04:12 +01:00
|
|
|
public static int[] QueryPresentationEntityIndices(
|
|
|
|
|
AetheriaRuntimeRunCheckpointCommit? run,
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
|
|
|
|
int observerEntityIndex,
|
|
|
|
|
double minimumInfoGathered,
|
|
|
|
|
AetheriaRuntimeXzRect viewport)
|
2026-06-25 18:09:33 +01:00
|
|
|
{
|
|
|
|
|
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<int> 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<int> 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)
|
2026-06-22 15:04:12 +01:00
|
|
|
{
|
|
|
|
|
var entityIndices = new List<int>();
|
2026-06-25 18:09:33 +01:00
|
|
|
var currentEntityIndex = TryParseEntityIndex(currentEntityKey);
|
|
|
|
|
QueryPresentationEntityIndices(
|
|
|
|
|
currentEntityIndex,
|
|
|
|
|
zone,
|
|
|
|
|
observerEntityIndex,
|
|
|
|
|
minimumInfoGathered,
|
|
|
|
|
viewport,
|
|
|
|
|
entityIndices);
|
|
|
|
|
return entityIndices.Count == 0 ? Array.Empty<int>() : entityIndices.ToArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static int[] QueryPresentationEntityIndices(
|
|
|
|
|
int currentEntityIndex,
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
|
|
|
|
int observerEntityIndex,
|
|
|
|
|
double minimumInfoGathered,
|
|
|
|
|
rect viewport)
|
|
|
|
|
{
|
|
|
|
|
var entityIndices = new List<int>();
|
|
|
|
|
QueryPresentationEntityIndices(
|
|
|
|
|
currentEntityIndex,
|
|
|
|
|
zone,
|
|
|
|
|
observerEntityIndex,
|
|
|
|
|
minimumInfoGathered,
|
|
|
|
|
viewport,
|
|
|
|
|
entityIndices);
|
2026-06-22 15:04:12 +01:00
|
|
|
return entityIndices.Count == 0 ? Array.Empty<int>() : entityIndices.ToArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static int QueryPresentationEntityIndices(
|
2026-06-25 18:09:33 +01:00
|
|
|
string? currentEntityKey,
|
2026-06-22 15:04:12 +01:00
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
|
|
|
|
int observerEntityIndex,
|
|
|
|
|
double minimumInfoGathered,
|
|
|
|
|
AetheriaRuntimeXzRect viewport,
|
|
|
|
|
List<int> entityIndices)
|
2026-06-25 18:09:33 +01:00
|
|
|
{
|
|
|
|
|
return QueryPresentationEntityIndices(currentEntityKey, zone, observerEntityIndex, minimumInfoGathered, viewport.ToAetheriaXyRect(), entityIndices);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static int QueryPresentationEntityIndices(
|
|
|
|
|
string? currentEntityKey,
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
|
|
|
|
int observerEntityIndex,
|
|
|
|
|
double minimumInfoGathered,
|
|
|
|
|
rect viewport,
|
|
|
|
|
List<int> 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<int> entityIndices)
|
2026-06-22 15:04:12 +01:00
|
|
|
{
|
|
|
|
|
if (entityIndices == null) throw new ArgumentNullException(nameof(entityIndices));
|
|
|
|
|
entityIndices.Clear();
|
|
|
|
|
if (zone == null)
|
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
|
|
var selected = new HashSet<int>();
|
|
|
|
|
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<AetheriaRuntimeEntityContactCommit>())
|
|
|
|
|
{
|
|
|
|
|
if (contact != null &&
|
|
|
|
|
contact.Visible &&
|
|
|
|
|
contact.InfoGathered > minimumInfoGathered &&
|
|
|
|
|
entities.ContainsKey(contact.TargetEntityIndex))
|
|
|
|
|
{
|
|
|
|
|
selected.Add(contact.TargetEntityIndex);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (var entity in zone.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>())
|
|
|
|
|
{
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-25 18:09:33 +01:00
|
|
|
public static int[] QueryObjectsViewportEntityIndices(
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
|
|
|
|
IReadOnlyList<int>? controlledEntityIndices,
|
|
|
|
|
double minimumInfoGathered,
|
|
|
|
|
rect viewport)
|
|
|
|
|
{
|
|
|
|
|
var entityIndices = new List<int>();
|
|
|
|
|
QueryObjectsViewportEntityIndices(zone, controlledEntityIndices, minimumInfoGathered, viewport, entityIndices);
|
|
|
|
|
return entityIndices.Count == 0 ? Array.Empty<int>() : entityIndices.ToArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static int QueryObjectsViewportEntityIndices(
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
|
|
|
|
IReadOnlyList<int>? controlledEntityIndices,
|
|
|
|
|
double minimumInfoGathered,
|
|
|
|
|
rect viewport,
|
|
|
|
|
List<int> 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<int>();
|
|
|
|
|
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<AetheriaRuntimeEntityContactCommit>())
|
|
|
|
|
{
|
|
|
|
|
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<int>? controlledEntityIndices,
|
|
|
|
|
double minimumInfoGathered,
|
|
|
|
|
AetheriaRuntimeXzRect unityViewport)
|
|
|
|
|
{
|
|
|
|
|
return QueryObjectsViewportEntityIndices(
|
|
|
|
|
zone,
|
|
|
|
|
controlledEntityIndices,
|
|
|
|
|
minimumInfoGathered,
|
|
|
|
|
ToRuntimeRect(unityViewport));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static int QueryObjectsViewportEntityIndices(
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
|
|
|
|
IReadOnlyList<int>? controlledEntityIndices,
|
|
|
|
|
double minimumInfoGathered,
|
|
|
|
|
AetheriaRuntimeXzRect unityViewport,
|
|
|
|
|
List<int> entityIndices)
|
|
|
|
|
{
|
|
|
|
|
return QueryObjectsViewportEntityIndices(
|
|
|
|
|
zone,
|
|
|
|
|
controlledEntityIndices,
|
|
|
|
|
minimumInfoGathered,
|
|
|
|
|
ToRuntimeRect(unityViewport),
|
|
|
|
|
entityIndices);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static AetheriaRuntimeDaemonObjectViewportEntity[] QueryObjectsViewport(
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
|
|
|
|
IReadOnlyList<int>? controlledEntityIndices,
|
|
|
|
|
double minimumInfoGathered,
|
|
|
|
|
rect viewport)
|
|
|
|
|
{
|
|
|
|
|
var entities = new List<AetheriaRuntimeDaemonObjectViewportEntity>();
|
|
|
|
|
QueryObjectsViewport(zone, controlledEntityIndices, minimumInfoGathered, viewport, entities);
|
|
|
|
|
return entities.Count == 0 ? Array.Empty<AetheriaRuntimeDaemonObjectViewportEntity>() : entities.ToArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static int QueryObjectsViewport(
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
|
|
|
|
IReadOnlyList<int>? controlledEntityIndices,
|
|
|
|
|
double minimumInfoGathered,
|
|
|
|
|
rect viewport,
|
|
|
|
|
List<AetheriaRuntimeDaemonObjectViewportEntity> entities)
|
|
|
|
|
{
|
|
|
|
|
if (entities == null) throw new ArgumentNullException(nameof(entities));
|
|
|
|
|
entities.Clear();
|
|
|
|
|
|
|
|
|
|
var entityIndices = new List<int>();
|
|
|
|
|
QueryObjectsViewportEntityIndices(zone, controlledEntityIndices, minimumInfoGathered, viewport, entityIndices);
|
|
|
|
|
if (zone == null || entityIndices.Count == 0)
|
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
|
|
var entityMap = BuildEntityMap(zone);
|
|
|
|
|
var controlled = new HashSet<int>(controlledEntityIndices ?? Array.Empty<int>());
|
|
|
|
|
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<int>? controlledEntityIndices,
|
|
|
|
|
double minimumInfoGathered,
|
|
|
|
|
AetheriaRuntimeXzRect unityViewport)
|
|
|
|
|
{
|
|
|
|
|
return QueryObjectsViewport(
|
|
|
|
|
zone,
|
|
|
|
|
controlledEntityIndices,
|
|
|
|
|
minimumInfoGathered,
|
|
|
|
|
ToRuntimeRect(unityViewport));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static int QueryObjectsViewport(
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
|
|
|
|
IReadOnlyList<int>? controlledEntityIndices,
|
|
|
|
|
double minimumInfoGathered,
|
|
|
|
|
AetheriaRuntimeXzRect unityViewport,
|
|
|
|
|
List<AetheriaRuntimeDaemonObjectViewportEntity> entities)
|
|
|
|
|
{
|
|
|
|
|
return QueryObjectsViewport(
|
|
|
|
|
zone,
|
|
|
|
|
controlledEntityIndices,
|
|
|
|
|
minimumInfoGathered,
|
|
|
|
|
ToRuntimeRect(unityViewport),
|
|
|
|
|
entities);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 17:30:14 +01:00
|
|
|
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<AetheriaRuntimeEntityContactCommit>())
|
|
|
|
|
{
|
|
|
|
|
if (contact == null || contact.TargetEntityIndex != targetEntityIndex)
|
|
|
|
|
continue;
|
|
|
|
|
|
2026-06-21 17:40:05 +01:00
|
|
|
entityContact = BuildEntityContact(observer, target, contact);
|
2026-06-21 17:30:14 +01:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 17:40:05 +01:00
|
|
|
public static AetheriaRuntimeDaemonEntityContact[] QueryEntityContacts(
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
|
|
|
|
int observerEntityIndex,
|
|
|
|
|
double minimumInfoGathered,
|
|
|
|
|
bool visibleOnly)
|
|
|
|
|
{
|
|
|
|
|
var contacts = new List<AetheriaRuntimeDaemonEntityContact>();
|
|
|
|
|
QueryEntityContacts(zone, observerEntityIndex, minimumInfoGathered, visibleOnly, contacts);
|
|
|
|
|
return contacts.Count == 0 ? Array.Empty<AetheriaRuntimeDaemonEntityContact>() : contacts.ToArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static int QueryEntityContacts(
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
|
|
|
|
int observerEntityIndex,
|
|
|
|
|
double minimumInfoGathered,
|
|
|
|
|
bool visibleOnly,
|
|
|
|
|
List<AetheriaRuntimeDaemonEntityContact> 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<AetheriaRuntimeEntityContactCommit>())
|
|
|
|
|
{
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 17:47:14 +01:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 16:07:13 +01:00
|
|
|
public static AetheriaRuntimeDaemonWormholeExit[] QueryWormholeExits(
|
|
|
|
|
AetheriaRuntimeRunCheckpointCommit? run,
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
|
|
|
|
double zoneRadius,
|
|
|
|
|
double wormholeDistanceRatio)
|
|
|
|
|
{
|
|
|
|
|
var exits = new List<AetheriaRuntimeDaemonWormholeExit>();
|
|
|
|
|
QueryWormholeExits(run, zone, zoneRadius, wormholeDistanceRatio, exits);
|
|
|
|
|
return exits.Count == 0 ? Array.Empty<AetheriaRuntimeDaemonWormholeExit>() : exits.ToArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static int QueryWormholeExits(
|
|
|
|
|
AetheriaRuntimeRunCheckpointCommit? run,
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
|
|
|
|
double zoneRadius,
|
|
|
|
|
double wormholeDistanceRatio,
|
|
|
|
|
List<AetheriaRuntimeDaemonWormholeExit> 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<int>())
|
|
|
|
|
{
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-25 18:09:33 +01:00
|
|
|
public static int QueryWormholeExits(
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit? zone,
|
|
|
|
|
IReadOnlyList<AetheriaRuntimeZoneRenderAdjacentZone>? adjacentZones,
|
|
|
|
|
double zoneRadius,
|
|
|
|
|
double wormholeDistanceRatio,
|
|
|
|
|
List<AetheriaRuntimeDaemonWormholeExit> 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<int>())
|
|
|
|
|
{
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 14:52:31 +01:00
|
|
|
public static AetheriaRuntimeDaemonRenderGroupDocument[] QueryRenderGroups(
|
|
|
|
|
AetheriaRuntimeDaemonSoaViewIndex? index,
|
|
|
|
|
double minX,
|
|
|
|
|
double minY,
|
|
|
|
|
double minZ,
|
|
|
|
|
double maxX,
|
|
|
|
|
double maxY,
|
|
|
|
|
double maxZ)
|
|
|
|
|
{
|
|
|
|
|
var groups = new List<AetheriaRuntimeDaemonRenderGroupDocument>();
|
|
|
|
|
QueryRenderGroups(index, minX, minY, minZ, maxX, maxY, maxZ, groups);
|
|
|
|
|
return groups.Count == 0 ? Array.Empty<AetheriaRuntimeDaemonRenderGroupDocument>() : groups.ToArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static int QueryRenderGroups(
|
|
|
|
|
AetheriaRuntimeDaemonSoaViewIndex? index,
|
|
|
|
|
double minX,
|
|
|
|
|
double minY,
|
|
|
|
|
double minZ,
|
|
|
|
|
double maxX,
|
|
|
|
|
double maxY,
|
|
|
|
|
double maxZ,
|
|
|
|
|
List<AetheriaRuntimeDaemonRenderGroupDocument> 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 15:10:54 +01:00
|
|
|
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,
|
2026-06-25 18:09:33 +01:00
|
|
|
new rect((float)positionX, (float)positionZ, (float)positionX, (float)positionZ)))
|
2026-06-21 15:10:54 +01:00
|
|
|
{
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 15:50:22 +01:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 15:57:11 +01:00
|
|
|
private static Dictionary<int, AetheriaRuntimeEntitySnapshotCommit> BuildEntityMap(AetheriaRuntimeZoneSnapshotCommit zone)
|
|
|
|
|
{
|
|
|
|
|
var entities = new Dictionary<int, AetheriaRuntimeEntitySnapshotCommit>();
|
|
|
|
|
foreach (var entity in zone.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>())
|
|
|
|
|
{
|
|
|
|
|
if (entity != null && entity.EntityIndex >= 0)
|
|
|
|
|
entities[entity.EntityIndex] = entity;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return entities;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 17:40:05 +01:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-25 18:09:33 +01:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 17:47:14 +01:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 16:07:13 +01:00
|
|
|
private static Dictionary<int, AetheriaRuntimeZoneSnapshotCommit> BuildZoneMap(AetheriaRuntimeRunCheckpointCommit run)
|
|
|
|
|
{
|
|
|
|
|
var zones = new Dictionary<int, AetheriaRuntimeZoneSnapshotCommit>();
|
|
|
|
|
foreach (var zone in run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
|
|
|
|
|
{
|
|
|
|
|
if (zone != null && zone.ZoneIndex >= 0)
|
|
|
|
|
zones[zone.ZoneIndex] = zone;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return zones;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 14:52:31 +01:00
|
|
|
private static Dictionary<string, AetheriaRuntimeXzPoint> BuildOrbitPositions(AetheriaRuntimeZoneSnapshotCommit zone)
|
|
|
|
|
{
|
2026-06-21 15:20:29 +01:00
|
|
|
var source = BuildOrbitMap(zone);
|
2026-06-21 14:52:31 +01:00
|
|
|
|
|
|
|
|
var positions = new Dictionary<string, AetheriaRuntimeXzPoint>(StringComparer.Ordinal)
|
|
|
|
|
{
|
|
|
|
|
[""] = new AetheriaRuntimeXzPoint(0, 0)
|
|
|
|
|
};
|
|
|
|
|
foreach (var orbitKey in source.Keys)
|
|
|
|
|
ResolveOrbitPosition(orbitKey, source, positions);
|
|
|
|
|
|
|
|
|
|
return positions;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 15:20:29 +01:00
|
|
|
private static Dictionary<string, AetheriaRuntimeOrbitSnapshotCommit> BuildOrbitMap(
|
|
|
|
|
AetheriaRuntimeZoneSnapshotCommit zone)
|
|
|
|
|
{
|
|
|
|
|
var source = new Dictionary<string, AetheriaRuntimeOrbitSnapshotCommit>(StringComparer.Ordinal);
|
|
|
|
|
foreach (var orbit in zone.Orbits ?? Array.Empty<AetheriaRuntimeOrbitSnapshotCommit>())
|
|
|
|
|
{
|
|
|
|
|
if (orbit != null && !string.IsNullOrWhiteSpace(orbit.OrbitKey))
|
|
|
|
|
source[orbit.OrbitKey] = orbit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return source;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 14:52:31 +01:00
|
|
|
private static AetheriaRuntimeXzPoint ResolveOrbitPosition(
|
|
|
|
|
string orbitKey,
|
|
|
|
|
Dictionary<string, AetheriaRuntimeOrbitSnapshotCommit> source,
|
|
|
|
|
Dictionary<string, AetheriaRuntimeXzPoint> 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-25 18:09:33 +01:00
|
|
|
private static bool IntersectsCircle(rect viewport, double centerX, double centerY, double radius)
|
2026-06-21 14:52:31 +01:00
|
|
|
{
|
|
|
|
|
if (radius <= 0)
|
|
|
|
|
return false;
|
|
|
|
|
|
2026-06-25 18:09:33 +01:00
|
|
|
var nearestX = Clamp(centerX, viewport.min.x, viewport.max.x);
|
|
|
|
|
var nearestZ = Clamp(centerY, viewport.min.y, viewport.max.y);
|
2026-06-21 14:52:31 +01:00
|
|
|
var dx = centerX - nearestX;
|
2026-06-25 18:09:33 +01:00
|
|
|
var dz = centerY - nearestZ;
|
2026-06-21 14:52:31 +01:00
|
|
|
return dx * dx + dz * dz <= radius * radius;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-25 18:09:33 +01:00
|
|
|
private static bool IntersectsCircle(AetheriaRuntimeXzRect rect, double centerX, double centerZ, double radius)
|
|
|
|
|
{
|
|
|
|
|
return IntersectsCircle(rect.ToAetheriaXyRect(), centerX, centerZ, radius);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 15:04:12 +01:00
|
|
|
private static bool ContainsPoint(AetheriaRuntimeXzRect rect, double x, double z)
|
|
|
|
|
{
|
2026-06-25 18:09:33 +01:00
|
|
|
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();
|
2026-06-22 15:04:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 14:52:31 +01:00
|
|
|
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<string, AetheriaRuntimeXzPoint> 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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 15:47:39 +01:00
|
|
|
private static bool TryResolveAsteroidBeltCenter(
|
|
|
|
|
AetheriaRuntimeBodySnapshotCommit body,
|
|
|
|
|
Dictionary<string, AetheriaRuntimeXzPoint> orbitPositions,
|
|
|
|
|
Dictionary<string, AetheriaRuntimeOrbitSnapshotCommit> 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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 14:52:31 +01:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 15:30:55 +01:00
|
|
|
private static double ResolveAsteroidBeltRadius(AetheriaRuntimeBodySnapshotCommit body)
|
|
|
|
|
{
|
|
|
|
|
var radius = 0.0;
|
|
|
|
|
foreach (var asteroid in body.Asteroids ?? Array.Empty<AetheriaRuntimeAsteroidCommit>())
|
|
|
|
|
{
|
|
|
|
|
if (asteroid != null && asteroid.Distance > radius)
|
|
|
|
|
radius = asteroid.Distance;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return radius;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 14:52:31 +01:00
|
|
|
private static double Clamp(double value, double min, double max)
|
|
|
|
|
{
|
|
|
|
|
return value < min ? min : value > max ? max : value;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 15:10:54 +01:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 15:47:39 +01:00
|
|
|
private static double Fraction(double value)
|
|
|
|
|
{
|
|
|
|
|
return value - Math.Floor(value);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 15:10:54 +01:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 14:52:31 +01:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|