Move Aetheria simulation tuning into daemon authority

This commit is contained in:
Meta Vens 2026-07-05 19:25:47 +01:00
commit 70607384fc
12 changed files with 449 additions and 121 deletions

View file

@ -264,7 +264,8 @@ export type AetheriaRuntimeDaemonFrameDocumentSlot =
| "duplicateImportedFactIds"
| "cumulativeImportedFactIds"
| "cumulativeRejectedImportedFactIds"
| "renderSettings";
| "renderSettings"
| "simulationSettings";
export const aetheriaRuntimeDaemonFrameDocumentSlots: Record<AetheriaRuntimeDaemonFrameDocumentSlot, number> = {
schema: 0,
@ -289,6 +290,7 @@ export const aetheriaRuntimeDaemonFrameDocumentSlots: Record<AetheriaRuntimeDaem
cumulativeImportedFactIds: 19,
cumulativeRejectedImportedFactIds: 20,
renderSettings: 21,
simulationSettings: 22,
};
export type AetheriaRuntimeDaemonHealthDocumentSlot =

View file

@ -93,9 +93,9 @@ app.whenReady().then(async () => {
rtsVerseId,
"--daemon-id",
rtsDaemonId,
"--rts-cultmesh-port",
"--client-cultmesh-port",
rtsCultMeshPort.toString(),
"--rts-cultmesh-advertise-host",
"--client-cultmesh-advertise-host",
rtsCultMeshAdvertiseHost,
"--tick-interval-ms",
"20",
@ -536,7 +536,7 @@ function stringOr(value: unknown, fallback: string): string {
function requireClient(): AetheriaCultMeshClient {
if (!rtsClient)
throw new Error("Aetheria RTS CultMesh client is not initialized.");
throw new Error("Aetheria CultMesh client is not initialized.");
return rtsClient;
}

View file

@ -21,7 +21,7 @@ try {
$daemonOutput = dotnet run --project (Join-Path $repoRoot "Aetheria.State.Daemon\Aetheria.State.Daemon.csproj") -- `
--state $statePath `
--once `
--rts-cultmesh-port 0 2>&1
--client-cultmesh-port 0 2>&1
}
finally {
$ErrorActionPreference = $previousErrorActionPreference
@ -32,12 +32,12 @@ try {
}
foreach ($line in $daemonOutput) {
if ($line -match "Aetheria RTS CultMesh endpoint: (rudp://127\.0\.0\.1:\d+)") {
if ($line -match "Aetheria client CultMesh endpoint: (rudp://127\.0\.0\.1:\d+)") {
$endpoint = $Matches[1]
}
}
if ([string]::IsNullOrWhiteSpace($endpoint)) {
Write-Error "Stage 7C verifier failed: one-shot daemon did not report an RTS CultMesh endpoint."
Write-Error "Stage 7C verifier failed: one-shot daemon did not report a client CultMesh endpoint."
}
$smokeScript = Join-Path $tempRoot "stage7c-smoke.mjs"

View file

@ -33,9 +33,9 @@ try {
"--",
"--state",
$statePath,
"--rts-cultmesh-host",
"--client-cultmesh-host",
"127.0.0.1",
"--rts-cultmesh-port",
"--client-cultmesh-port",
$port,
"--tick-interval-ms",
"20",

View file

@ -38,22 +38,22 @@ discoveryHost.Update(verseHost);
await PublishRuntimeSessionAsync(node, options, startedAtUtc, "starting").ConfigureAwait(false);
await PublishStateSurfacesAsync(node, options, startedAtUtc).ConfigureAwait(false);
var latestFrame = await node.MutableDocument<AetheriaRuntimeDaemonFrameDocument>(AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest).ReadAsync().ConfigureAwait(false);
using var cultMeshRudpHost = StartRtsCultMeshHost(node, options, () => latestFrame);
using var rtsPumpCancellation = new CancellationTokenSource();
var rtsPump = RunRtsCultMeshPumpAsync(cultMeshRudpHost, rtsPumpCancellation.Token);
using var cultMeshRudpHost = StartClientCultMeshHost(node, options, () => latestFrame);
using var clientPumpCancellation = new CancellationTokenSource();
var clientPump = RunClientCultMeshPumpAsync(cultMeshRudpHost, clientPumpCancellation.Token);
var nextApiPublicationUtc = DateTimeOffset.UtcNow;
var firstTick = await TickAsync(node, options, latestFrame, buildPublications: true).ConfigureAwait(false);
ThrowIfRtsPumpFaulted(rtsPump);
ThrowIfClientPumpFaulted(clientPump);
latestFrame = firstTick.Frame;
nextApiPublicationUtc = DateTimeOffset.UtcNow.Add(options.ApiPublicationInterval);
Console.WriteLine($"Aetheria Verse daemon published frame {firstTick.Frame.FrameId}.");
Console.WriteLine($"Aetheria RTS CultMesh endpoint: rudp://{options.RtsCultMeshAdvertiseHost}:{cultMeshRudpHost.LocalEndPoint.Port}");
Console.WriteLine($"Aetheria client CultMesh endpoint: rudp://{options.ClientCultMeshAdvertiseHost}:{cultMeshRudpHost.LocalEndPoint.Port}");
if (options.Once)
{
await PublishRuntimeSessionAsync(node, options, startedAtUtc, "completed").ConfigureAwait(false);
rtsPumpCancellation.Cancel();
await rtsPump.ConfigureAwait(false);
clientPumpCancellation.Cancel();
await clientPump.ConfigureAwait(false);
return;
}
var stopped = new TaskCompletionSource<object?>();
@ -69,7 +69,7 @@ Console.WriteLine("Aetheria Verse daemon is running. Press Ctrl+C to stop.");
var nextTickUtc = DateTimeOffset.UtcNow.Add(options.TickInterval);
while (!stopped.Task.IsCompleted)
{
ThrowIfRtsPumpFaulted(rtsPump);
ThrowIfClientPumpFaulted(clientPump);
var delay = nextTickUtc - DateTimeOffset.UtcNow;
if (delay > TimeSpan.Zero)
{
@ -80,7 +80,7 @@ while (!stopped.Task.IsCompleted)
var buildPublications = DateTimeOffset.UtcNow >= nextApiPublicationUtc;
var tick = await TickAsync(node, options, latestFrame, buildPublications).ConfigureAwait(false);
ThrowIfRtsPumpFaulted(rtsPump);
ThrowIfClientPumpFaulted(clientPump);
latestFrame = tick.Frame;
nextTickUtc += options.TickInterval;
if (nextTickUtc < DateTimeOffset.UtcNow - options.TickInterval)
@ -96,19 +96,19 @@ while (!stopped.Task.IsCompleted)
}
await PublishRuntimeSessionAsync(node, options, startedAtUtc, "stopping").ConfigureAwait(false);
rtsPumpCancellation.Cancel();
await rtsPump.ConfigureAwait(false);
clientPumpCancellation.Cancel();
await clientPump.ConfigureAwait(false);
Console.WriteLine("Aetheria Verse daemon stopping.");
static void ThrowIfRtsPumpFaulted(Task rtsPump)
static void ThrowIfClientPumpFaulted(Task clientPump)
{
if (!rtsPump.IsCompleted)
if (!clientPump.IsCompleted)
return;
if (rtsPump.IsFaulted)
throw new InvalidOperationException("Aetheria RTS CultMesh pump faulted.", rtsPump.Exception);
if (clientPump.IsFaulted)
throw new InvalidOperationException("Aetheria client CultMesh pump faulted.", clientPump.Exception);
throw new InvalidOperationException("Aetheria RTS CultMesh pump stopped unexpectedly.");
throw new InvalidOperationException("Aetheria client CultMesh pump stopped unexpectedly.");
}
static async Task<AetheriaRuntimeDaemonTickResult> TickAsync(
@ -177,6 +177,7 @@ static async Task<AetheriaRuntimeDaemonTickResult> TickAsync(
CumulativeRejectedCommandIds = currentFrame?.CumulativeRejectedCommandIds ?? currentFrame?.RejectedCommandIds ?? Array.Empty<string>(),
Catalog = node.RuntimeCatalog().Latest(),
RenderSettings = options.RenderSettings,
SimulationSettings = options.SimulationSettings,
StarbridgeScenario = starbridgeScenario,
StarbridgeSession = starbridgeSession,
BuildPublications = buildPublications,
@ -250,19 +251,19 @@ static async Task PublishCommittedCommandFactsAsync(
}
}
static RudpCultNetSchemaServer StartRtsCultMeshHost(
static RudpCultNetSchemaServer StartClientCultMeshHost(
AetheriaStateNode node,
AetheriaDaemonHostOptions options,
Func<AetheriaRuntimeDaemonFrameDocument?> latestFrame)
{
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
socket.Bind(new IPEndPoint(ParseBindAddress(options.RtsCultMeshHost), options.RtsCultMeshPort));
socket.Bind(new IPEndPoint(ParseBindAddress(options.ClientCultMeshHost), options.ClientCultMeshPort));
var server = new RudpCultNetSchemaServer(new RudpCultNetSchemaServerOptions
{
RuntimeId = $"{options.DaemonId}.rts",
RuntimeId = $"{options.DaemonId}.client",
Socket = socket,
ConnectionId = 0x43554c54,
TransportId = "aetheria-rts-rudp",
TransportId = "aetheria-client-rudp",
MaxFragmentBytes = 1200,
MaxPendingReliablePackets = 512
});
@ -869,7 +870,7 @@ static Task<EveSurfaceDocument?> ReadEveSurfacePublicationAsync(AetheriaStateNod
};
}
static async Task RunRtsCultMeshPumpAsync(
static async Task RunClientCultMeshPumpAsync(
RudpCultNetSchemaServer server,
CancellationToken cancellationToken)
{
@ -1573,14 +1574,14 @@ static Dictionary<string, object?> BuildOdinProviderAdvertisement(
new Dictionary<string, object?>(StringComparer.Ordinal)
{
["transport"] = "cultmesh-rudp",
["address"] = $"rudp://{options.RtsCultMeshAdvertiseHost}:{options.RtsCultMeshPort}",
["address"] = $"rudp://{options.ClientCultMeshAdvertiseHost}:{options.ClientCultMeshPort}",
["resolver"] = "provider-cultmesh-rudp",
["role"] = "cultmesh-snapshot"
},
new Dictionary<string, object?>(StringComparer.Ordinal)
{
["transport"] = "cultmesh-rudp",
["address"] = $"rudp://{options.RtsCultMeshAdvertiseHost}:{options.RtsCultMeshPort}",
["address"] = $"rudp://{options.ClientCultMeshAdvertiseHost}:{options.ClientCultMeshPort}",
["schemaId"] = AetheriaRuntimeDaemonSchemas.CultMeshCdnAssetBlob,
["resolver"] = "provider-cultmesh-rudp",
["role"] = "cultmesh-cdn"
@ -2383,9 +2384,9 @@ internal sealed class AetheriaDaemonHostOptions
public string SessionId { get; init; } = "local";
public string VerseId { get; init; } = "aetheria.local";
public string CultMeshAddress { get; init; } = "cultmesh://aetheria.local/eve/providers/aetheria.daemon";
public string RtsCultMeshHost { get; init; } = "127.0.0.1";
public string RtsCultMeshAdvertiseHost { get; init; } = "127.0.0.1";
public int RtsCultMeshPort { get; init; } = 3076;
public string ClientCultMeshHost { get; init; } = "127.0.0.1";
public string ClientCultMeshAdvertiseHost { get; init; } = "127.0.0.1";
public int ClientCultMeshPort { get; init; } = 3076;
public string AetheriaResourcesRoot { get; init; } = "";
public string OdinCultMeshUri { get; init; } = "cultmesh://odin/rendezvous/provider-catalog";
public bool EnableOdinAnnouncements { get; init; } = true;
@ -2394,6 +2395,8 @@ internal sealed class AetheriaDaemonHostOptions
public double FixedDeltaSeconds { get; init; } = 0.02;
public AetheriaRuntimeDaemonRenderSettings RenderSettings { get; init; } =
AetheriaRuntimeDaemonRenderSettings.AetheriaDefault;
public AetheriaRuntimeDaemonSimulationSettings SimulationSettings { get; init; } =
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault;
public bool Once { get; init; }
public static AetheriaDaemonHostOptions Parse(IReadOnlyList<string> args)
@ -2408,9 +2411,9 @@ internal sealed class AetheriaDaemonHostOptions
var daemonId = ReadOption(args, "--daemon-id");
var verseId = ReadOption(args, "--verse-id");
var cultMeshAddress = ReadOption(args, "--cultmesh-address");
var rtsCultMeshHost = ReadOption(args, "--rts-cultmesh-host");
var rtsCultMeshAdvertiseHost = ReadOption(args, "--rts-cultmesh-advertise-host");
var rtsCultMeshPort = ReadNonNegativeInt(args, "--rts-cultmesh-port") ?? 3076;
var clientCultMeshHost = ReadOption(args, "--client-cultmesh-host");
var clientCultMeshAdvertiseHost = ReadOption(args, "--client-cultmesh-advertise-host");
var clientCultMeshPort = ReadNonNegativeInt(args, "--client-cultmesh-port") ?? 3076;
var aetheriaResourcesRoot = ReadOption(args, "--aetheria-resources-root");
RejectRemovedOption(args, "--peer-cultmesh-endpoint", "Odin-discovered CultMesh peer documents");
RejectRemovedOption(args, "--odin-cultmesh-rudp", "--odin-cultmesh-uri");
@ -2432,11 +2435,11 @@ internal sealed class AetheriaDaemonHostOptions
CultMeshAddress = string.IsNullOrWhiteSpace(cultMeshAddress)
? "cultmesh://aetheria.local/eve/providers/aetheria.daemon"
: cultMeshAddress,
RtsCultMeshHost = string.IsNullOrWhiteSpace(rtsCultMeshHost) ? "127.0.0.1" : rtsCultMeshHost,
RtsCultMeshAdvertiseHost = string.IsNullOrWhiteSpace(rtsCultMeshAdvertiseHost)
? (string.IsNullOrWhiteSpace(rtsCultMeshHost) || rtsCultMeshHost == "0.0.0.0" || rtsCultMeshHost == "*" ? "127.0.0.1" : rtsCultMeshHost)
: rtsCultMeshAdvertiseHost,
RtsCultMeshPort = rtsCultMeshPort,
ClientCultMeshHost = string.IsNullOrWhiteSpace(clientCultMeshHost) ? "127.0.0.1" : clientCultMeshHost,
ClientCultMeshAdvertiseHost = string.IsNullOrWhiteSpace(clientCultMeshAdvertiseHost)
? (string.IsNullOrWhiteSpace(clientCultMeshHost) || clientCultMeshHost == "0.0.0.0" || clientCultMeshHost == "*" ? "127.0.0.1" : clientCultMeshHost)
: clientCultMeshAdvertiseHost,
ClientCultMeshPort = clientCultMeshPort,
AetheriaResourcesRoot = string.IsNullOrWhiteSpace(aetheriaResourcesRoot)
? Path.GetFullPath(Path.Combine(root, "Assets", "Resources"))
: Path.GetFullPath(aetheriaResourcesRoot),

View file

@ -16020,6 +16020,10 @@ static void RequireUnityDoesNotCallSharedSimulationTicks(string root)
var daemonTickRunner = File.Exists(daemonTickRunnerPath)
? File.ReadAllText(daemonTickRunnerPath)
: throw new InvalidOperationException("Cannot verify shared simulation authority; daemon tick runner is missing.");
var daemonProgramPath = Path.Combine(root, "Aetheria.State.Daemon", "Program.cs");
var daemonProgram = File.Exists(daemonProgramPath)
? File.ReadAllText(daemonProgramPath)
: throw new InvalidOperationException("Cannot verify shared simulation authority; daemon host is missing.");
var requiredDaemonTickSymbols = new[]
{
@ -16047,12 +16051,30 @@ static void RequireUnityDoesNotCallSharedSimulationTicks(string root)
"org.gamecult.aetheria.state",
"Runtime",
"AetheriaRuntimeSnapshotDocuments.cs");
var rtsSimulationPath = Path.Combine(
var oldRtsSimulationPath = Path.Combine(
root,
"Packages",
"org.gamecult.aetheria.state",
"Runtime",
"AetheriaRuntimeRtsSimulation.cs");
if (File.Exists(oldRtsSimulationPath))
{
throw new InvalidOperationException(
"Aetheria simulation authority is still runtime-named. Delete AetheriaRuntimeRtsSimulation.cs and keep runtime-specific names at projection edges.");
}
var daemonSimulationPath = Path.Combine(
root,
"Packages",
"org.gamecult.aetheria.state",
"Runtime",
"AetheriaRuntimeDaemonSimulation.cs");
var daemonSimulationSettingsPath = Path.Combine(
root,
"Packages",
"org.gamecult.aetheria.state",
"Runtime",
"AetheriaRuntimeDaemonSimulationSettings.cs");
var ymirProjectilePhysicsPath = Path.Combine(
root,
"Packages",
@ -16075,9 +16097,12 @@ static void RequireUnityDoesNotCallSharedSimulationTicks(string root)
var snapshotDocuments = File.Exists(snapshotDocumentsPath)
? File.ReadAllText(snapshotDocumentsPath)
: throw new InvalidOperationException("Cannot verify daemon projectile authority; runtime snapshot documents are missing.");
var rtsSimulation = File.Exists(rtsSimulationPath)
? File.ReadAllText(rtsSimulationPath)
: throw new InvalidOperationException("Cannot verify daemon projectile authority; RTS simulation is missing.");
var daemonSimulation = File.Exists(daemonSimulationPath)
? File.ReadAllText(daemonSimulationPath)
: throw new InvalidOperationException("Cannot verify daemon projectile authority; daemon simulation is missing.");
var daemonSimulationSettings = File.Exists(daemonSimulationSettingsPath)
? File.ReadAllText(daemonSimulationSettingsPath)
: throw new InvalidOperationException("Cannot verify daemon simulation authority; daemon simulation settings are missing.");
var ymirProjectilePhysics = File.Exists(ymirProjectilePhysicsPath)
? File.ReadAllText(ymirProjectilePhysicsPath)
: throw new InvalidOperationException("Cannot verify daemon projectile authority; Ymir projectile physics boundary is missing.");
@ -16098,14 +16123,23 @@ static void RequireUnityDoesNotCallSharedSimulationTicks(string root)
"public IReadOnlyList<AetheriaRuntimeProjectileCommit> Projectiles",
"public sealed class AetheriaRuntimeProjectileCommit"
},
[rtsSimulationPath] = new[]
[daemonSimulationPath] = new[]
{
"StepCombat(zone, entities, deltaSeconds)",
"public static class AetheriaRuntimeDaemonSimulation",
"AetheriaRuntimeDaemonSimulationSettings settings",
"StepCombat(zone, entities, deltaSeconds, settings)",
"EnsureDaemonWeaponState(",
"SpawnProjectile(zone, attacker, target)",
"SpawnProjectile(zone, attacker, target, settings)",
"AetheriaRuntimeYmirProjectilePhysics.Step(zone, entities, deltaSeconds)",
"Damage(target, hit.Projectile.Damage)"
},
[daemonSimulationSettingsPath] = new[]
{
"public readonly struct AetheriaRuntimeDaemonSimulationSettings",
"public static AetheriaRuntimeDaemonSimulationSettings AetheriaDefault",
"public double ProjectileSpeed { get; }",
"public double AttackRange { get; }"
},
[ymirProjectilePhysicsPath] = new[]
{
"public static class AetheriaRuntimeYmirProjectilePhysics",
@ -16137,7 +16171,8 @@ static void RequireUnityDoesNotCallSharedSimulationTicks(string root)
.SelectMany(pair =>
{
var text = pair.Key == snapshotDocumentsPath ? snapshotDocuments :
pair.Key == rtsSimulationPath ? rtsSimulation :
pair.Key == daemonSimulationPath ? daemonSimulation :
pair.Key == daemonSimulationSettingsPath ? daemonSimulationSettings :
pair.Key == ymirProjectilePhysicsPath ? ymirProjectilePhysics :
pair.Key == rtsDocumentsPath ? rtsDocuments :
pair.Key == rtsViewportDocumentsPath ? rtsViewportDocuments :
@ -16154,13 +16189,35 @@ static void RequireUnityDoesNotCallSharedSimulationTicks(string root)
string.Join("; ", missingProjectileAuthoritySymbols));
}
if (rtsSimulation.Contains("Damage(target, damage)", StringComparison.Ordinal) ||
rtsSimulation.Contains("Damage(target, ResolveProjectileDamage(attacker)", StringComparison.Ordinal) ||
rtsSimulation.Contains("private static void StepProjectiles(", StringComparison.Ordinal) ||
rtsSimulation.Contains("private static bool ProjectileHits(", StringComparison.Ordinal))
if (daemonSimulation.Contains("AetheriaRuntimeRtsSimulation", StringComparison.Ordinal) ||
daemonSimulation.Contains("Damage(target, damage)", StringComparison.Ordinal) ||
daemonSimulation.Contains("Damage(target, ResolveProjectileDamage(attacker)", StringComparison.Ordinal) ||
daemonSimulation.Contains("private static void StepProjectiles(", StringComparison.Ordinal) ||
daemonSimulation.Contains("private static bool ProjectileHits(", StringComparison.Ordinal) ||
daemonSimulation.Contains("private const double PawnSpeed", StringComparison.Ordinal) ||
daemonSimulation.Contains("private const double AttackRange", StringComparison.Ordinal) ||
daemonSimulation.Contains("private const double ProjectileSpeed", StringComparison.Ordinal))
{
throw new InvalidOperationException(
"Daemon RTS combat regressed to direct target damage or local projectile physics instead of Ymir-shaped projectile contact damage.");
"Daemon combat regressed to runtime-named simulation, hardcoded game feel, direct target damage, or local projectile physics instead of daemon settings plus Ymir-shaped projectile contact damage.");
}
if (!daemonTickRunner.Contains("AetheriaRuntimeDaemonSimulation.Step(", StringComparison.Ordinal) ||
!daemonTickRunner.Contains("public AetheriaRuntimeDaemonSimulationSettings SimulationSettings { get; set; }", StringComparison.Ordinal) ||
!daemonTickRunner.Contains("AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault", StringComparison.Ordinal) ||
!daemonTickRunner.Contains("simulationSettings: options.SimulationSettings", StringComparison.Ordinal))
{
throw new InvalidOperationException(
"Daemon ticks must route Aetheria simulation through daemon-owned settings, not runtime-local constants.");
}
if (daemonProgram.Contains("RtsCultMesh", StringComparison.Ordinal) ||
daemonProgram.Contains("--rts-cultmesh", StringComparison.Ordinal) ||
daemonProgram.Contains("RTS CultMesh endpoint", StringComparison.Ordinal) ||
daemonProgram.Contains("aetheria-rts-rudp", StringComparison.Ordinal))
{
throw new InvalidOperationException(
"Aetheria daemon transport must be client-neutral; runtime-branded CultMesh flags and transport ids belong outside daemon authority.");
}
if (daemonTickRunner.Contains("AetheriaRuntimeDaemonFrameStore.PublishFrame", StringComparison.Ordinal))

View file

@ -1925,10 +1925,11 @@ public class DaemonRuntimeDocumentTests
}
};
AetheriaRuntimeRtsSimulation.Step(
AetheriaRuntimeDaemonSimulation.Step(
run,
new AetheriaRuntimeDaemonIntentState(),
0.1);
0.1,
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault);
var zone = FindZone(run, 0);
Assert.IsNotEmpty(zone.Projectiles);
@ -1961,6 +1962,102 @@ public class DaemonRuntimeDocumentTests
obj.IconAsset.AssetKey == "aetheria.asset.sprite.rts.projectile"));
}
[Test]
public void DaemonSimulationSettingsDriveAetheriaCombatGameFeel()
{
var run = new AetheriaRuntimeRunCheckpointCommit
{
RunId = "simulation-settings-run",
CurrentZoneIndex = 0,
CurrentEntityKey = "zone.0.entity.0",
Zones = new[]
{
new AetheriaRuntimeZoneSnapshotCommit
{
ZoneIndex = 0,
SimulationTimeSeconds = 4,
Entities = new[]
{
new AetheriaRuntimeEntitySnapshotCommit
{
EntityIndex = 0,
Name = "Vanguard",
Kind = "ship",
FactionKey = "player",
IsActive = true,
TargetEntityIndex = 1,
PositionX = 0,
PositionZ = 0
},
new AetheriaRuntimeEntitySnapshotCommit
{
EntityIndex = 1,
Name = "Raider",
Kind = "ship",
FactionKey = "raider",
IsActive = true,
PositionX = 90,
PositionZ = 0
}
}
}
}
};
var settings = new AetheriaRuntimeDaemonSimulationSettings(
pawnSpeed: 11,
raiderSpeed: 7,
attackRange: 120,
attackHoldRatio: 0.75,
pawnProjectileDamage: 33,
raiderProjectileDamage: 5,
weaponCooldownSeconds: 1.25,
projectileSpeed: 444,
projectileRadius: 9,
projectileLifetimeSeconds: 3.5,
projectileSpawnOffset: 21,
projectileHeatScale: 0.5,
heatDissipationPerSecond: 0,
stationSensorRange: 333,
entitySensorRange: 222,
playerStationHull: 900,
hostileStationHull: 300,
playerEntityHull: 88,
raiderEntityHull: 66,
stationShield: 44,
entityShield: 22);
AetheriaRuntimeDaemonSimulation.Step(
run,
new AetheriaRuntimeDaemonIntentState(),
0.1,
settings);
var zone = FindZone(run, 0);
var projectile = zone.Projectiles.Single(projectile => projectile.SourceEntityIndex == 0);
Assert.AreEqual(21, projectile.PositionX, 0.0001);
Assert.AreEqual(444, projectile.VelocityX, 0.0001);
Assert.AreEqual(33, projectile.Damage, 0.0001);
Assert.AreEqual(9, projectile.Radius, 0.0001);
Assert.AreEqual(3.5, projectile.LifetimeSeconds, 0.0001);
var weapon = zone.Entities[0].WeaponStates.Single();
Assert.AreEqual(1.25, weapon.CooldownProgress, 0.0001);
Assert.AreEqual(1.25, weapon.BurstInterval, 0.0001);
Assert.AreEqual(88, zone.Entities[0].StatGrids.Single(grid => grid.Name == "hull").Values[0], 0.0001);
Assert.AreEqual(66, zone.Entities[1].StatGrids.Single(grid => grid.Name == "hull").Values[0], 0.0001);
Assert.AreEqual(222, zone.Entities[0].Visibility, 0.0001);
var frame = AetheriaRuntimeDaemonFrameDocument.Create(
run,
"daemon",
"session",
5,
4.1,
0.1,
simulationSettings: settings);
Assert.AreEqual(444, frame.SimulationSettings.ProjectileSpeed, 0.0001);
Assert.AreEqual(120, frame.SimulationSettings.AttackRange, 0.0001);
}
[Test]
public void SoaViewIndexRejectsInvalidRenderGroups()
{

View file

@ -123,7 +123,8 @@
<Compile Include="Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeDaemonOperations.cs" />
<Compile Include="Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeCommittedFactImporter.cs" />
<Compile Include="Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeInputSettingsCommands.cs" />
<Compile Include="Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeRtsSimulation.cs" />
<Compile Include="Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeDaemonSimulation.cs" />
<Compile Include="Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeDaemonSimulationSettings.cs" />
<Compile Include="Packages\org.gamecult.aetheria.state\Runtime\AetheriaState.cs" />
<Compile Include="Packages\org.gamecult.aetheria.state\Runtime\AetheriaClientState.cs" />
<Compile Include="Packages\org.gamecult.aetheria.state\Runtime\AetheriaClient.cs" />

View file

@ -536,6 +536,10 @@ namespace GameCult.Aetheria.State.Verse
public AetheriaRuntimeDaemonRenderSettings RenderSettings { get; set; } =
AetheriaRuntimeDaemonRenderSettings.AetheriaDefault;
[Key(22)]
public AetheriaRuntimeDaemonSimulationSettings SimulationSettings { get; set; } =
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault;
public static AetheriaRuntimeDaemonFrameDocument Create(
AetheriaRuntimeRunCheckpointCommit run,
string daemonId,
@ -545,7 +549,8 @@ namespace GameCult.Aetheria.State.Verse
double fixedDeltaSeconds,
bool isAuthoritative = true,
string stateSource = "daemon",
AetheriaRuntimeDaemonRenderSettings? renderSettings = null)
AetheriaRuntimeDaemonRenderSettings? renderSettings = null,
AetheriaRuntimeDaemonSimulationSettings? simulationSettings = null)
{
return new AetheriaRuntimeDaemonFrameDocument
{
@ -558,7 +563,8 @@ namespace GameCult.Aetheria.State.Verse
IsAuthoritative = isAuthoritative,
StateSource = string.IsNullOrWhiteSpace(stateSource) ? "daemon" : stateSource,
Run = run ?? new AetheriaRuntimeRunCheckpointCommit(),
RenderSettings = renderSettings ?? AetheriaRuntimeDaemonRenderSettings.AetheriaDefault
RenderSettings = renderSettings ?? AetheriaRuntimeDaemonRenderSettings.AetheriaDefault,
SimulationSettings = simulationSettings ?? AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault
};
}
}

View file

@ -6,25 +6,17 @@ using System.Linq;
namespace GameCult.Aetheria.State.Verse
{
public static class AetheriaRuntimeRtsSimulation
public static class AetheriaRuntimeDaemonSimulation
{
private const string Hull = "hull";
private const string Shield = "shield";
private const string Heat = "heat";
private const double PawnSpeed = 84.0;
private const double RaiderSpeed = 68.0;
private const double AttackRange = 145.0;
private const double PawnProjectileDamage = 18.0;
private const double RaiderProjectileDamage = 12.0;
private const double WeaponCooldownSeconds = 0.55;
private const double ProjectileSpeed = 330.0;
private const double ProjectileRadius = 18.0;
private const double ProjectileLifetimeSeconds = 2.2;
public static void Step(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonIntentState intents,
double deltaSeconds)
double deltaSeconds,
AetheriaRuntimeDaemonSimulationSettings settings)
{
if (run == null || deltaSeconds <= 0)
return;
@ -40,20 +32,21 @@ namespace GameCult.Aetheria.State.Verse
if (entities.Length == 0)
continue;
EnsureStats(entities);
ApplyMovementIntent(run, entities, intents?.Movement);
EnsureStats(entities, settings);
ApplyMovementIntent(run, entities, intents?.Movement, settings);
StepRaiderAi(entities);
StepTargetPursuit(entities);
StepTargetPursuit(entities, settings);
StepMovement(entities, deltaSeconds);
StepCombat(zone, entities, deltaSeconds);
RefreshContacts(entities);
StepCombat(zone, entities, deltaSeconds, settings);
RefreshContacts(entities, settings);
}
}
private static void ApplyMovementIntent(
AetheriaRuntimeRunCheckpointCommit run,
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities,
AetheriaRuntimeDaemonMovementIntent? movement)
AetheriaRuntimeDaemonMovementIntent? movement,
AetheriaRuntimeDaemonSimulationSettings settings)
{
if (movement == null || !TryParseEntityIndex(movement.ActorEntityKey, out var entityIndex))
return;
@ -64,7 +57,7 @@ namespace GameCult.Aetheria.State.Verse
var magnitude = Clamp01(movement.Magnitude);
var normalized = Normalize(movement.DirectionX, movement.DirectionY);
var speed = ResolveSpeed(entity);
var speed = ResolveSpeed(entity, settings);
entity.VelocityX = normalized.X * speed * magnitude;
entity.VelocityY = normalized.Y * speed * magnitude;
if (magnitude <= 0.001)
@ -92,7 +85,9 @@ namespace GameCult.Aetheria.State.Verse
}
}
private static void StepTargetPursuit(IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities)
private static void StepTargetPursuit(
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities,
AetheriaRuntimeDaemonSimulationSettings settings)
{
var byIndex = entities.ToDictionary(entity => entity.EntityIndex);
foreach (var entity in entities)
@ -108,7 +103,7 @@ namespace GameCult.Aetheria.State.Verse
var dx = target.PositionX - entity.PositionX;
var dy = target.PositionZ - entity.PositionZ;
var distance = Math.Sqrt(dx * dx + dy * dy);
if (distance <= AttackRange * 0.82)
if (distance <= settings.AttackRange * settings.AttackHoldRatio)
{
entity.VelocityX *= 0.72;
entity.VelocityY *= 0.72;
@ -119,7 +114,7 @@ namespace GameCult.Aetheria.State.Verse
if (string.Equals(entity.FactionKey, "raider", StringComparison.OrdinalIgnoreCase))
{
var direction = Normalize(dx, dy);
var speed = ResolveSpeed(entity);
var speed = ResolveSpeed(entity, settings);
entity.VelocityX = direction.X * speed;
entity.VelocityY = direction.Y * speed;
Face(entity, direction.X, direction.Y);
@ -148,12 +143,13 @@ namespace GameCult.Aetheria.State.Verse
private static void StepCombat(
AetheriaRuntimeZoneSnapshotCommit zone,
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities,
double deltaSeconds)
double deltaSeconds,
AetheriaRuntimeDaemonSimulationSettings settings)
{
var byIndex = entities.ToDictionary(entity => entity.EntityIndex);
foreach (var attacker in entities)
{
var weaponState = EnsureDaemonWeaponState(attacker);
var weaponState = EnsureDaemonWeaponState(attacker, settings);
weaponState.Firing = false;
weaponState.CooldownProgress = Math.Max(0, weaponState.CooldownProgress - deltaSeconds);
weaponState.CoolingDown = weaponState.CooldownProgress > 0;
@ -167,7 +163,7 @@ namespace GameCult.Aetheria.State.Verse
continue;
}
if (DistanceSq(attacker, target) > AttackRange * AttackRange)
if (DistanceSq(attacker, target) > settings.AttackRange * settings.AttackRange)
continue;
Face(attacker, target.PositionX - attacker.PositionX, target.PositionZ - attacker.PositionZ);
@ -177,11 +173,11 @@ namespace GameCult.Aetheria.State.Verse
if (weaponState.CooldownProgress > 0)
continue;
SpawnProjectile(zone, attacker, target);
SpawnProjectile(zone, attacker, target, settings);
weaponState.Firing = true;
weaponState.CoolingDown = true;
weaponState.CooldownProgress = WeaponCooldownSeconds;
SetStat(attacker, Heat, Math.Min(100, GetStat(attacker, Heat) + ResolveProjectileDamage(attacker) * 0.18));
weaponState.CooldownProgress = settings.WeaponCooldownSeconds;
SetStat(attacker, Heat, Math.Min(100, GetStat(attacker, Heat) + ResolveProjectileDamage(attacker, settings) * settings.ProjectileHeatScale));
}
var projectileStep = AetheriaRuntimeYmirProjectilePhysics.Step(zone, entities, deltaSeconds);
@ -194,7 +190,7 @@ namespace GameCult.Aetheria.State.Verse
foreach (var entity in entities)
{
SetStat(entity, Heat, Math.Max(0, GetStat(entity, Heat) - 8.0 * deltaSeconds));
SetStat(entity, Heat, Math.Max(0, GetStat(entity, Heat) - settings.HeatDissipationPerSecond * deltaSeconds));
entity.IsActive = IsAlive(entity);
if (!entity.IsActive)
{
@ -206,7 +202,8 @@ namespace GameCult.Aetheria.State.Verse
}
private static AetheriaRuntimeWeaponStateCommit EnsureDaemonWeaponState(
AetheriaRuntimeEntitySnapshotCommit entity)
AetheriaRuntimeEntitySnapshotCommit entity,
AetheriaRuntimeDaemonSimulationSettings settings)
{
var states = (entity.WeaponStates ?? Array.Empty<AetheriaRuntimeWeaponStateCommit>()).ToList();
var state = states.FirstOrDefault(candidate =>
@ -224,7 +221,7 @@ namespace GameCult.Aetheria.State.Verse
BehaviorKind = "ProjectileWeapon",
Ammo = -1,
BurstRemaining = 0,
BurstInterval = WeaponCooldownSeconds,
BurstInterval = settings.WeaponCooldownSeconds,
LockTargetEntityIndex = -1
};
states.Add(state);
@ -235,7 +232,8 @@ namespace GameCult.Aetheria.State.Verse
private static void SpawnProjectile(
AetheriaRuntimeZoneSnapshotCommit zone,
AetheriaRuntimeEntitySnapshotCommit attacker,
AetheriaRuntimeEntitySnapshotCommit target)
AetheriaRuntimeEntitySnapshotCommit target,
AetheriaRuntimeDaemonSimulationSettings settings)
{
var direction = Normalize(target.PositionX - attacker.PositionX, target.PositionZ - attacker.PositionZ);
if (Math.Abs(direction.X) + Math.Abs(direction.Y) <= 0.0001)
@ -252,16 +250,16 @@ namespace GameCult.Aetheria.State.Verse
SourceEntityIndex = attacker.EntityIndex,
TargetEntityIndex = target.EntityIndex,
FactionKey = attacker.FactionKey ?? "",
PositionX = attacker.PositionX + direction.X * 18.0,
PositionX = attacker.PositionX + direction.X * settings.ProjectileSpawnOffset,
PositionY = attacker.PositionY,
PositionZ = attacker.PositionZ + direction.Y * 18.0,
PositionZ = attacker.PositionZ + direction.Y * settings.ProjectileSpawnOffset,
DirectionX = direction.X,
DirectionY = direction.Y,
VelocityX = direction.X * ProjectileSpeed,
VelocityY = direction.Y * ProjectileSpeed,
Damage = ResolveProjectileDamage(attacker),
Radius = ProjectileRadius,
LifetimeSeconds = ProjectileLifetimeSeconds,
VelocityX = direction.X * settings.ProjectileSpeed,
VelocityY = direction.Y * settings.ProjectileSpeed,
Damage = ResolveProjectileDamage(attacker, settings),
Radius = settings.ProjectileRadius,
LifetimeSeconds = settings.ProjectileLifetimeSeconds,
Guided = true,
Active = true,
WeaponKind = IsPlayerOwned(attacker) ? "vanguard-bolt" : "raider-bolt"
@ -279,12 +277,16 @@ namespace GameCult.Aetheria.State.Verse
return $"projectile:{zone.ZoneIndex}:{tick}:{attacker.EntityIndex}:{target.EntityIndex}:{ordinal}";
}
private static double ResolveProjectileDamage(AetheriaRuntimeEntitySnapshotCommit attacker)
private static double ResolveProjectileDamage(
AetheriaRuntimeEntitySnapshotCommit attacker,
AetheriaRuntimeDaemonSimulationSettings settings)
{
return IsPlayerOwned(attacker) ? PawnProjectileDamage : RaiderProjectileDamage;
return IsPlayerOwned(attacker) ? settings.PawnProjectileDamage : settings.RaiderProjectileDamage;
}
private static void RefreshContacts(IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities)
private static void RefreshContacts(
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities,
AetheriaRuntimeDaemonSimulationSettings settings)
{
foreach (var observer in entities)
{
@ -295,7 +297,7 @@ namespace GameCult.Aetheria.State.Verse
continue;
var distance = Math.Sqrt(DistanceSq(observer, target));
var visible = distance <= ResolveSensorRange(observer);
var visible = distance <= ResolveSensorRange(observer, settings);
if (!visible && !Hostile(observer, target))
continue;
@ -309,12 +311,14 @@ namespace GameCult.Aetheria.State.Verse
}
observer.Contacts = contacts;
observer.Visibility = observer.IsActive ? ResolveSensorRange(observer) : 0;
observer.Visibility = observer.IsActive ? ResolveSensorRange(observer, settings) : 0;
observer.VisibilitySourceCount = observer.IsActive ? 1 : 0;
}
}
private static void EnsureStats(IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities)
private static void EnsureStats(
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities,
AetheriaRuntimeDaemonSimulationSettings settings)
{
foreach (var entity in entities)
{
@ -322,8 +326,8 @@ namespace GameCult.Aetheria.State.Verse
{
entity.StatGrids = new[]
{
Stat(Hull, DefaultHull(entity)),
Stat(Shield, DefaultShield(entity)),
Stat(Hull, DefaultHull(entity, settings)),
Stat(Shield, DefaultShield(entity, settings)),
Stat(Heat, 0)
};
}
@ -366,33 +370,45 @@ namespace GameCult.Aetheria.State.Verse
string.Equals(right.FactionKey, "raider", StringComparison.OrdinalIgnoreCase));
}
private static double ResolveSpeed(AetheriaRuntimeEntitySnapshotCommit entity)
private static double ResolveSpeed(
AetheriaRuntimeEntitySnapshotCommit entity,
AetheriaRuntimeDaemonSimulationSettings settings)
{
if (string.Equals(entity.Kind, "station", StringComparison.OrdinalIgnoreCase))
return 0;
return string.Equals(entity.FactionKey, "raider", StringComparison.OrdinalIgnoreCase)
? RaiderSpeed
: PawnSpeed;
? settings.RaiderSpeed
: settings.PawnSpeed;
}
private static double ResolveSensorRange(AetheriaRuntimeEntitySnapshotCommit entity)
{
return string.Equals(entity.Kind, "station", StringComparison.OrdinalIgnoreCase) ? 720 : 520;
}
private static double DefaultHull(AetheriaRuntimeEntitySnapshotCommit entity)
private static double ResolveSensorRange(
AetheriaRuntimeEntitySnapshotCommit entity,
AetheriaRuntimeDaemonSimulationSettings settings)
{
return string.Equals(entity.Kind, "station", StringComparison.OrdinalIgnoreCase)
? (IsPlayerOwned(entity) ? 420 : 240)
: string.Equals(entity.FactionKey, "raider", StringComparison.OrdinalIgnoreCase)
? 80
: 120;
? settings.StationSensorRange
: settings.EntitySensorRange;
}
private static double DefaultShield(AetheriaRuntimeEntitySnapshotCommit entity)
private static double DefaultHull(
AetheriaRuntimeEntitySnapshotCommit entity,
AetheriaRuntimeDaemonSimulationSettings settings)
{
return string.Equals(entity.Kind, "station", StringComparison.OrdinalIgnoreCase) ? 120 : 45;
return string.Equals(entity.Kind, "station", StringComparison.OrdinalIgnoreCase)
? (IsPlayerOwned(entity) ? settings.PlayerStationHull : settings.HostileStationHull)
: string.Equals(entity.FactionKey, "raider", StringComparison.OrdinalIgnoreCase)
? settings.RaiderEntityHull
: settings.PlayerEntityHull;
}
private static double DefaultShield(
AetheriaRuntimeEntitySnapshotCommit entity,
AetheriaRuntimeDaemonSimulationSettings settings)
{
return string.Equals(entity.Kind, "station", StringComparison.OrdinalIgnoreCase)
? settings.StationShield
: settings.EntityShield;
}
private static double GetStat(AetheriaRuntimeEntitySnapshotCommit entity, string name)

View file

@ -0,0 +1,142 @@
using MessagePack;
#nullable enable
namespace GameCult.Aetheria.State.Verse
{
[MessagePackObject(true)]
public readonly struct AetheriaRuntimeDaemonSimulationSettings
{
public static AetheriaRuntimeDaemonSimulationSettings AetheriaDefault { get; } =
new AetheriaRuntimeDaemonSimulationSettings(
pawnSpeed: 84.0,
raiderSpeed: 68.0,
attackRange: 145.0,
attackHoldRatio: 0.82,
pawnProjectileDamage: 18.0,
raiderProjectileDamage: 12.0,
weaponCooldownSeconds: 0.55,
projectileSpeed: 330.0,
projectileRadius: 18.0,
projectileLifetimeSeconds: 2.2,
projectileSpawnOffset: 18.0,
projectileHeatScale: 0.18,
heatDissipationPerSecond: 8.0,
stationSensorRange: 720.0,
entitySensorRange: 520.0,
playerStationHull: 420.0,
hostileStationHull: 240.0,
playerEntityHull: 120.0,
raiderEntityHull: 80.0,
stationShield: 120.0,
entityShield: 45.0);
public AetheriaRuntimeDaemonSimulationSettings(
double pawnSpeed,
double raiderSpeed,
double attackRange,
double attackHoldRatio,
double pawnProjectileDamage,
double raiderProjectileDamage,
double weaponCooldownSeconds,
double projectileSpeed,
double projectileRadius,
double projectileLifetimeSeconds,
double projectileSpawnOffset,
double projectileHeatScale,
double heatDissipationPerSecond,
double stationSensorRange,
double entitySensorRange,
double playerStationHull,
double hostileStationHull,
double playerEntityHull,
double raiderEntityHull,
double stationShield,
double entityShield)
{
PawnSpeed = PositiveOr(pawnSpeed, AetheriaDefaultRaw.PawnSpeed);
RaiderSpeed = PositiveOr(raiderSpeed, AetheriaDefaultRaw.RaiderSpeed);
AttackRange = PositiveOr(attackRange, AetheriaDefaultRaw.AttackRange);
AttackHoldRatio = Clamp01Or(attackHoldRatio, AetheriaDefaultRaw.AttackHoldRatio);
PawnProjectileDamage = PositiveOr(pawnProjectileDamage, AetheriaDefaultRaw.PawnProjectileDamage);
RaiderProjectileDamage = PositiveOr(raiderProjectileDamage, AetheriaDefaultRaw.RaiderProjectileDamage);
WeaponCooldownSeconds = PositiveOr(weaponCooldownSeconds, AetheriaDefaultRaw.WeaponCooldownSeconds);
ProjectileSpeed = PositiveOr(projectileSpeed, AetheriaDefaultRaw.ProjectileSpeed);
ProjectileRadius = PositiveOr(projectileRadius, AetheriaDefaultRaw.ProjectileRadius);
ProjectileLifetimeSeconds = PositiveOr(projectileLifetimeSeconds, AetheriaDefaultRaw.ProjectileLifetimeSeconds);
ProjectileSpawnOffset = PositiveOr(projectileSpawnOffset, AetheriaDefaultRaw.ProjectileSpawnOffset);
ProjectileHeatScale = NonNegativeOr(projectileHeatScale, AetheriaDefaultRaw.ProjectileHeatScale);
HeatDissipationPerSecond = NonNegativeOr(heatDissipationPerSecond, AetheriaDefaultRaw.HeatDissipationPerSecond);
StationSensorRange = PositiveOr(stationSensorRange, AetheriaDefaultRaw.StationSensorRange);
EntitySensorRange = PositiveOr(entitySensorRange, AetheriaDefaultRaw.EntitySensorRange);
PlayerStationHull = PositiveOr(playerStationHull, AetheriaDefaultRaw.PlayerStationHull);
HostileStationHull = PositiveOr(hostileStationHull, AetheriaDefaultRaw.HostileStationHull);
PlayerEntityHull = PositiveOr(playerEntityHull, AetheriaDefaultRaw.PlayerEntityHull);
RaiderEntityHull = PositiveOr(raiderEntityHull, AetheriaDefaultRaw.RaiderEntityHull);
StationShield = NonNegativeOr(stationShield, AetheriaDefaultRaw.StationShield);
EntityShield = NonNegativeOr(entityShield, AetheriaDefaultRaw.EntityShield);
}
public double PawnSpeed { get; }
public double RaiderSpeed { get; }
public double AttackRange { get; }
public double AttackHoldRatio { get; }
public double PawnProjectileDamage { get; }
public double RaiderProjectileDamage { get; }
public double WeaponCooldownSeconds { get; }
public double ProjectileSpeed { get; }
public double ProjectileRadius { get; }
public double ProjectileLifetimeSeconds { get; }
public double ProjectileSpawnOffset { get; }
public double ProjectileHeatScale { get; }
public double HeatDissipationPerSecond { get; }
public double StationSensorRange { get; }
public double EntitySensorRange { get; }
public double PlayerStationHull { get; }
public double HostileStationHull { get; }
public double PlayerEntityHull { get; }
public double RaiderEntityHull { get; }
public double StationShield { get; }
public double EntityShield { get; }
private static double PositiveOr(double value, double fallback)
{
return double.IsFinite(value) && value > 0 ? value : fallback;
}
private static double NonNegativeOr(double value, double fallback)
{
return double.IsFinite(value) && value >= 0 ? value : fallback;
}
private static double Clamp01Or(double value, double fallback)
{
return double.IsFinite(value) ? value < 0 ? 0 : value > 1 ? 1 : value : fallback;
}
private static class AetheriaDefaultRaw
{
public const double PawnSpeed = 84.0;
public const double RaiderSpeed = 68.0;
public const double AttackRange = 145.0;
public const double AttackHoldRatio = 0.82;
public const double PawnProjectileDamage = 18.0;
public const double RaiderProjectileDamage = 12.0;
public const double WeaponCooldownSeconds = 0.55;
public const double ProjectileSpeed = 330.0;
public const double ProjectileRadius = 18.0;
public const double ProjectileLifetimeSeconds = 2.2;
public const double ProjectileSpawnOffset = 18.0;
public const double ProjectileHeatScale = 0.18;
public const double HeatDissipationPerSecond = 8.0;
public const double StationSensorRange = 720.0;
public const double EntitySensorRange = 520.0;
public const double PlayerStationHull = 420.0;
public const double HostileStationHull = 240.0;
public const double PlayerEntityHull = 120.0;
public const double RaiderEntityHull = 80.0;
public const double StationShield = 120.0;
public const double EntityShield = 45.0;
}
}
}

View file

@ -32,6 +32,8 @@ namespace GameCult.Aetheria.State.Verse
public AetheriaRuntimeCatalogSnapshot? Catalog { get; set; }
public AetheriaRuntimeDaemonRenderSettings RenderSettings { get; set; } =
AetheriaRuntimeDaemonRenderSettings.AetheriaDefault;
public AetheriaRuntimeDaemonSimulationSettings SimulationSettings { get; set; } =
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault;
public bool BuildPublications { get; set; } = true;
}
@ -126,10 +128,11 @@ namespace GameCult.Aetheria.State.Verse
.ToArray(),
operationResult.Intents);
}
AetheriaRuntimeRtsSimulation.Step(
AetheriaRuntimeDaemonSimulation.Step(
operationResult.Run,
operationResult.Intents,
options.FixedDeltaSeconds);
options.FixedDeltaSeconds,
options.SimulationSettings);
StampZoneSimulationTime(operationResult.Run, options.SimulationTimeSeconds);
var frame = AetheriaRuntimeDaemonFrameDocument.Create(
@ -139,7 +142,8 @@ namespace GameCult.Aetheria.State.Verse
options.FrameId,
options.SimulationTimeSeconds,
options.FixedDeltaSeconds,
renderSettings: options.RenderSettings);
renderSettings: options.RenderSettings,
simulationSettings: options.SimulationSettings);
frame.AppliedCommandIds = operationResult.AppliedCommandIds;
frame.RejectedCommandIds = operationResult.RejectedCommandIds;
frame.AccountedCommandIds = accountedBeforeTick