Move Aetheria simulation tuning into daemon authority
This commit is contained in:
parent
403913e899
commit
70607384fc
12 changed files with 449 additions and 121 deletions
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue