Sync daemon authority migration

This commit is contained in:
Meta Vens 2026-06-21 09:37:54 +01:00
commit 39a0dc8737
295 changed files with 25121 additions and 7360 deletions

View file

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Aetheria.State\Aetheria.State.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,402 @@
using Aetheria.State;
using Aetheria.State.Documents;
using GameCult.Aetheria.State.Unity;
var options = AetheriaDaemonHostOptions.Parse(args);
var startedAtUtc = DateTimeOffset.UtcNow.ToString("O");
Console.WriteLine($"Aetheria Verse daemon starting: {options.StatePath}");
await using var node = await AetheriaStateNode.OpenAsync(
options.StatePath,
runtimeId: options.DaemonId,
startServer: true).ConfigureAwait(false);
using var discoveryHost = new AetheriaVerseDiscoveryHost(node);
await EnsureWorldDocumentAsync(node).ConfigureAwait(false);
var verseHost = await EnsureVerseHostSettingsAsync(node, options, startedAtUtc).ConfigureAwait(false);
discoveryHost.Update(verseHost);
await PublishRuntimeSessionAsync(node, options, startedAtUtc, "starting").ConfigureAwait(false);
await PublishStateSurfacesAsync(node, options, startedAtUtc).ConfigureAwait(false);
var firstTick = await TickAsync(node, options).ConfigureAwait(false);
Console.WriteLine($"Aetheria Verse daemon published frame {firstTick.Frame.FrameId}.");
if (options.Once)
{
await PublishRuntimeSessionAsync(node, options, startedAtUtc, "completed").ConfigureAwait(false);
return;
}
var stopped = new TaskCompletionSource<object?>();
Console.CancelKeyPress += (_, eventArgs) =>
{
eventArgs.Cancel = true;
stopped.TrySetResult(null);
};
await PublishRuntimeSessionAsync(node, options, startedAtUtc, "running").ConfigureAwait(false);
Console.WriteLine("Aetheria Verse daemon is running. Press Ctrl+C to stop.");
while (!stopped.Task.IsCompleted)
{
var completed = await Task.WhenAny(stopped.Task, Task.Delay(options.TickInterval)).ConfigureAwait(false);
if (completed == stopped.Task)
break;
var tick = await TickAsync(node, options).ConfigureAwait(false);
discoveryHost.Update(await node.GetVerseHostSettingsAsync().ConfigureAwait(false));
await PublishRuntimeSessionAsync(node, options, startedAtUtc, "running").ConfigureAwait(false);
Console.WriteLine($"Aetheria Verse daemon published frame {tick.Frame.FrameId}.");
}
await PublishRuntimeSessionAsync(node, options, startedAtUtc, "stopping").ConfigureAwait(false);
Console.WriteLine("Aetheria Verse daemon stopping.");
static async Task<AetheriaRuntimeDaemonTickResult> TickAsync(
AetheriaStateNode node,
AetheriaDaemonHostOptions options)
{
await AcceptEveCommandsAsync(node, options).ConfigureAwait(false);
var currentFrame = AetheriaRuntimeDaemonFrameStore.TryReadFrame(node.StatePath, out var frame)
? frame
: null;
var fixedDeltaSeconds = currentFrame?.FixedDeltaSeconds > 0
? currentFrame.FixedDeltaSeconds
: options.FixedDeltaSeconds;
var nextFrameId = (currentFrame?.FrameId ?? -1) + 1;
var simulationTimeSeconds = (currentFrame?.SimulationTimeSeconds ?? 0) + fixedDeltaSeconds;
var sessionId = string.IsNullOrWhiteSpace(currentFrame?.SessionId)
? options.SessionId
: currentFrame.SessionId;
var run = currentFrame?.Run ?? new AetheriaRuntimeRunCheckpointCommit();
var loadoutTemplates = node.Cache
.GetAll<AetheriaLoadoutTemplate>()
.Select(ToLoadoutTemplateCommit)
.ToArray();
var observedCommands = node.ReadObservedDaemonCommands();
var result = AetheriaRuntimeDaemonTickRunner.Tick(
node.StatePath,
run,
new AetheriaRuntimeDaemonTickOptions
{
DaemonId = options.DaemonId,
SessionId = sessionId,
VerseId = options.VerseId,
CultMeshAddress = options.CultMeshAddress,
FrameId = nextFrameId,
SimulationTimeSeconds = simulationTimeSeconds,
FixedDeltaSeconds = fixedDeltaSeconds,
ObservedCommands = observedCommands,
AccountedCommandIds = currentFrame?.AccountedCommandIds ?? Array.Empty<string>(),
OperationContext = new AetheriaRuntimeDaemonOperationContext
{
LoadoutTemplates = loadoutTemplates
}
});
await PublishDaemonApiDocumentsAsync(node, result).ConfigureAwait(false);
await PublishStateSurfacesAsync(node, options, result.Frame.PublishedAtUtc).ConfigureAwait(false);
return result;
}
static async Task PublishDaemonApiDocumentsAsync(
AetheriaStateNode node,
AetheriaRuntimeDaemonTickResult result)
{
await node.PutDaemonFrameAsync(result.Frame).ConfigureAwait(false);
if (AetheriaRuntimeDaemonSoaViewStore.TryReadView(node.StatePath, out var soaView) &&
string.Equals(soaView.Schema, AetheriaRuntimeDaemonSchemas.SoaView, StringComparison.Ordinal))
{
await node.PutDaemonSoaViewAsync(soaView).ConfigureAwait(false);
}
if (AetheriaRuntimeDaemonPublicationStore.TryReadProviderAdvertisement(node.StatePath, out var provider))
await node.PutDaemonProviderAdvertisementAsync(provider).ConfigureAwait(false);
if (AetheriaRuntimeDaemonPublicationStore.TryReadHealth(node.StatePath, out var health))
await node.PutDaemonHealthAsync(health).ConfigureAwait(false);
if (AetheriaRuntimeDaemonPublicationStore.TryReadCommandBoundary(node.StatePath, out var commandBoundary))
await node.PutDaemonCommandBoundaryAsync(commandBoundary).ConfigureAwait(false);
if (AetheriaRuntimeDaemonPublicationStore.TryReadGameSurface(node.StatePath, out var gameSurface))
await node.PutDaemonGameSurfaceAsync(AetheriaRuntimeEveSurfaceStateProjector.ToState(gameSurface)).ConfigureAwait(false);
if (AetheriaRuntimeDaemonPublicationStore.TryReadGameTuiSurface(node.StatePath, out var gameTuiSurface))
await node.PutDaemonGameTuiSurfaceAsync(AetheriaRuntimeEveSurfaceStateProjector.ToState(gameTuiSurface)).ConfigureAwait(false);
if (AetheriaRuntimeDaemonPublicationStore.TryReadEditorSurface(node.StatePath, out var editorSurface))
await node.PutDaemonEditorSurfaceAsync(AetheriaRuntimeEveSurfaceStateProjector.ToState(editorSurface)).ConfigureAwait(false);
if (AetheriaRuntimeDaemonPublicationStore.TryReadEditorTuiSurface(node.StatePath, out var editorTuiSurface))
await node.PutDaemonEditorTuiSurfaceAsync(AetheriaRuntimeEveSurfaceStateProjector.ToState(editorTuiSurface)).ConfigureAwait(false);
}
static async Task AcceptEveCommandsAsync(AetheriaStateNode node, AetheriaDaemonHostOptions options)
{
var commandCountBefore = node.ReadObservedEveCommands().Count;
var now = DateTimeOffset.UtcNow.ToString("O");
try
{
var existingStatus = await node.GetEveCommandAcceptanceStatusAsync().ConfigureAwait(false);
var report = await AetheriaEveCommandBridge.AcceptObservedAsync(
node,
existingStatus?.AccountedCommandIds)
.ConfigureAwait(false);
await node.PutEveCommandAcceptanceStatusAsync(new AetheriaEveCommandAcceptanceStatus
{
RuntimeId = options.DaemonId,
StatePath = node.StatePath,
LastPollAtUtc = now,
LastAcceptedAtUtc = report.AcceptedCommandIds.Length > 0 ? now : "",
ObservedBeforeAccept = commandCountBefore,
CommandsAccepted = report.AcceptedCommandIds.Length,
CommandsRejected = report.RejectedCommands,
AppliedCatalogRefreshes = report.AcceptedCatalogRefreshes,
AppliedOperationsRefreshes = report.AcceptedOperationsRefreshes,
AppliedPlayerSettingsCommands = report.AcceptedPlayerSettingsCommands,
AppliedInputSettingsCommands = report.AcceptedInputSettingsCommands,
AppliedLoadoutTemplateCommands = report.AcceptedLoadoutTemplateCommands,
AppliedVerseHostCommands = report.AcceptedVerseHostCommands,
AccountedCommandIds = report.AccountedCommandIds,
LastRejectedCommand = report.LastRejectedCommand,
LastRejectedReason = report.LastRejectedReason,
ConsecutiveFailures = 0,
Status = report.RejectedCommands > 0 ? "rejected" : "ok"
}).ConfigureAwait(false);
}
catch (Exception ex)
{
var existing = await node.GetEveCommandAcceptanceStatusAsync().ConfigureAwait(false);
await node.PutEveCommandAcceptanceStatusAsync(new AetheriaEveCommandAcceptanceStatus
{
RuntimeId = options.DaemonId,
StatePath = node.StatePath,
LastPollAtUtc = now,
LastAcceptedAtUtc = existing?.LastAcceptedAtUtc ?? "",
ObservedBeforeAccept = commandCountBefore,
AccountedCommandIds = existing?.AccountedCommandIds ?? [],
ConsecutiveFailures = (existing?.ConsecutiveFailures ?? 0) + 1,
LastError = ex.ToString(),
Status = "error"
}).ConfigureAwait(false);
throw;
}
}
static async Task PublishStateSurfacesAsync(
AetheriaStateNode node,
AetheriaDaemonHostOptions options,
string updatedAtUtc)
{
var verseHost = await EnsureVerseHostSettingsAsync(node, options, updatedAtUtc).ConfigureAwait(false);
var eveStatus = await node.GetEveCommandAcceptanceStatusAsync().ConfigureAwait(false);
var runtimeSession = await node.GetRuntimeSessionAsync(options.DaemonId).ConfigureAwait(false);
var playerSettings = await node.GetPlayerSettingsAsync().ConfigureAwait(false) ?? new AetheriaPlayerSettings();
var playerSettingsUpdatedAt = string.IsNullOrWhiteSpace(playerSettings.LastUpdatedAtUtc)
? updatedAtUtc
: playerSettings.LastUpdatedAtUtc;
await node.PutOperationsSurfaceAsync(
AetheriaOperationsSurfaceProjector.Build(eveStatus, verseHost, runtimeSession)).ConfigureAwait(false);
await node.PutPlayerSettingsSurfaceAsync(
AetheriaPlayerSettingsSurfaceProjector.Build(playerSettings, playerSettingsUpdatedAt)).ConfigureAwait(false);
await node.PutProviderAdvertisementAsync(
AetheriaProviderAdvertisementProjector.Build(verseHost, node.StatePath, updatedAtUtc)).ConfigureAwait(false);
await node.FlushAsync().ConfigureAwait(false);
}
static async Task PublishRuntimeSessionAsync(
AetheriaStateNode node,
AetheriaDaemonHostOptions options,
string startedAtUtc,
string status)
{
var now = DateTimeOffset.UtcNow.ToString("O");
await node.PutRuntimeSessionAsync(new AetheriaRuntimeSession
{
RuntimeId = options.DaemonId,
Role = "verse-daemon",
StartedAtUtc = startedAtUtc,
LastSeenAtUtc = now,
Status = status
}).ConfigureAwait(false);
await PublishStateSurfacesAsync(node, options, now).ConfigureAwait(false);
}
static async Task EnsureWorldDocumentAsync(AetheriaStateNode node)
{
var now = DateTimeOffset.UtcNow.ToString("O");
var existing = await node.GetWorldAsync().ConfigureAwait(false);
if (existing != null)
{
existing.UpdatedAtUtc = now;
await node.PutWorldAsync(existing).ConfigureAwait(false);
return;
}
await node.PutWorldAsync(new AetheriaWorldState
{
Name = "Aetheria",
WorldId = "aetheria",
SchemaEpoch = 1,
CreatedAtUtc = now,
UpdatedAtUtc = now
}).ConfigureAwait(false);
}
static async Task<AetheriaVerseHostSettings> EnsureVerseHostSettingsAsync(
AetheriaStateNode node,
AetheriaDaemonHostOptions options,
string now)
{
var existing = await node.GetVerseHostSettingsAsync().ConfigureAwait(false);
var normalized = AetheriaVerseHostSettingsNormalizer.Normalize(existing);
normalized.ServiceId = options.DaemonId;
normalized.VerseId = options.VerseId;
normalized.CultMeshAddress = options.CultMeshAddress;
if (existing == null ||
string.IsNullOrWhiteSpace(existing.LastUpdatedAtUtc) ||
!AetheriaVerseHostSettingsNormalizer.Equivalent(existing, normalized))
{
normalized.LastUpdatedAtUtc = now;
await node.PutVerseHostSettingsAsync(normalized).ConfigureAwait(false);
}
return normalized;
}
static AetheriaRuntimeLoadoutTemplateCommit ToLoadoutTemplateCommit(AetheriaLoadoutTemplate template)
{
return new AetheriaRuntimeLoadoutTemplateCommit
{
Name = template.Name ?? "",
OwnerPlayerKey = template.OwnerPlayerKey ?? "",
RootEntity = ToEntityLoadoutCommit(template.RootEntity)
};
}
static AetheriaRuntimeEntityLoadoutCommit ToEntityLoadoutCommit(AetheriaEntityLoadout? entity)
{
entity ??= new AetheriaEntityLoadout();
return new AetheriaRuntimeEntityLoadoutCommit
{
Name = entity.Name ?? "",
Kind = entity.Kind ?? "",
FactionKey = entity.FactionKey ?? "",
Hull = ToLoadoutItemCommit(entity.Hull),
Equipment = ToSlotCommits(entity.Equipment),
CargoBays = ToSlotCommits(entity.CargoBays),
DockingBays = ToSlotCommits(entity.DockingBays),
CargoContents = ToCargoBayCommits(entity.CargoContents),
DockingBayContents = ToCargoBayCommits(entity.DockingBayContents),
DockingBayAssignments = (entity.DockingBayAssignments ?? Array.Empty<int>()).ToArray(),
WeaponGroups = (entity.WeaponGroups ?? Array.Empty<IReadOnlyList<int>>())
.Select(group => (group ?? Array.Empty<int>()).ToArray())
.ToArray(),
Children = (entity.Children ?? Array.Empty<AetheriaEntityLoadout>())
.Select(ToEntityLoadoutCommit)
.ToArray()
};
}
static AetheriaRuntimeLoadoutItemSlotCommit[] ToSlotCommits(
IReadOnlyList<AetheriaLoadoutItemSlot>? slots)
{
return (slots ?? Array.Empty<AetheriaLoadoutItemSlot>())
.Select(slot => new AetheriaRuntimeLoadoutItemSlotCommit
{
X = slot.Position?.X ?? 0,
Y = slot.Position?.Y ?? 0,
Item = ToLoadoutItemCommit(slot.Item)
})
.ToArray();
}
static AetheriaRuntimeCargoBayLoadoutCommit[] ToCargoBayCommits(
IReadOnlyList<AetheriaCargoBayLoadout>? cargoBays)
{
return (cargoBays ?? Array.Empty<AetheriaCargoBayLoadout>())
.Select(cargoBay => new AetheriaRuntimeCargoBayLoadoutCommit
{
Items = ToSlotCommits(cargoBay.Items)
})
.ToArray();
}
static AetheriaRuntimeLoadoutItemCommit ToLoadoutItemCommit(AetheriaLoadoutItem? item)
{
return new AetheriaRuntimeLoadoutItemCommit
{
ItemKey = item?.ItemKey ?? "",
Quality = item?.Quality ?? 0,
Durability = item?.Durability ?? 0,
Quantity = item?.Quantity ?? 0,
Enabled = item?.Enabled ?? false,
OverrideShutdown = item?.OverrideShutdown ?? false
};
}
internal sealed class AetheriaDaemonHostOptions
{
public string StatePath { get; init; } = "";
public string DaemonId { get; init; } = "aetheria-daemon";
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 TimeSpan TickInterval { get; init; } = TimeSpan.FromMilliseconds(50);
public double FixedDeltaSeconds { get; init; } = 0.02;
public bool Once { get; init; }
public static AetheriaDaemonHostOptions Parse(IReadOnlyList<string> args)
{
var root = ReadOption(args, "--root");
if (string.IsNullOrWhiteSpace(root))
root = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", ".."));
var state = ReadOption(args, "--state");
var intervalMs = ReadPositiveInt(args, "--tick-interval-ms") ?? 50;
var fixedDeltaMs = ReadPositiveInt(args, "--fixed-delta-ms") ?? 20;
var daemonId = ReadOption(args, "--daemon-id");
var verseId = ReadOption(args, "--verse-id");
var cultMeshAddress = ReadOption(args, "--cultmesh-address");
var sessionId = ReadOption(args, "--session-id");
return new AetheriaDaemonHostOptions
{
StatePath = string.IsNullOrWhiteSpace(state)
? AetheriaStatePaths.ResolveDefaultStatePath(root)
: Path.GetFullPath(state),
DaemonId = string.IsNullOrWhiteSpace(daemonId) ? "aetheria-daemon" : daemonId,
SessionId = string.IsNullOrWhiteSpace(sessionId) ? "local" : sessionId,
VerseId = string.IsNullOrWhiteSpace(verseId) ? "aetheria.local" : verseId,
CultMeshAddress = string.IsNullOrWhiteSpace(cultMeshAddress)
? "cultmesh://aetheria.local/eve/providers/aetheria.daemon"
: cultMeshAddress,
TickInterval = TimeSpan.FromMilliseconds(intervalMs),
FixedDeltaSeconds = fixedDeltaMs / 1000.0,
Once = HasFlag(args, "--once")
};
}
private static string ReadOption(IReadOnlyList<string> args, string name)
{
for (var i = 0; i < args.Count - 1; i++)
{
if (string.Equals(args[i], name, StringComparison.OrdinalIgnoreCase))
return args[i + 1];
}
return "";
}
private static int? ReadPositiveInt(IReadOnlyList<string> args, string name)
{
return int.TryParse(ReadOption(args, name), out var value) && value > 0
? value
: null;
}
private static bool HasFlag(IReadOnlyList<string> args, string name)
{
return args.Any(arg => string.Equals(arg, name, StringComparison.OrdinalIgnoreCase));
}
}

View file

@ -9,6 +9,5 @@
<ItemGroup>
<ProjectReference Include="..\Aetheria.State\Aetheria.State.csproj" />
<ProjectReference Include="..\Aetheria.State.Unity\Aetheria.State.Unity.csproj" />
</ItemGroup>
</Project>

View file

@ -3,7 +3,6 @@ using Aetheria.State.Documents;
using Aetheria.State.Migration;
using GameCult.Caching;
using GameCult.Aetheria.State.Unity;
using GameCult.Eve.Surface;
var root = args.Length > 0 ? args[0] : Directory.GetCurrentDirectory();
var stateDirectory = Path.Combine(Path.GetTempPath(), "aetheria-state-smoke", Guid.NewGuid().ToString("N"));
@ -93,27 +92,73 @@ await using (var node = await AetheriaStateNode.OpenAsync(statePath, "aetheria-s
await node.PutCatalogSurfaceAsync(
AetheriaCatalogSurfaceProjector.Build(node.ReadCatalogSnapshot(), now));
var drainStatus = new AetheriaRuntimeCommitDrainStatus
{
RuntimeId = "smoke-runtime",
StatePath = statePath,
LastPollAtUtc = now,
LastAppliedAtUtc = now,
PendingBeforeApply = 1,
CommandsApplied = 1,
AppliedPlayerSettings = 1,
Status = "ok"
};
await node.PutRuntimeCommitDrainStatusAsync(drainStatus);
var verseHostSettings = AetheriaVerseHostSettingsNormalizer.Normalize(new AetheriaVerseHostSettings
{
LastUpdatedAtUtc = now
});
await node.PutVerseHostSettingsAsync(verseHostSettings);
await node.PutOperationsSurfaceAsync(
AetheriaOperationsSurfaceProjector.Build(drainStatus, verseHostSettings: verseHostSettings));
AetheriaOperationsSurfaceProjector.Build(verseHostSettings: verseHostSettings));
await node.PutProviderAdvertisementAsync(
AetheriaProviderAdvertisementProjector.Build(verseHostSettings, statePath, now));
var daemonProvider = AetheriaRuntimeDaemonProviderAdvertisementDocument.Create(
statePath,
"smoke-daemon",
"aetheria.local",
"cultmesh://aetheria.local/eve/providers/aetheria.daemon");
var daemonHealth = new AetheriaRuntimeDaemonHealthDocument
{
DaemonId = "smoke-daemon",
VerseId = "aetheria.local",
PublishedAtUtc = now,
StatePath = statePath,
FrameId = 7,
ObservedCommandCount = 1,
AppliedCommandCount = 2,
RejectedCommandCount = 0,
Status = "healthy",
CommandBoundaryPath = AetheriaRuntimeStateBoundary.GetDaemonCommandBoundaryPath(statePath)
};
var daemonCommandBoundary = AetheriaRuntimeDaemonCommandBoundaryDocument.Create("smoke-daemon");
var daemonFrame = AetheriaRuntimeDaemonFrameDocument.Create(
new AetheriaRuntimeRunCheckpointCommit
{
RunId = "smoke-run",
CurrentZoneIndex = 0,
CurrentEntityKey = entityKey.ToString()
},
"smoke-daemon",
"smoke-session",
7,
0.14,
0.02);
var daemonGameSurface = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
daemonFrame,
daemonHealth,
daemonCommandBoundary);
await node.PutDaemonProviderAdvertisementAsync(daemonProvider);
await node.PutDaemonHealthAsync(daemonHealth);
await node.PutDaemonCommandBoundaryAsync(daemonCommandBoundary);
using var daemonCommandPort = await AetheriaCommandPort.OpenAsync(
statePath,
"aetheria-state-smoke-command-client");
var daemonSubmitEnvelope = await daemonCommandPort.SubmitDaemonCommandAsync(
AetheriaRuntimeDaemonCommandDocument.Create(
AetheriaRuntimeDaemonCommandKinds.SensorPing,
"aetheria-state-smoke",
"smoke-session",
daemonFrame.FrameId,
entityKey.ToString()));
await using (var commandVerifyNode = await AetheriaStateNode.OpenAsync(
statePath,
"aetheria-state-smoke-command-check"))
{
if (commandVerifyNode.ReadObservedDaemonCommands().All(command => command.CommandId != daemonSubmitEnvelope!.CommandId))
throw new InvalidOperationException("Runtime command port daemon submission did not appear as a typed state record.");
}
await node.PutDaemonFrameAsync(daemonFrame);
await node.PutDaemonGameSurfaceAsync(AetheriaRuntimeEveSurfaceStateProjector.ToState(daemonGameSurface));
await node.PutRuntimeSessionAsync(new AetheriaRuntimeSession
{
RuntimeId = "smoke-runtime",
@ -123,7 +168,6 @@ await using (var node = await AetheriaStateNode.OpenAsync(statePath, "aetheria-s
Status = "running"
});
await node.PutOperationsSurfaceAsync(AetheriaOperationsSurfaceProjector.Build(
drainStatus,
verseHostSettings: verseHostSettings,
runtimeSession: await node.GetRuntimeSessionAsync("smoke-runtime")));
@ -488,62 +532,64 @@ await using (var node = await AetheriaStateNode.OpenAsync(statePath, "aetheria-s
await node.GetPlayerSettingsAsync(),
now));
AetheriaRuntimeEveCommandLog.QueueCommand(
using var eveCommandPort = await AetheriaCommandPort.OpenAsync(
statePath,
new EveSurfaceCommandRequest(
AetheriaProviderAdvertisementProjector.ProviderId,
AetheriaCatalogSurfaceProjector.SurfaceId,
"aetheria.catalog.refresh",
new Dictionary<string, string> { ["source"] = "state-smoke" },
DateTimeOffset.UtcNow,
"aetheria-state-smoke"));
AetheriaRuntimeEveCommandLog.QueueCommand(
statePath,
new EveSurfaceCommandRequest(
AetheriaProviderAdvertisementProjector.ProviderId,
AetheriaPlayerSettingsSurfaceProjector.SurfaceId,
"aetheria.player_settings.gameplay.significant_digits.increment",
new Dictionary<string, string> { ["source"] = "state-smoke" },
DateTimeOffset.UtcNow,
"aetheria-state-smoke"));
AetheriaRuntimeEveCommandLog.QueueCommand(
statePath,
new EveSurfaceCommandRequest(
AetheriaProviderAdvertisementProjector.ProviderId,
AetheriaPlayerSettingsSurfaceProjector.SurfaceId,
"aetheria.player_settings.graphics.show_asteroids.toggle",
new Dictionary<string, string> { ["source"] = "state-smoke" },
DateTimeOffset.UtcNow,
"aetheria-state-smoke"));
AetheriaRuntimeEveCommandLog.QueueCommand(
statePath,
new EveSurfaceCommandRequest(
AetheriaProviderAdvertisementProjector.ProviderId,
AetheriaCatalogSurfaceProjector.SurfaceId,
"aetheria-state-smoke-eve-command-client");
var eveSubmitEnvelope = await eveCommandPort.SubmitEveCommandAsync(
AetheriaRuntimeEveCommandClient.ToDocument(
AetheriaRuntimeEveCommands.SubmitCatalogCommand(
statePath,
AetheriaRuntimeCatalogCommands.Refresh,
"aetheria-state-smoke")));
await using (var commandVerifyNode = await AetheriaStateNode.OpenAsync(
statePath,
"aetheria-state-smoke-eve-command-check"))
{
if (commandVerifyNode.ReadObservedEveCommands().All(command => command.CommandId != eveSubmitEnvelope!.CommandId))
throw new InvalidOperationException("Runtime command port Eve submission did not appear as a typed state record.");
}
await node.SubmitEveCommandAsync(AetheriaRuntimeEveCommandClient.ToDocument(
AetheriaRuntimeEveCommands.SubmitCatalogCommand(
statePath,
AetheriaRuntimeCatalogCommands.Refresh,
"aetheria-state-smoke")));
await node.SubmitEveCommandAsync(AetheriaRuntimeEveCommandClient.ToDocument(
AetheriaRuntimeEveCommands.SubmitPlayerSettingsCommand(
statePath,
AetheriaRuntimePlayerSettingsCommands.IncrementSignificantDigits,
new AetheriaRuntimePlayerSettingsCommandBody(),
"aetheria-state-smoke")));
await node.SubmitEveCommandAsync(AetheriaRuntimeEveCommandClient.ToDocument(
AetheriaRuntimeEveCommands.SubmitPlayerSettingsCommand(
statePath,
AetheriaRuntimePlayerSettingsCommands.ToggleShowAsteroidsInMinimap,
new AetheriaRuntimePlayerSettingsCommandBody(),
"aetheria-state-smoke")));
await node.SubmitEveCommandAsync(AetheriaRuntimeEveCommandClient.ToDocument(
AetheriaRuntimeEveCommands.SubmitCatalogCommand(
statePath,
"aetheria.catalog.unknown",
new Dictionary<string, string> { ["source"] = "state-smoke" },
DateTimeOffset.UtcNow,
"aetheria-state-smoke"));
var eveCommandReport = await AetheriaEveCommandBridge.ApplyPendingAsync(node);
var eveCommandStatus = new AetheriaEveCommandDrainStatus
"aetheria-state-smoke")));
var eveCommandReport = await AetheriaEveCommandBridge.AcceptObservedAsync(node);
var eveCommandStatus = new AetheriaEveCommandAcceptanceStatus
{
RuntimeId = "smoke-runtime",
StatePath = statePath,
LastPollAtUtc = now,
LastAcceptedAtUtc = now,
PendingBeforeApply = 4,
CommandsAccepted = eveCommandReport.AcceptedPaths.Length,
ObservedBeforeAccept = 4,
CommandsAccepted = eveCommandReport.AcceptedCommandIds.Length,
CommandsRejected = eveCommandReport.RejectedCommands,
AppliedCatalogRefreshes = eveCommandReport.AppliedCatalogRefreshes,
AppliedOperationsRefreshes = eveCommandReport.AppliedOperationsRefreshes,
AppliedPlayerSettingsCommands = eveCommandReport.AppliedPlayerSettingsCommands,
AppliedCatalogRefreshes = eveCommandReport.AcceptedCatalogRefreshes,
AppliedOperationsRefreshes = eveCommandReport.AcceptedOperationsRefreshes,
AppliedPlayerSettingsCommands = eveCommandReport.AcceptedPlayerSettingsCommands,
AccountedCommandIds = eveCommandReport.AccountedCommandIds,
LastRejectedCommand = eveCommandReport.LastRejectedCommand,
LastRejectedReason = eveCommandReport.LastRejectedReason,
Status = eveCommandReport.RejectedCommands > 0 ? "rejected" : "ok"
};
await node.PutEveCommandDrainStatusAsync(eveCommandStatus);
await node.PutEveCommandAcceptanceStatusAsync(eveCommandStatus);
await node.PutOperationsSurfaceAsync(AetheriaOperationsSurfaceProjector.Build(
drainStatus,
eveCommandStatus,
verseHostSettings: await node.GetVerseHostSettingsAsync(),
runtimeSession: await node.GetRuntimeSessionAsync("smoke-runtime")));
@ -559,11 +605,15 @@ await using (var reopened = await AetheriaStateNode.OpenAsync(statePath, "aether
var nameFile = await reopened.GetNameFileByLegacyIdAsync(nameFileLegacyId);
var quarantine = await reopened.GetLegacyCatalogQuarantineAsync();
var catalogSurface = await reopened.GetCatalogSurfaceAsync();
var drainStatus = await reopened.GetRuntimeCommitDrainStatusAsync();
var eveCommandStatus = await reopened.GetEveCommandDrainStatusAsync();
var eveCommandStatus = await reopened.GetEveCommandAcceptanceStatusAsync();
var operationsSurface = await reopened.GetOperationsSurfaceAsync();
var playerSettingsSurface = await reopened.GetPlayerSettingsSurfaceAsync();
var advertisement = await reopened.GetProviderAdvertisementAsync();
var daemonProvider = await reopened.GetDaemonProviderAdvertisementAsync();
var daemonHealth = await reopened.GetDaemonHealthAsync();
var daemonCommandBoundary = await reopened.GetDaemonCommandBoundaryAsync();
var daemonFrame = await reopened.GetDaemonFrameAsync();
var daemonGameSurface = await reopened.GetDaemonGameSurfaceAsync();
var runtimeSession = await reopened.GetRuntimeSessionAsync("smoke-runtime");
var playerSettings = await reopened.GetPlayerSettingsAsync();
var loadout = await reopened.GetLoadoutTemplateAsync(loadoutKey);
@ -602,32 +652,71 @@ await using (var reopened = await AetheriaStateNode.OpenAsync(statePath, "aether
throw new InvalidOperationException("Eve catalog surface did not survive flush/reopen.");
}
if (drainStatus?.RuntimeId != "smoke-runtime" ||
drainStatus.CommandsApplied != 1 ||
eveCommandStatus?.CommandsAccepted != 3 ||
if (eveCommandStatus?.CommandsAccepted != 3 ||
eveCommandStatus.CommandsRejected != 1 ||
eveCommandStatus.AppliedCatalogRefreshes != 1 ||
eveCommandStatus.AppliedPlayerSettingsCommands != 2 ||
!eveCommandStatus.LastRejectedReason.Contains("not advertised", StringComparison.Ordinal) ||
operationsSurface?.Surface.Id != AetheriaOperationsSurfaceProjector.SurfaceId ||
!operationsSurface.Surface.Root.Children.Any(child => child.Id == "aetheria.operations.eveCommandDrain") ||
operationsSurface.Surface.Root.Children.Any(child => child.Id == "aetheria.operations.commitDrain") ||
!operationsSurface.Surface.Root.Children.Any(child => child.Id == "aetheria.operations.runtimeSession"))
{
throw new InvalidOperationException("Runtime commit/Eve command drain status or operations surface did not survive flush/reopen.");
throw new InvalidOperationException("Eve request acceptance status or operations surface did not survive flush/reopen.");
}
if (advertisement?.ProviderId != AetheriaProviderAdvertisementProjector.ProviderId ||
advertisement.Surfaces.Length < 3 ||
advertisement.Surfaces.Length < 7 ||
!advertisement.Surfaces.Any(surface => surface.SurfaceId == AetheriaCatalogSurfaceProjector.SurfaceId) ||
!advertisement.Surfaces.Any(surface => surface.SurfaceId == AetheriaOperationsSurfaceProjector.SurfaceId) ||
!advertisement.Surfaces.Any(surface => surface.SurfaceId == AetheriaPlayerSettingsSurfaceProjector.SurfaceId) ||
!advertisement.Surfaces.Any(surface =>
surface.SurfaceId == AetheriaRuntimeDaemonGameSurfaceBuilder.SurfaceId &&
surface.Key == AetheriaProviderAdvertisementProjector.DaemonGameSurfaceKey) ||
!advertisement.Surfaces.Any(surface =>
surface.SurfaceId == AetheriaRuntimeDaemonGameSurfaceBuilder.TuiSurfaceId &&
surface.Key == AetheriaProviderAdvertisementProjector.DaemonGameTuiSurfaceKey) ||
!advertisement.Surfaces.Any(surface =>
surface.SurfaceId == AetheriaRuntimeDaemonEditorSurfaceBuilder.SurfaceId &&
surface.Key == AetheriaProviderAdvertisementProjector.DaemonEditorSurfaceKey) ||
!advertisement.Surfaces.Any(surface =>
surface.SurfaceId == AetheriaRuntimeDaemonEditorSurfaceBuilder.TuiSurfaceId &&
surface.Key == AetheriaProviderAdvertisementProjector.DaemonEditorTuiSurfaceKey) ||
!advertisement.Schemas.Contains("aetheria.runtime_session.v1") ||
!advertisement.Schemas.Contains(AetheriaRuntimeDaemonSchemas.ProviderAdvertisement) ||
!advertisement.Schemas.Contains(AetheriaRuntimeDaemonSchemas.Frame) ||
!advertisement.Schemas.Contains(AetheriaRuntimeDaemonSchemas.SoaView) ||
!advertisement.Schemas.Contains(AetheriaRuntimeDaemonSchemas.Health) ||
!advertisement.Schemas.Contains(AetheriaRuntimeDaemonSchemas.CommandBoundary) ||
!advertisement.Schemas.Contains(AetheriaRuntimeDaemonSchemas.GameSurface) ||
!advertisement.Schemas.Contains(AetheriaRuntimeDaemonSchemas.EditorSurface) ||
!advertisement.Witnesses.Any(witness =>
witness.Kind == "cultcache-witness" &&
witness.Ref == AetheriaRuntimeStateBoundary.GetDaemonProviderPath(statePath)) ||
!advertisement.Witnesses.Any(witness =>
witness.Kind == "cultcache-witness" &&
witness.Ref == AetheriaRuntimeStateBoundary.GetDaemonCommandBoundaryPath(statePath)) ||
!advertisement.Commands.Any(command =>
command.Command == "aetheria.daemon.commands" &&
command.Transport == "cultcache-witness") ||
!advertisement.Commands.Any(command => command.Command == "aetheria.player_settings.graphics.show_asteroids.toggle") ||
!advertisement.Schemas.Contains(AetheriaEveCommandBridge.CommandSchema))
{
throw new InvalidOperationException("Aetheria Eve provider advertisement did not survive flush/reopen.");
}
if (daemonProvider?.DaemonId != "smoke-daemon" ||
daemonHealth?.FrameId != 7 ||
daemonCommandBoundary?.BoundaryId != "aetheria.daemon.commands" ||
!daemonCommandBoundary.Commands.Any(command => command.Kind == AetheriaRuntimeDaemonCommandKinds.FireWeaponGroup) ||
daemonFrame?.FrameId != 7 ||
daemonFrame.SessionId != "smoke-session" ||
daemonGameSurface?.Surface.Id != AetheriaRuntimeDaemonGameSurfaceBuilder.SurfaceId ||
daemonGameSurface.Surface.Root.Id != "aetheria.daemon.game.root")
{
throw new InvalidOperationException("Daemon Verse API documents did not survive flush/reopen as typed CultCache records.");
}
if (runtimeSession?.RuntimeId != "smoke-runtime" ||
runtimeSession.Role != "state-smoke" ||
runtimeSession.Status != "running")

View file

@ -9,5 +9,7 @@
<ItemGroup>
<ProjectReference Include="..\Aetheria.State.Unity\Aetheria.State.Unity.csproj" />
<ProjectReference Include="..\GameCult.Aetheria.State.Unity.csproj" Aliases="PackageUnity" />
<ProjectReference Include="..\GameCult.Eve.Surface.csproj" />
</ItemGroup>
</Project>

File diff suppressed because it is too large Load diff

View file

@ -8,23 +8,7 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="..\..\Eve\packages\org.gamecult.eve.surface\Runtime\EveSurfaceDocument.cs" Link="EveSurfaceDocument.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeCatalogSnapshot.cs" Link="AetheriaRuntimeCatalogSnapshot.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeCatalogStore.cs" Link="AetheriaRuntimeCatalogStore.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeClientTargetCommands.cs" Link="AetheriaRuntimeClientTargetCommands.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeClientTargetSurfaceBuilder.cs" Link="AetheriaRuntimeClientTargetSurfaceBuilder.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeClientTargetStore.cs" Link="AetheriaRuntimeClientTargetStore.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeVerseDiscovery.cs" Link="AetheriaRuntimeVerseDiscovery.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeStateBoundary.cs" Link="AetheriaRuntimeStateBoundary.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeStateBoot.cs" Link="AetheriaRuntimeStateBoot.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeVerseReplicaBridge.cs" Link="AetheriaRuntimeVerseReplicaBridge.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeStateReader.cs" Link="AetheriaRuntimeStateReader.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeBehaviorMetadata.cs" Link="AetheriaRuntimeBehaviorMetadata.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeBehaviorKinds.cs" Link="AetheriaRuntimeBehaviorKinds.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeItemCategories.cs" Link="AetheriaRuntimeItemCategories.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimePendingCultCacheStore.cs" Link="AetheriaRuntimePendingCultCacheStore.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeEveCommandLog.cs" Link="AetheriaRuntimeEveCommandLog.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeStateCommitLog.cs" Link="AetheriaRuntimeStateCommitLog.cs" />
<ProjectReference Include="..\Aetheria.State\Aetheria.State.csproj" />
<ProjectReference Include="..\GameCult.Aetheria.State.Unity.csproj" />
</ItemGroup>
</Project>

View file

@ -78,7 +78,8 @@ internal static class AetheriaRuntimeCatalogSnapshotMapper
stat.Max,
stat.HeatExponentMultiplier,
stat.DurabilityExponentMultiplier,
stat.QualityExponent);
stat.QualityExponent,
null);
}
private static AetheriaRuntimeShapeCell FromState(AetheriaShapeCell cell)

View file

@ -10,5 +10,6 @@
<ItemGroup>
<ProjectReference Include="..\Aetheria.State\Aetheria.State.csproj" />
<ProjectReference Include="..\Aetheria.State.Unity\Aetheria.State.Unity.csproj" />
<ProjectReference Include="..\GameCult.Aetheria.State.Unity.csproj" />
</ItemGroup>
</Project>

View file

@ -4755,6 +4755,7 @@ static void RequireClientTargetBootAuthority(string root)
var boundaryPath = Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeStateBoundary.cs");
var bootPath = Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeStateBoot.cs");
var clientTargetStorePath = Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeClientTargetStore.cs");
var clientTargetCommandsPath = Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeClientTargetCommands.cs");
var aetheriaStatePath = Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaState.cs");
var verseDiscoveryPath = Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeVerseDiscovery.cs");
var replicaBridgePath = Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeVerseReplicaBridge.cs");
@ -4770,6 +4771,7 @@ static void RequireClientTargetBootAuthority(string root)
boundaryPath,
bootPath,
clientTargetStorePath,
clientTargetCommandsPath,
aetheriaStatePath,
verseDiscoveryPath,
replicaBridgePath,
@ -4795,6 +4797,7 @@ static void RequireClientTargetBootAuthority(string root)
var boundary = File.ReadAllText(boundaryPath);
var boot = File.ReadAllText(bootPath);
var clientTargetStore = File.ReadAllText(clientTargetStorePath);
var clientTargetCommands = File.ReadAllText(clientTargetCommandsPath);
var aetheriaState = File.ReadAllText(aetheriaStatePath);
var verseDiscovery = File.ReadAllText(verseDiscoveryPath);
var replicaBridge = File.ReadAllText(replicaBridgePath);
@ -4941,9 +4944,6 @@ static void RequireClientTargetBootAuthority(string root)
"public AetheriaRuntimeClientTargetDocument RequestStateFilePath(string stateFilePath)",
"public AetheriaRuntimeClientTargetDocument RequestDiscoveryEndpoints(IEnumerable<string>? discoveryEndpoints)",
"public AetheriaRuntimeClientTargetDocument SelectDiscoveredVerse(",
"public bool TryRequest(AetheriaClientTargetOperation operation, out AetheriaRuntimeClientTargetDocument? document)",
"public AetheriaRuntimeClientTargetDocument Request(AetheriaClientTargetOperation operation)",
"AetheriaClientTargetOperationKind.SelectDiscoveredVerse",
"AetheriaRuntimeStateBoundary.GetClientTargetPath(_gameDataDirectory)",
"AetheriaRuntimeStateBoundary.GetStateFilePath(_gameDataDirectory)",
"AetheriaRuntimeVerseDiscovery.RefreshClientTarget(",
@ -4969,6 +4969,15 @@ static void RequireClientTargetBootAuthority(string root)
"Aetheria state sugar still exposes Apply vocabulary for client-target edits; Unity should request typed target changes.");
}
if (aetheriaState.Contains("public bool TryRequest(AetheriaClientTargetOperation", StringComparison.Ordinal) ||
aetheriaState.Contains("public AetheriaRuntimeClientTargetDocument Request(AetheriaClientTargetOperation", StringComparison.Ordinal) ||
clientTargetCommands.Contains("public readonly struct AetheriaClientTargetOperation", StringComparison.Ordinal) ||
clientTargetCommands.Contains("public enum AetheriaClientTargetOperationKind", StringComparison.Ordinal))
{
throw new InvalidOperationException(
"Aetheria client-target sugar still exposes generic operation-bag dispatch instead of native request methods plus a surface adapter.");
}
var forbiddenClientTargetSetters = new[]
{
"public AetheriaRuntimeClientTargetDocument SetTitle(",
@ -5081,8 +5090,7 @@ static void RequireClientTargetBootAuthority(string root)
"AetheriaState.At(ActionGameManager.GameDataDirectory)",
".ClientTarget",
"TryRequestClientTargetCommand(request)",
"AetheriaRuntimeClientTargetSurfaceCommands.TryRead(request, out var operation)",
".ClientTarget.Request(operation)",
"AetheriaRuntimeClientTargetSurfaceCommands.TryRequest(",
"ProjectMainMenuSurfaceState(",
"AetheriaRuntimeMainMenuSurfaceBuilder.BuildRoot(",
"AetheriaRuntimeMainMenuSurfaceBuilder.BuildSettings(",
@ -5424,9 +5432,6 @@ static void RequireVerseSettingsShellAndBridge(string root)
"public AetheriaRuntimeClientTargetDocument RequestStateFilePath(string stateFilePath)",
"public AetheriaRuntimeClientTargetDocument RequestDiscoveryEndpoints(IEnumerable<string>? discoveryEndpoints)",
"public AetheriaRuntimeClientTargetDocument SelectDiscoveredVerse(",
"public bool TryRequest(AetheriaClientTargetOperation operation, out AetheriaRuntimeClientTargetDocument? document)",
"public AetheriaRuntimeClientTargetDocument Request(AetheriaClientTargetOperation operation)",
"AetheriaClientTargetOperationKind.SelectDiscoveredVerse",
"AetheriaRuntimeClientTargetStore.Update(",
"AetheriaRuntimeVerseDiscovery.RefreshClientTarget(",
"AetheriaRuntimeVerseReplicaBridge.Sync(",
@ -5449,6 +5454,15 @@ static void RequireVerseSettingsShellAndBridge(string root)
"Aetheria state sugar still exposes Apply vocabulary for Verse settings edits; Unity should request typed target changes.");
}
if (aetheriaState.Contains("public bool TryRequest(AetheriaClientTargetOperation", StringComparison.Ordinal) ||
aetheriaState.Contains("public AetheriaRuntimeClientTargetDocument Request(AetheriaClientTargetOperation", StringComparison.Ordinal) ||
clientTargetCommands.Contains("public readonly struct AetheriaClientTargetOperation", StringComparison.Ordinal) ||
clientTargetCommands.Contains("public enum AetheriaClientTargetOperationKind", StringComparison.Ordinal))
{
throw new InvalidOperationException(
"Verse settings sugar still exposes generic operation-bag dispatch instead of native request methods plus a surface adapter.");
}
var forbiddenClientTargetSetters = new[]
{
"public AetheriaRuntimeClientTargetDocument SetTitle(",
@ -5536,8 +5550,7 @@ static void RequireVerseSettingsShellAndBridge(string root)
"AetheriaRuntimeEveCommands.TrySendVerseHostCommand(",
"AetheriaState.At(ActionGameManager.GameDataDirectory)",
".ClientTarget",
"AetheriaRuntimeClientTargetSurfaceCommands.TryRead(request, out var operation)",
".ClientTarget.Request(operation)",
"AetheriaRuntimeClientTargetSurfaceCommands.TryRequest(",
"AetheriaRuntimeEveCommands.TrySendVerseHostCommand("
};
var missingMainMenuSymbols = requiredMainMenuSymbols
@ -5892,7 +5905,6 @@ static void RequireTypedDaemonCommandPayloads(string root)
"public AetheriaRuntimeDaemonCommandEnvelope Send(",
"public bool TrySend(",
"ReadObservedDaemonCommands()",
"DeleteDaemonCommandAsync(",
"_operationClient.Create(",
"_operationClient.TrySend(command, out var envelope, out var error)",
"command.ActionBarBinding.Kind",
@ -6275,12 +6287,10 @@ static void RequireDaemonVersePublication(string root)
string.Join(", ", survivingDaemonQueueSymbols));
}
if (daemonDocuments.Contains("public int PendingBeforeApply", StringComparison.Ordinal) &&
(!daemonDocuments.Contains("[IgnoreMember]", StringComparison.Ordinal) ||
!daemonDocuments.Contains("public int ObservedCommandCount", StringComparison.Ordinal)))
if (daemonDocuments.Contains("PendingBeforeApply", StringComparison.Ordinal))
{
throw new InvalidOperationException(
"Daemon health exposes PendingBeforeApply without the ObservedCommandCount compatibility wrapper.");
"Daemon health still exposes the legacy PendingBeforeApply queue vocabulary instead of observed Verse command documents.");
}
var requiredProviderAdvertisementSymbols = new[]
@ -6339,7 +6349,9 @@ static void RequireDaemonVersePublication(string root)
"AetheriaRuntimeDaemonPublicationStore.PublishEditorSurface",
"AetheriaRuntimeDaemonPublicationStore.PublishEditorTuiSurface",
"ObservedCommands",
"ObservedCommandCount = commands.Length",
"AccountedCommandIds",
"frame.AccountedCommandIds = accountedBeforeTick",
"ObservedCommandCount = observedCommands.Length",
"PublicationSource = \"daemon-published\""
};
var missingTickSymbols = requiredTickSymbols
@ -6365,10 +6377,8 @@ static void RequireDaemonVersePublication(string root)
"PutDaemonGameSurfaceAsync(AetheriaRuntimeEveSurfaceStateProjector.ToState(gameSurface))",
"PutDaemonEditorTuiSurfaceAsync(AetheriaRuntimeEveSurfaceStateProjector.ToState(editorTuiSurface))",
"AetheriaEveCommandBridge.AcceptObservedAsync(",
"!options.KeepAccountedCommandRecords",
"ReadObservedDaemonCommands()",
"DeleteAccountedDaemonCommandsAsync(node, result)",
"DeleteDaemonCommandAsync(commandId)",
"AccountedCommandIds = currentFrame?.AccountedCommandIds ?? Array.Empty<string>()",
"AetheriaRuntimeDaemonTickRunner.Tick(",
"AetheriaProviderAdvertisementProjector.Build(verseHost, node.StatePath, updatedAtUtc)",
"AetheriaOperationsSurfaceProjector.Build(eveStatus, verseHost, runtimeSession)",
@ -6397,6 +6407,21 @@ static void RequireDaemonVersePublication(string root)
"MapGet(",
"JsonSerializer.Serialize"
};
if (daemonHostSource.Contains("KeepAccountedCommandRecords", StringComparison.Ordinal) ||
daemonHostSource.Contains("--keep-accounted-command-records", StringComparison.Ordinal))
{
throw new InvalidOperationException(
"Aetheria.State.Daemon still exposes command-retention toggles after adopting receipt-ledger command accounting.");
}
if (stateNode.Contains("DeleteDaemonCommandAsync(", StringComparison.Ordinal) ||
daemonHostSource.Contains("DeleteAccountedDaemonCommandsAsync", StringComparison.Ordinal) ||
daemonHostSource.Contains("DeleteDaemonCommandAsync(", StringComparison.Ordinal))
{
throw new InvalidOperationException(
"Daemon command documents are still consumed by deletion instead of accounted by frame receipts.");
}
var survivingDaemonHostSymbols = forbiddenDaemonHostSymbols
.Where(symbol => daemonHostSource.Contains(symbol, StringComparison.Ordinal))
.ToArray();
@ -6643,10 +6668,9 @@ static void RequireTypedEveCommandBodies(string root)
"ToDocument(",
"AcceptObservedAsync(",
"node.ReadObservedEveCommands()",
"node.DeleteEveCommandAsync(",
"AccountedCommandIds",
"SubmitEveCommandAsync(",
"ReadObservedEveCommands(",
"DeleteEveCommandAsync(",
"EveCommandKey(",
"CultNetDocumentBinding.ForDocument<AetheriaRuntimeEveCommandDocument>",
"typeof(AetheriaRuntimeEveCommandDocument)",
@ -6661,6 +6685,19 @@ static void RequireTypedEveCommandBodies(string root)
};
var mainMenu = File.ReadAllText(mainMenuPath);
var typedCommandSources = eveCommandClient + "\n" + runtimeCommandPort + "\n" + eveCommandBridge + "\n" + stateNode + "\n" + documentRegistry + "\n" + actionGameManager + "\n" + mainMenu + "\n" + evePresenter;
if (typedCommandSources.Contains("DeleteEveCommandAsync(", StringComparison.Ordinal))
{
throw new InvalidOperationException(
"Eve command documents are still consumed by deletion instead of accounted by acceptance receipts.");
}
if (eveCommandBridge.Contains("AcceptedPaths", StringComparison.Ordinal) ||
eveCommandBridge.Contains("RejectedPaths", StringComparison.Ordinal))
{
throw new InvalidOperationException(
"Eve command acceptance report still exposes path vocabulary instead of command-id receipts.");
}
var missingTypedCommandSymbols = requiredTypedCommandSymbols
.Where(symbol => !typedCommandSources.Contains(symbol, StringComparison.Ordinal))
.ToArray();

View file

@ -8,15 +8,49 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeStateCommitDocument.cs" Link="AetheriaRuntimeStateCommitDocument.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeStateBoundary.cs" Link="AetheriaRuntimeStateBoundary.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeSnapshotDocuments.cs" Link="AetheriaRuntimeSnapshotDocuments.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeObservedDaemonState.cs" Link="AetheriaRuntimeObservedDaemonState.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeDaemonDocuments.cs" Link="AetheriaRuntimeDaemonDocuments.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeDaemonSoaDocuments.cs" Link="AetheriaRuntimeDaemonSoaDocuments.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeDaemonSoaViewIndex.cs" Link="AetheriaRuntimeDaemonSoaViewIndex.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeDaemonSoaViewStore.cs" Link="AetheriaRuntimeDaemonSoaViewStore.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeDaemonOperationClient.cs" Link="AetheriaRuntimeDaemonOperationClient.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeCommandPort.cs" Link="AetheriaRuntimeCommandPort.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeDaemonOperations.cs" Link="AetheriaRuntimeDaemonOperations.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeClientTargetCommands.cs" Link="AetheriaRuntimeClientTargetCommands.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeDaemonIntentState.cs" Link="AetheriaRuntimeDaemonIntentState.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeDaemonFrameStore.cs" Link="AetheriaRuntimeDaemonFrameStore.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeDaemonPublicationStore.cs" Link="AetheriaRuntimeDaemonPublicationStore.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeDaemonTickRunner.cs" Link="AetheriaRuntimeDaemonTickRunner.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeLoadoutSnapshotProjector.cs" Link="AetheriaRuntimeLoadoutSnapshotProjector.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeEveCommandDocument.cs" Link="AetheriaRuntimeEveCommandDocument.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimePendingCultCacheStore.cs" Link="AetheriaRuntimePendingCultCacheStore.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeEveCommandClient.cs" Link="AetheriaRuntimeEveCommandClient.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeEveSurfaceAdapter.cs" Link="AetheriaRuntimeEveSurfaceAdapter.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeCultCacheDocumentStore.cs" Link="AetheriaRuntimeCultCacheDocumentStore.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeVerseHostCommands.cs" Link="AetheriaRuntimeVerseHostCommands.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeCatalogCommands.cs" Link="AetheriaRuntimeCatalogCommands.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeOperationsCommands.cs" Link="AetheriaRuntimeOperationsCommands.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeCargoItemDetailsSurfaceBuilder.cs" Link="AetheriaRuntimeCargoItemDetailsSurfaceBuilder.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeEquippedItemDetailsSurfaceBuilder.cs" Link="AetheriaRuntimeEquippedItemDetailsSurfaceBuilder.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeShipSettingsSurfaceBuilder.cs" Link="AetheriaRuntimeShipSettingsSurfaceBuilder.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeZoneDetailsSurfaceBuilder.cs" Link="AetheriaRuntimeZoneDetailsSurfaceBuilder.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeTradeItemDetailsSurfaceBuilder.cs" Link="AetheriaRuntimeTradeItemDetailsSurfaceBuilder.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeInventoryDropdownSurfaceBuilder.cs" Link="AetheriaRuntimeInventoryDropdownSurfaceBuilder.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeTradeInteractionSurfaceBuilder.cs" Link="AetheriaRuntimeTradeInteractionSurfaceBuilder.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeTradeCargoSelectorSurfaceBuilder.cs" Link="AetheriaRuntimeTradeCargoSelectorSurfaceBuilder.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeMainMenuSurfaceBuilder.cs" Link="AetheriaRuntimeMainMenuSurfaceBuilder.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeMenuTabsSurfaceBuilder.cs" Link="AetheriaRuntimeMenuTabsSurfaceBuilder.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimePlayerSettingsSurfaceBuilder.cs" Link="AetheriaRuntimePlayerSettingsSurfaceBuilder.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeDaemonGameSurfaceBuilder.cs" Link="AetheriaRuntimeDaemonGameSurfaceBuilder.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeDaemonEditorSurfaceBuilder.cs" Link="AetheriaRuntimeDaemonEditorSurfaceBuilder.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimePlayerSettingsCommands.cs" Link="AetheriaRuntimePlayerSettingsCommands.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeInputSettingsCommands.cs" Link="AetheriaRuntimeInputSettingsCommands.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeLoadoutTemplateCommands.cs" Link="AetheriaRuntimeLoadoutTemplateCommands.cs" />
<ProjectReference Include="..\..\CultLib\src\GameCult.Caching\GameCult.Caching.csproj" />
<ProjectReference Include="..\..\CultLib\src\GameCult.Caching.MessagePack\GameCult.Caching.MessagePack.csproj" />
<ProjectReference Include="..\..\CultLib\src\GameCult.Mesh\GameCult.Mesh.csproj" />
<ProjectReference Include="..\..\CultLib\src\GameCult.Networking\GameCult.Networking.csproj" />
<ProjectReference Include="..\GameCult.Eve.Surface.csproj" />
</ItemGroup>
</Project>

View file

@ -1,13 +1,14 @@
using System;
using System.Linq;
using Aetheria.State.Documents;
using GameCult.Aetheria.State.Unity;
namespace Aetheria.State;
public static class AetheriaCatalogSurfaceProjector
{
public const string SurfaceKey = "eve:surface:aetheria.catalog.operator";
public const string SurfaceId = "aetheria.catalog.operator";
public const string SurfaceId = AetheriaRuntimeCatalogCommands.SurfaceId;
public static EveSurfaceState Build(AetheriaCatalogSnapshot catalog, string updatedAtUtc, long version = 1)
{
@ -68,7 +69,7 @@ public static class AetheriaCatalogSurfaceProjector
[
new EveCommandTemplate
{
Command = "aetheria.catalog.refresh",
Command = AetheriaRuntimeCatalogCommands.Refresh,
Label = "Refresh",
Transport = "cultmesh"
}

View file

@ -1,5 +1,6 @@
using Aetheria.State.Documents;
using GameCult.Caching;
using GameCult.Aetheria.State.Unity;
using GameCult.Networking;
namespace Aetheria.State;
@ -38,8 +39,14 @@ public static class AetheriaDocumentRegistry
CultNetDocumentBinding.ForDocument<AetheriaZoneState>(registry),
CultNetDocumentBinding.ForDocument<AetheriaEntitySnapshot>(registry),
CultNetDocumentBinding.ForDocument<AetheriaVerseHostSettings>(registry),
CultNetDocumentBinding.ForDocument<AetheriaRuntimeCommitDrainStatus>(registry),
CultNetDocumentBinding.ForDocument<AetheriaEveCommandDrainStatus>(registry),
CultNetDocumentBinding.ForDocument<AetheriaEveCommandAcceptanceStatus>(registry),
CultNetDocumentBinding.ForDocument<AetheriaRuntimeDaemonProviderAdvertisementDocument>(registry),
CultNetDocumentBinding.ForDocument<AetheriaRuntimeDaemonHealthDocument>(registry),
CultNetDocumentBinding.ForDocument<AetheriaRuntimeDaemonCommandBoundaryDocument>(registry),
CultNetDocumentBinding.ForDocument<AetheriaRuntimeDaemonFrameDocument>(registry),
CultNetDocumentBinding.ForDocument<AetheriaRuntimeDaemonSoaViewDocument>(registry),
CultNetDocumentBinding.ForDocument<AetheriaRuntimeDaemonCommandDocument>(registry),
CultNetDocumentBinding.ForDocument<AetheriaRuntimeEveCommandDocument>(registry),
CultNetDocumentBinding.ForDocument<EveSurfaceState>(registry),
CultNetDocumentBinding.ForDocument<EveProviderAdvertisementState>(registry)
});
@ -61,8 +68,14 @@ public static class AetheriaDocumentRegistry
typeof(AetheriaZoneState),
typeof(AetheriaEntitySnapshot),
typeof(AetheriaVerseHostSettings),
typeof(AetheriaRuntimeCommitDrainStatus),
typeof(AetheriaEveCommandDrainStatus),
typeof(AetheriaEveCommandAcceptanceStatus),
typeof(AetheriaRuntimeDaemonProviderAdvertisementDocument),
typeof(AetheriaRuntimeDaemonHealthDocument),
typeof(AetheriaRuntimeDaemonCommandBoundaryDocument),
typeof(AetheriaRuntimeDaemonFrameDocument),
typeof(AetheriaRuntimeDaemonSoaViewDocument),
typeof(AetheriaRuntimeDaemonCommandDocument),
typeof(AetheriaRuntimeEveCommandDocument),
typeof(EveSurfaceState),
typeof(EveProviderAdvertisementState)
];

View file

@ -3,15 +3,18 @@ using GameCult.Aetheria.State.Unity;
namespace Aetheria.State;
public sealed class AetheriaEveCommandApplyReport
public sealed class AetheriaEveCommandAcceptanceReport
{
public int AppliedCatalogRefreshes { get; set; }
public int AppliedOperationsRefreshes { get; set; }
public int AppliedPlayerSettingsCommands { get; set; }
public int AppliedVerseHostCommands { get; set; }
public int AcceptedCatalogRefreshes { get; set; }
public int AcceptedOperationsRefreshes { get; set; }
public int AcceptedPlayerSettingsCommands { get; set; }
public int AcceptedInputSettingsCommands { get; set; }
public int AcceptedLoadoutTemplateCommands { get; set; }
public int AcceptedVerseHostCommands { get; set; }
public int RejectedCommands { get; set; }
public string[] AcceptedPaths { get; set; } = [];
public string[] RejectedPaths { get; set; } = [];
public string[] AcceptedCommandIds { get; set; } = [];
public string[] RejectedCommandIds { get; set; } = [];
public string[] AccountedCommandIds { get; set; } = [];
public string LastRejectedCommand { get; set; } = "";
public string LastRejectedReason { get; set; } = "";
}
@ -20,61 +23,45 @@ public static class AetheriaEveCommandBridge
{
public const string CommandSchema = "gamecult.eve.command.v1";
public static string GetPendingDirectory(string stateFilePath)
{
if (string.IsNullOrWhiteSpace(stateFilePath))
throw new ArgumentException("State file path must be non-empty.", nameof(stateFilePath));
return stateFilePath + ".eve.pending";
}
public static async Task<AetheriaEveCommandApplyReport> ApplyPendingAsync(
public static async Task<AetheriaEveCommandAcceptanceReport> AcceptObservedAsync(
AetheriaStateNode node,
bool deleteAccounted = true)
IEnumerable<string>? accountedCommandIds = null)
{
if (node == null) throw new ArgumentNullException(nameof(node));
var pendingDirectory = GetPendingDirectory(node.StatePath);
var report = new AetheriaEveCommandApplyReport();
var report = new AetheriaEveCommandAcceptanceReport();
var accepted = new List<string>();
var rejected = new List<string>();
if (!Directory.Exists(pendingDirectory))
return report;
foreach (var path in Directory.EnumerateFiles(pendingDirectory, "*.cc").OrderBy(path => path, StringComparer.Ordinal))
var accounted = new HashSet<string>(accountedCommandIds ?? Array.Empty<string>(), StringComparer.Ordinal);
foreach (var command in node.ReadObservedEveCommands()
.Where(command => !string.IsNullOrWhiteSpace(command.CommandId))
.Where(command => !accounted.Contains(command.CommandId)))
{
var command = ReadCommand(path);
var rejection = Validate(command);
if (!string.IsNullOrWhiteSpace(rejection))
{
RecordRejection(report, command, rejection, path, rejected, deleteAccounted);
RecordRejection(report, command, rejection, rejected);
continue;
}
switch (command.Command)
{
case "aetheria.catalog.refresh":
case AetheriaRuntimeCatalogCommands.Refresh:
await node.PutCatalogSurfaceAsync(
AetheriaCatalogSurfaceProjector.Build(node.ReadCatalogSnapshot(), command.IssuedAtUtc)).ConfigureAwait(false);
report.AppliedCatalogRefreshes++;
report.AcceptedCatalogRefreshes++;
break;
case "aetheria.operations.refresh":
var commitStatus = await node.GetRuntimeCommitDrainStatusAsync().ConfigureAwait(false) ??
EmptyCommitDrainStatus(node.StatePath, command.IssuedAtUtc);
var eveStatus = await node.GetEveCommandDrainStatusAsync().ConfigureAwait(false) ??
EmptyEveCommandDrainStatus(node.StatePath, command.IssuedAtUtc);
case AetheriaRuntimeOperationsCommands.Refresh:
var eveStatus = await node.GetEveCommandAcceptanceStatusAsync().ConfigureAwait(false) ??
EmptyEveCommandAcceptanceStatus(node.StatePath, command.IssuedAtUtc);
var verseHostSettings = await node.GetVerseHostSettingsAsync().ConfigureAwait(false);
var runtimeSession = string.IsNullOrWhiteSpace(commitStatus.RuntimeId)
? null
: await node.GetRuntimeSessionAsync(commitStatus.RuntimeId).ConfigureAwait(false);
var runtimeSession = await node.GetRuntimeSessionAsync(eveStatus.RuntimeId).ConfigureAwait(false);
await node.PutOperationsSurfaceAsync(
AetheriaOperationsSurfaceProjector.Build(
commitStatus,
eveStatus,
verseHostSettings,
runtimeSession)).ConfigureAwait(false);
report.AppliedOperationsRefreshes++;
report.AcceptedOperationsRefreshes++;
break;
case GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.Refresh:
case GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.SetPlayerName:
@ -83,32 +70,41 @@ public static class AetheriaEveCommandBridge
case GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.IncrementSignificantDigits:
case GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.CycleNebulaQuality:
case GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.ToggleShowAsteroidsInMinimap:
await ApplyPlayerSettingsCommandAsync(node, command).ConfigureAwait(false);
report.AppliedPlayerSettingsCommands++;
await ExecutePlayerSettingsCommandAsync(node, command).ConfigureAwait(false);
report.AcceptedPlayerSettingsCommands++;
break;
case GameCult.Aetheria.State.Unity.AetheriaRuntimeInputSettingsCommands.Refresh:
case GameCult.Aetheria.State.Unity.AetheriaRuntimeInputSettingsCommands.SetBindingOverride:
case GameCult.Aetheria.State.Unity.AetheriaRuntimeInputSettingsCommands.SetActionBarEnabled:
await ExecuteInputSettingsCommandAsync(node, command).ConfigureAwait(false);
report.AcceptedInputSettingsCommands++;
break;
case GameCult.Aetheria.State.Unity.AetheriaRuntimeLoadoutTemplateCommands.Save:
await ExecuteLoadoutTemplateCommandAsync(node, command).ConfigureAwait(false);
report.AcceptedLoadoutTemplateCommands++;
break;
case GameCult.Aetheria.State.Unity.AetheriaRuntimeVerseHostCommands.Refresh:
case GameCult.Aetheria.State.Unity.AetheriaRuntimeVerseHostCommands.CycleVisibility:
await ApplyVerseHostCommandAsync(node, command).ConfigureAwait(false);
report.AppliedVerseHostCommands++;
await ExecuteVerseHostCommandAsync(node, command).ConfigureAwait(false);
report.AcceptedVerseHostCommands++;
break;
}
accepted.Add(path);
if (deleteAccounted)
File.Delete(path);
accepted.Add(command.CommandId ?? "");
}
await node.FlushAsync().ConfigureAwait(false);
report.AcceptedPaths = accepted.ToArray();
report.RejectedPaths = rejected.ToArray();
report.AcceptedCommandIds = accepted.ToArray();
report.RejectedCommandIds = rejected.ToArray();
report.AccountedCommandIds = accounted
.Concat(report.AcceptedCommandIds)
.Concat(report.RejectedCommandIds)
.Where(commandId => !string.IsNullOrWhiteSpace(commandId))
.Distinct(StringComparer.Ordinal)
.ToArray();
return report;
}
private static AetheriaRuntimeEveCommandDocument ReadCommand(string path)
{
return AetheriaRuntimePendingCultCacheStore.ReadEveCommand(path);
}
private static string Validate(AetheriaRuntimeEveCommandDocument command)
{
if (!string.Equals(command.Schema, CommandSchema, StringComparison.Ordinal))
@ -123,17 +119,21 @@ public static class AetheriaEveCommandBridge
private static bool KnownCommand(string surfaceId, string command)
{
return (string.Equals(surfaceId, AetheriaCatalogSurfaceProjector.SurfaceId, StringComparison.Ordinal) &&
string.Equals(command, "aetheria.catalog.refresh", StringComparison.Ordinal)) ||
(string.Equals(surfaceId, AetheriaOperationsSurfaceProjector.SurfaceId, StringComparison.Ordinal) &&
string.Equals(command, "aetheria.operations.refresh", StringComparison.Ordinal)) ||
return (string.Equals(surfaceId, AetheriaRuntimeCatalogCommands.SurfaceId, StringComparison.Ordinal) &&
AetheriaRuntimeCatalogCommands.IsKnown(command)) ||
(string.Equals(surfaceId, AetheriaRuntimeOperationsCommands.SurfaceId, StringComparison.Ordinal) &&
AetheriaRuntimeOperationsCommands.IsKnown(command)) ||
(string.Equals(surfaceId, AetheriaPlayerSettingsSurfaceProjector.SurfaceId, StringComparison.Ordinal) &&
GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.IsKnown(command)) ||
(string.Equals(surfaceId, GameCult.Aetheria.State.Unity.AetheriaRuntimeInputSettingsCommands.SurfaceId, StringComparison.Ordinal) &&
GameCult.Aetheria.State.Unity.AetheriaRuntimeInputSettingsCommands.IsKnown(command)) ||
(string.Equals(surfaceId, GameCult.Aetheria.State.Unity.AetheriaRuntimeLoadoutTemplateCommands.SurfaceId, StringComparison.Ordinal) &&
GameCult.Aetheria.State.Unity.AetheriaRuntimeLoadoutTemplateCommands.IsKnown(command)) ||
(string.Equals(surfaceId, GameCult.Aetheria.State.Unity.AetheriaRuntimeVerseHostCommands.SurfaceId, StringComparison.Ordinal) &&
GameCult.Aetheria.State.Unity.AetheriaRuntimeVerseHostCommands.IsKnown(command));
}
private static async Task ApplyPlayerSettingsCommandAsync(
private static async Task ExecutePlayerSettingsCommandAsync(
AetheriaStateNode node,
AetheriaRuntimeEveCommandDocument command)
{
@ -145,9 +145,7 @@ public static class AetheriaEveCommandBridge
switch (command.Command)
{
case GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.SetPlayerName:
settings.PlayerName = command.Payload.TryGetValue("value", out var playerName)
? playerName ?? ""
: "";
settings.PlayerName = command.PlayerSettings.PlayerName ?? "";
persistSettings = true;
break;
case GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.CycleTemperatureUnit:
@ -193,7 +191,66 @@ public static class AetheriaEveCommandBridge
.ConfigureAwait(false);
}
private static async Task ApplyVerseHostCommandAsync(
private static async Task ExecuteInputSettingsCommandAsync(
AetheriaStateNode node,
AetheriaRuntimeEveCommandDocument command)
{
var settings = await node.GetPlayerSettingsAsync().ConfigureAwait(false) ?? new AetheriaPlayerSettings();
settings.Input ??= new AetheriaPlayerInputSettings();
var persistSettings = false;
switch (command.Command)
{
case GameCult.Aetheria.State.Unity.AetheriaRuntimeInputSettingsCommands.SetBindingOverride:
var actionName = command.InputSettings.ActionName ?? "";
var inputSystemPath = command.InputSettings.InputSystemPath ?? "";
var bindingIndex = command.InputSettings.BindingIndex;
if (!string.IsNullOrWhiteSpace(actionName) && bindingIndex >= 0)
{
settings.Input.BindingOverrides = settings.Input.BindingOverrides
.Where(binding => !string.Equals(binding.ActionName, actionName, StringComparison.Ordinal) ||
binding.BindingIndex != bindingIndex)
.Concat(new[]
{
new AetheriaInputBindingOverride
{
ActionName = actionName,
BindingIndex = bindingIndex,
BindingPath = inputSystemPath
}
})
.OrderBy(binding => binding.ActionName, StringComparer.Ordinal)
.ThenBy(binding => binding.BindingIndex)
.ToArray();
persistSettings = true;
}
break;
case GameCult.Aetheria.State.Unity.AetheriaRuntimeInputSettingsCommands.SetActionBarEnabled:
var inputPath = command.InputSettings.InputSystemPath ?? "";
var enabled = command.InputSettings.Enabled;
if (!string.IsNullOrWhiteSpace(inputPath))
{
var inputs = settings.Input.ActionBarInputs
.Where(path => !string.Equals(path, inputPath, StringComparison.Ordinal))
.ToList();
if (enabled)
inputs.Add(inputPath);
settings.Input.ActionBarInputs = inputs
.OrderBy(path => path, StringComparer.Ordinal)
.ToArray();
persistSettings = true;
}
break;
}
if (persistSettings)
{
settings.LastUpdatedAtUtc = command.IssuedAtUtc;
await node.PutPlayerSettingsAsync(settings).ConfigureAwait(false);
}
}
private static async Task ExecuteVerseHostCommandAsync(
AetheriaStateNode node,
AetheriaRuntimeEveCommandDocument command)
{
@ -215,17 +272,12 @@ public static class AetheriaEveCommandBridge
await node.PutVerseHostSettingsAsync(normalized).ConfigureAwait(false);
}
var commitStatus = await node.GetRuntimeCommitDrainStatusAsync().ConfigureAwait(false) ??
EmptyCommitDrainStatus(node.StatePath, command.IssuedAtUtc);
var eveStatus = await node.GetEveCommandDrainStatusAsync().ConfigureAwait(false) ??
EmptyEveCommandDrainStatus(node.StatePath, command.IssuedAtUtc);
var runtimeSession = string.IsNullOrWhiteSpace(commitStatus.RuntimeId)
? null
: await node.GetRuntimeSessionAsync(commitStatus.RuntimeId).ConfigureAwait(false);
var eveStatus = await node.GetEveCommandAcceptanceStatusAsync().ConfigureAwait(false) ??
EmptyEveCommandAcceptanceStatus(node.StatePath, command.IssuedAtUtc);
var runtimeSession = await node.GetRuntimeSessionAsync(eveStatus.RuntimeId).ConfigureAwait(false);
await node.PutOperationsSurfaceAsync(
AetheriaOperationsSurfaceProjector.Build(
commitStatus,
eveStatus,
normalized,
runtimeSession)).ConfigureAwait(false);
@ -233,6 +285,18 @@ public static class AetheriaEveCommandBridge
AetheriaProviderAdvertisementProjector.Build(normalized, node.StatePath, command.IssuedAtUtc)).ConfigureAwait(false);
}
private static async Task ExecuteLoadoutTemplateCommandAsync(
AetheriaStateNode node,
AetheriaRuntimeEveCommandDocument command)
{
var commit = command.LoadoutTemplate ?? throw new InvalidOperationException(
$"Loadout template command '{command.CommandId}' is missing its typed payload.");
var loadout = AetheriaRuntimeStateMapper.ToLoadoutTemplate(commit, command.IssuedAtUtc);
await node.PutLoadoutTemplateAsync(
AetheriaRuntimeStateMapper.LoadoutKey(loadout.Name),
loadout).ConfigureAwait(false);
}
private static string Cycle(string current, params string[] values)
{
if (values.Length == 0)
@ -246,37 +310,22 @@ public static class AetheriaEveCommandBridge
}
private static void RecordRejection(
AetheriaEveCommandApplyReport report,
AetheriaEveCommandAcceptanceReport report,
AetheriaRuntimeEveCommandDocument command,
string reason,
string path,
List<string> rejected,
bool deleteAccounted)
List<string> rejected)
{
report.RejectedCommands++;
report.LastRejectedCommand = string.IsNullOrWhiteSpace(command.Command)
? command.CommandId
: command.Command;
report.LastRejectedReason = reason;
rejected.Add(path);
if (deleteAccounted)
File.Delete(path);
rejected.Add(command.CommandId ?? "");
}
private static AetheriaRuntimeCommitDrainStatus EmptyCommitDrainStatus(string statePath, string now)
private static AetheriaEveCommandAcceptanceStatus EmptyEveCommandAcceptanceStatus(string statePath, string now)
{
return new AetheriaRuntimeCommitDrainStatus
{
RuntimeId = "aetheria-state",
StatePath = statePath,
LastPollAtUtc = now,
Status = "idle"
};
}
private static AetheriaEveCommandDrainStatus EmptyEveCommandDrainStatus(string statePath, string now)
{
return new AetheriaEveCommandDrainStatus
return new AetheriaEveCommandAcceptanceStatus
{
RuntimeId = "aetheria-state",
StatePath = statePath,

View file

@ -1,22 +1,21 @@
using Aetheria.State.Documents;
using GameCult.Aetheria.State.Unity;
namespace Aetheria.State;
public static class AetheriaOperationsSurfaceProjector
{
public const string SurfaceKey = "eve:surface:aetheria.operations";
public const string SurfaceId = "aetheria.operations";
public const string SurfaceId = AetheriaRuntimeOperationsCommands.SurfaceId;
public static EveSurfaceState Build(
AetheriaRuntimeCommitDrainStatus drainStatus,
AetheriaEveCommandDrainStatus? eveCommandStatus = null,
AetheriaEveCommandAcceptanceStatus? eveCommandStatus = null,
AetheriaVerseHostSettings? verseHostSettings = null,
AetheriaRuntimeSession? runtimeSession = null,
long version = 1)
{
var normalizedVerseHost = AetheriaVerseHostSettingsNormalizer.Normalize(verseHostSettings);
var updatedAtUtc = LatestTimestamp(
drainStatus.LastPollAtUtc,
eveCommandStatus?.LastPollAtUtc,
normalizedVerseHost.LastUpdatedAtUtc,
runtimeSession?.LastSeenAtUtc);
@ -35,37 +34,22 @@ public static class AetheriaOperationsSurfaceProjector
"surface",
[],
Node(
"aetheria.operations.commitDrain",
"aetheria.operations.eveCommandAcceptance",
"card",
[("title", "Runtime Commit Drain")],
Metric("commitDrain.status", "Status", drainStatus.Status),
Metric("commitDrain.pending", "Pending Before Apply", drainStatus.PendingBeforeApply.ToString()),
Metric("commitDrain.applied", "Commands Applied", drainStatus.CommandsApplied.ToString()),
Metric("commitDrain.settings", "Settings", drainStatus.AppliedPlayerSettings.ToString()),
Metric("commitDrain.loadouts", "Loadouts", drainStatus.AppliedLoadoutTemplates.ToString()),
Metric("commitDrain.runs", "Run Checkpoints", drainStatus.AppliedRunCheckpoints.ToString()),
Metric("commitDrain.failures", "Consecutive Failures", drainStatus.ConsecutiveFailures.ToString()),
[("title", "Eve Request Acceptance")],
Metric("eveCommandAcceptance.status", "Status", eveCommandStatus?.Status ?? "missing"),
Metric("eveCommandAcceptance.observed", "Observed Before Accept", (eveCommandStatus?.ObservedBeforeAccept ?? 0).ToString()),
Metric("eveCommandAcceptance.accepted", "Commands Accepted", (eveCommandStatus?.CommandsAccepted ?? 0).ToString()),
Metric("eveCommandAcceptance.rejected", "Commands Rejected", (eveCommandStatus?.CommandsRejected ?? 0).ToString()),
Metric("eveCommandAcceptance.catalogRefreshes", "Catalog Refreshes", (eveCommandStatus?.AppliedCatalogRefreshes ?? 0).ToString()),
Metric("eveCommandAcceptance.operationsRefreshes", "Operations Refreshes", (eveCommandStatus?.AppliedOperationsRefreshes ?? 0).ToString()),
Metric("eveCommandAcceptance.playerSettings", "Player Settings Commands", (eveCommandStatus?.AppliedPlayerSettingsCommands ?? 0).ToString()),
Metric("eveCommandAcceptance.inputSettings", "Input Settings Commands", (eveCommandStatus?.AppliedInputSettingsCommands ?? 0).ToString()),
Metric("eveCommandAcceptance.loadoutTemplates", "Loadout Template Commands", (eveCommandStatus?.AppliedLoadoutTemplateCommands ?? 0).ToString()),
Metric("eveCommandAcceptance.verseHost", "Verse Host Commands", (eveCommandStatus?.AppliedVerseHostCommands ?? 0).ToString()),
Metric("eveCommandAcceptance.failures", "Consecutive Failures", (eveCommandStatus?.ConsecutiveFailures ?? 0).ToString()),
Row(
"commitDrain.last",
("runtime", drainStatus.RuntimeId),
("lastPoll", drainStatus.LastPollAtUtc),
("lastApplied", drainStatus.LastAppliedAtUtc),
("error", drainStatus.LastError))),
Node(
"aetheria.operations.eveCommandDrain",
"card",
[("title", "Eve Command Drain")],
Metric("eveCommandDrain.status", "Status", eveCommandStatus?.Status ?? "missing"),
Metric("eveCommandDrain.pending", "Pending Before Apply", (eveCommandStatus?.PendingBeforeApply ?? 0).ToString()),
Metric("eveCommandDrain.accepted", "Commands Accepted", (eveCommandStatus?.CommandsAccepted ?? 0).ToString()),
Metric("eveCommandDrain.rejected", "Commands Rejected", (eveCommandStatus?.CommandsRejected ?? 0).ToString()),
Metric("eveCommandDrain.catalogRefreshes", "Catalog Refreshes", (eveCommandStatus?.AppliedCatalogRefreshes ?? 0).ToString()),
Metric("eveCommandDrain.operationsRefreshes", "Operations Refreshes", (eveCommandStatus?.AppliedOperationsRefreshes ?? 0).ToString()),
Metric("eveCommandDrain.playerSettings", "Player Settings Commands", (eveCommandStatus?.AppliedPlayerSettingsCommands ?? 0).ToString()),
Metric("eveCommandDrain.verseHost", "Verse Host Commands", (eveCommandStatus?.AppliedVerseHostCommands ?? 0).ToString()),
Metric("eveCommandDrain.failures", "Consecutive Failures", (eveCommandStatus?.ConsecutiveFailures ?? 0).ToString()),
Row(
"eveCommandDrain.last",
"eveCommandAcceptance.last",
("runtime", eveCommandStatus?.RuntimeId ?? ""),
("lastPoll", eveCommandStatus?.LastPollAtUtc ?? ""),
("lastAccepted", eveCommandStatus?.LastAcceptedAtUtc ?? ""),
@ -101,7 +85,7 @@ public static class AetheriaOperationsSurfaceProjector
[
new EveCommandTemplate
{
Command = "aetheria.operations.refresh",
Command = AetheriaRuntimeOperationsCommands.Refresh,
Label = "Refresh",
Transport = "cultmesh"
}

View file

@ -1,11 +1,18 @@
using Aetheria.State.Documents;
using GameCult.Aetheria.State.Unity;
namespace Aetheria.State;
public static class AetheriaProviderAdvertisementProjector
{
public const string AdvertisementKey = "eve:provider:aetheria";
public const string DaemonGameSurfaceKey = "eve:surface:aetheria.daemon.game";
public const string DaemonGameTuiSurfaceKey = "eve:surface:aetheria.daemon.game.tui";
public const string DaemonEditorSurfaceKey = "eve:surface:aetheria.daemon.editor";
public const string DaemonEditorTuiSurfaceKey = "eve:surface:aetheria.daemon.editor.tui";
public const string ProviderId = "aetheria";
private const string DaemonCommandBoundaryId = "aetheria.daemon.commands";
private const string DaemonWitnessTransport = "cultcache-witness";
public static EveProviderAdvertisementState Build(
AetheriaVerseHostSettings settings,
@ -44,10 +51,17 @@ public static class AetheriaProviderAdvertisementProjector
"aetheria.entity_snapshot.v1",
"aetheria.verse_host_settings.v1",
"aetheria.runtime_session.v1",
"aetheria.runtime_commit_drain_status.v1",
"aetheria.eve_command_drain_status.v1",
"aetheria.eve_command_acceptance_status.v1",
"gamecult.eve.surface.v1",
"gamecult.eve.command.v1"
"gamecult.eve.command.v1",
AetheriaRuntimeDaemonSchemas.ProviderAdvertisement,
AetheriaRuntimeDaemonSchemas.Frame,
AetheriaRuntimeDaemonSchemas.SoaView,
AetheriaRuntimeDaemonSchemas.Health,
AetheriaRuntimeDaemonSchemas.CommandBoundary,
AetheriaRuntimeDaemonSchemas.GameSurface,
AetheriaRuntimeDaemonSchemas.EditorSurface,
AetheriaRuntimeDaemonSchemas.Command
],
Witnesses =
[
@ -56,6 +70,60 @@ public static class AetheriaProviderAdvertisementProjector
Kind = "cultcache",
Ref = statePath,
Summary = "Aetheria typed CultCache state file"
},
new EveProviderWitness
{
Kind = DaemonWitnessTransport,
Ref = AetheriaRuntimeStateBoundary.GetDaemonProviderPath(statePath),
Summary = "Aetheria daemon-owned provider advertisement"
},
new EveProviderWitness
{
Kind = DaemonWitnessTransport,
Ref = AetheriaRuntimeStateBoundary.GetDaemonFramePath(statePath),
Summary = "Aetheria daemon simulation frame"
},
new EveProviderWitness
{
Kind = DaemonWitnessTransport,
Ref = AetheriaRuntimeStateBoundary.GetDaemonSoaViewPath(statePath),
Summary = "Aetheria daemon SoA view for thin clients"
},
new EveProviderWitness
{
Kind = DaemonWitnessTransport,
Ref = AetheriaRuntimeStateBoundary.GetDaemonHealthPath(statePath),
Summary = "Aetheria daemon health publication"
},
new EveProviderWitness
{
Kind = DaemonWitnessTransport,
Ref = AetheriaRuntimeStateBoundary.GetDaemonCommandBoundaryPath(statePath),
Summary = "Aetheria daemon typed command boundary"
},
new EveProviderWitness
{
Kind = DaemonWitnessTransport,
Ref = AetheriaRuntimeStateBoundary.GetDaemonGameSurfacePath(statePath),
Summary = "Aetheria daemon game Eve GUI surface"
},
new EveProviderWitness
{
Kind = DaemonWitnessTransport,
Ref = AetheriaRuntimeStateBoundary.GetDaemonGameTuiSurfacePath(statePath),
Summary = "Aetheria daemon game Eve TUI surface"
},
new EveProviderWitness
{
Kind = DaemonWitnessTransport,
Ref = AetheriaRuntimeStateBoundary.GetDaemonEditorSurfacePath(statePath),
Summary = "Aetheria daemon editor Eve GUI surface"
},
new EveProviderWitness
{
Kind = DaemonWitnessTransport,
Ref = AetheriaRuntimeStateBoundary.GetDaemonEditorTuiSurfacePath(statePath),
Summary = "Aetheria daemon editor Eve TUI surface"
}
],
Surfaces =
@ -74,48 +142,74 @@ public static class AetheriaProviderAdvertisementProjector
{
SurfaceId = AetheriaPlayerSettingsSurfaceProjector.SurfaceId,
Key = AetheriaPlayerSettingsSurfaceProjector.SurfaceKey
},
new EveProviderSurfaceRef
{
SurfaceId = AetheriaRuntimeDaemonGameSurfaceBuilder.SurfaceId,
Key = DaemonGameSurfaceKey
},
new EveProviderSurfaceRef
{
SurfaceId = AetheriaRuntimeDaemonGameSurfaceBuilder.TuiSurfaceId,
Key = DaemonGameTuiSurfaceKey
},
new EveProviderSurfaceRef
{
SurfaceId = AetheriaRuntimeDaemonEditorSurfaceBuilder.SurfaceId,
Key = DaemonEditorSurfaceKey
},
new EveProviderSurfaceRef
{
SurfaceId = AetheriaRuntimeDaemonEditorSurfaceBuilder.TuiSurfaceId,
Key = DaemonEditorTuiSurfaceKey
}
],
Commands =
[
new EveProviderCommandRef
{
Command = "aetheria.catalog.refresh",
Command = DaemonCommandBoundaryId,
Transport = DaemonWitnessTransport,
Summary = "Aetheria daemon typed command boundary"
},
new EveProviderCommandRef
{
Command = AetheriaRuntimeCatalogCommands.Refresh,
Summary = "Refresh catalog projection"
},
new EveProviderCommandRef
{
Command = "aetheria.operations.refresh",
Command = AetheriaRuntimeOperationsCommands.Refresh,
Summary = "Refresh operations projection"
},
new EveProviderCommandRef
{
Command = GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.Refresh,
Command = AetheriaRuntimePlayerSettingsCommands.Refresh,
Summary = "Refresh player settings projection"
},
new EveProviderCommandRef
{
Command = GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.CycleTemperatureUnit,
Command = AetheriaRuntimePlayerSettingsCommands.CycleTemperatureUnit,
Summary = "Cycle the typed player temperature unit"
},
new EveProviderCommandRef
{
Command = GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.DecrementSignificantDigits,
Command = AetheriaRuntimePlayerSettingsCommands.DecrementSignificantDigits,
Summary = "Decrease typed player significant digits"
},
new EveProviderCommandRef
{
Command = GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.IncrementSignificantDigits,
Command = AetheriaRuntimePlayerSettingsCommands.IncrementSignificantDigits,
Summary = "Increase typed player significant digits"
},
new EveProviderCommandRef
{
Command = GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.CycleNebulaQuality,
Command = AetheriaRuntimePlayerSettingsCommands.CycleNebulaQuality,
Summary = "Cycle typed player nebula quality"
},
new EveProviderCommandRef
{
Command = GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.ToggleShowAsteroidsInMinimap,
Command = AetheriaRuntimePlayerSettingsCommands.ToggleShowAsteroidsInMinimap,
Summary = "Toggle typed player minimap asteroid visibility"
}
]

View file

@ -1,756 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Aetheria.State.Documents;
using GameCult.Aetheria.State.Unity;
using GameCult.Caching;
namespace Aetheria.State;
public sealed class AetheriaRuntimeCommitApplyReport
{
public int AppliedPlayerSettings { get; set; }
public int AppliedLoadoutTemplates { get; set; }
public int AppliedRunCheckpoints { get; set; }
public string[] AppliedPaths { get; set; } = [];
}
public static class AetheriaRuntimeCommitLogApplier
{
public static async Task<AetheriaRuntimeCommitApplyReport> ApplyPendingAsync(
AetheriaStateNode node,
bool deleteApplied = true)
{
if (node == null) throw new ArgumentNullException(nameof(node));
var pendingDirectory = node.StatePath + ".pending";
var report = new AetheriaRuntimeCommitApplyReport();
var applied = new List<string>();
if (!Directory.Exists(pendingDirectory))
return report;
foreach (var path in Directory.EnumerateFiles(pendingDirectory, "*.cc").OrderBy(path => path, StringComparer.Ordinal))
{
var command = ReadCommand(path);
switch (command.Kind)
{
case "player_settings":
var settings = ToPlayerSettings(
command.PlayerSettings ?? throw MissingPayload(command),
command.CreatedAtUtc);
await node.PutPlayerSettingsAsync(settings).ConfigureAwait(false);
await node.PutPlayerSettingsSurfaceAsync(
AetheriaPlayerSettingsSurfaceProjector.Build(settings, command.CreatedAtUtc))
.ConfigureAwait(false);
report.AppliedPlayerSettings++;
break;
case "loadout_template":
var loadout = ToLoadoutTemplate(
command.LoadoutTemplate ?? throw MissingPayload(command),
command.CreatedAtUtc);
await node.PutLoadoutTemplateAsync(LoadoutKey(loadout.Name), loadout).ConfigureAwait(false);
report.AppliedLoadoutTemplates++;
break;
case "run_checkpoint":
await ApplyRunCheckpointAsync(
node,
command.RunCheckpoint ?? throw MissingPayload(command),
command.CreatedAtUtc)
.ConfigureAwait(false);
report.AppliedRunCheckpoints++;
break;
default:
throw new InvalidDataException($"Unknown Aetheria runtime commit kind '{command.Kind}' in {path}.");
}
applied.Add(path);
if (deleteApplied)
File.Delete(path);
}
await node.FlushAsync().ConfigureAwait(false);
report.AppliedPaths = applied.ToArray();
return report;
}
private static AetheriaRuntimeStateCommitDocument ReadCommand(string path)
{
var command = AetheriaRuntimePendingCultCacheStore.ReadStateCommit(path);
if (!string.Equals(command.Schema, AetheriaRuntimeStateCommitDocument.SchemaId, StringComparison.Ordinal))
throw new InvalidDataException($"Unexpected Aetheria runtime commit schema '{command.Schema}' in {path}.");
return command;
}
private static AetheriaPlayerSettings ToPlayerSettings(
AetheriaRuntimePlayerSettingsCommit settings,
string updatedAtUtc)
{
return new AetheriaPlayerSettings
{
PlayerName = settings.PlayerName ?? "",
TutorialPassed = settings.TutorialPassed,
StoryFileHashes = (settings.StoryFileHashes ?? Array.Empty<AetheriaRuntimeStoryFileHashCommit>())
.Select(hash => new AetheriaStoryFileHash
{
StoryPath = hash.StoryPath ?? "",
Hash = hash.Hash ?? ""
})
.ToArray(),
Gameplay = new AetheriaPlayerGameplaySettings
{
TemperatureUnit = settings.TemperatureUnit ?? "",
SignificantDigits = settings.SignificantDigits
},
Graphics = new AetheriaPlayerGraphicsSettings
{
NebulaQuality = settings.NebulaQuality ?? "",
ShowAsteroidsInMinimap = settings.ShowAsteroidsInMinimap
},
Input = new AetheriaPlayerInputSettings
{
BindingOverrides = (settings.BindingOverrides ?? Array.Empty<AetheriaRuntimeInputBindingCommit>())
.Select(binding => new AetheriaInputBindingOverride
{
ActionName = binding.ActionName ?? "",
BindingIndex = binding.BindingIndex,
BindingPath = binding.BindingPath ?? ""
})
.ToArray(),
ActionBarInputs = (settings.ActionBarInputs ?? Array.Empty<string>())
.Select(input => input ?? "")
.ToArray()
},
LastUpdatedAtUtc = updatedAtUtc
};
}
private static AetheriaLoadoutTemplate ToLoadoutTemplate(
AetheriaRuntimeLoadoutTemplateCommit loadout,
string updatedAtUtc)
{
return new AetheriaLoadoutTemplate
{
Name = loadout.Name ?? "",
OwnerPlayerKey = loadout.OwnerPlayerKey ?? "",
RootEntity = ToEntityLoadout(loadout.RootEntity),
CreatedAtUtc = updatedAtUtc,
UpdatedAtUtc = updatedAtUtc
};
}
private static AetheriaEntityLoadout ToEntityLoadout(AetheriaRuntimeEntityLoadoutCommit? entity)
{
entity ??= new AetheriaRuntimeEntityLoadoutCommit();
return new AetheriaEntityLoadout
{
Name = entity.Name ?? "",
Kind = entity.Kind ?? "",
FactionKey = entity.FactionKey ?? "",
Hull = ToLoadoutItem(entity.Hull),
Equipment = ToItemSlots(entity.Equipment),
CargoBays = ToItemSlots(entity.CargoBays),
DockingBays = ToItemSlots(entity.DockingBays),
CargoContents = ToCargoBays(entity.CargoContents),
DockingBayContents = ToCargoBays(entity.DockingBayContents),
DockingBayAssignments = ToIntArray(entity.DockingBayAssignments),
WeaponGroups = ToIntArrayArray(entity.WeaponGroups),
Children = (entity.Children ?? Array.Empty<AetheriaRuntimeEntityLoadoutCommit>())
.Select(ToEntityLoadout)
.ToArray()
};
}
private static AetheriaLoadoutItem ToLoadoutItem(AetheriaRuntimeLoadoutItemCommit? item)
{
item ??= new AetheriaRuntimeLoadoutItemCommit();
return new AetheriaLoadoutItem
{
ItemKey = item.ItemKey ?? "",
Quality = item.Quality,
Durability = item.Durability,
Quantity = item.Quantity,
Enabled = item.Enabled,
OverrideShutdown = item.OverrideShutdown
};
}
private static AetheriaLoadoutItemSlot[] ToItemSlots(
IReadOnlyList<AetheriaRuntimeLoadoutItemSlotCommit>? slots)
{
return (slots ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>())
.Select(slot => new AetheriaLoadoutItemSlot
{
Position = new AetheriaGridCoord
{
X = slot.X,
Y = slot.Y
},
Item = ToLoadoutItem(slot.Item)
})
.ToArray();
}
private static AetheriaEntityItemSlot[] ToEntityItemSlots(
IReadOnlyList<AetheriaRuntimeLoadoutItemSlotCommit>? slots)
{
return (slots ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>())
.Select(slot =>
{
var item = ToLoadoutItem(slot.Item);
return new AetheriaEntityItemSlot
{
Position = new AetheriaGridCoord
{
X = slot.X,
Y = slot.Y
},
ItemKey = item.ItemKey,
Quality = item.Quality,
Durability = item.Durability,
Quantity = item.Quantity,
Enabled = item.Enabled,
OverrideShutdown = item.OverrideShutdown
};
})
.ToArray();
}
private static AetheriaCargoBayLoadout[] ToCargoBays(
IReadOnlyList<AetheriaRuntimeCargoBayLoadoutCommit>? cargoBays)
{
return (cargoBays ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>())
.Select(bay => new AetheriaCargoBayLoadout
{
Items = ToItemSlots(bay.Items)
})
.ToArray();
}
private static async Task ApplyRunCheckpointAsync(
AetheriaStateNode node,
AetheriaRuntimeRunCheckpointCommit checkpoint,
string updatedAtUtc)
{
var run = new AetheriaRunState
{
RunId = checkpoint.RunId ?? "",
IsTutorial = checkpoint.IsTutorial,
EntranceZoneIndex = checkpoint.EntranceZoneIndex,
ExitZoneIndex = checkpoint.ExitZoneIndex,
CurrentZoneIndex = checkpoint.CurrentZoneIndex,
DiscoveredZoneIndices = ToIntArray(checkpoint.DiscoveredZoneIndices),
ActionBarBindings = ToActionBarBindings(checkpoint.ActionBarBindings),
FactionRelationships = ToFactionRelationships(checkpoint.FactionRelationships),
UpdatedAtUtc = updatedAtUtc,
GenerationSeed = checkpoint.GenerationSeed
};
var zoneKeys = new List<string>();
foreach (var zone in checkpoint.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
{
var zoneKey = ZoneKey(run.RunId, zone.ZoneIndex);
var entityKeys = new List<string>();
foreach (var entity in zone.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>())
{
var entityKey = EntityKey(run.RunId, zone.ZoneIndex, entity.EntityIndex);
var snapshot = ToEntitySnapshot(entity, run.RunId, zone.ZoneIndex);
await node.PutEntitySnapshotAsync(entityKey, snapshot).ConfigureAwait(false);
entityKeys.Add(entityKey.ToString());
}
await node.PutZoneStateAsync(
zoneKey,
new AetheriaZoneState
{
Name = zone.Name ?? "",
Position = new AetheriaVector2
{
X = zone.PositionX,
Y = zone.PositionY
},
AdjacentZoneIndices = ToIntArray(zone.AdjacentZoneIndices),
FactionIndices = ToIntArray(zone.FactionIndices),
OwnerFactionIndex = zone.OwnerFactionIndex,
EntityKeys = entityKeys.ToArray(),
Orbits = ToOrbitSnapshots(zone.Orbits),
Bodies = ToBodySnapshots(zone.Bodies, run.RunId, zone.ZoneIndex),
DroppedPickups = ToDroppedPickups(zone.DroppedPickups)
})
.ConfigureAwait(false);
zoneKeys.Add(zoneKey.ToString());
}
if (zoneKeys.Count > 0)
run.ZoneKeys = zoneKeys.ToArray();
run.CurrentEntityKey = checkpoint.CurrentEntityKey ?? "";
await node.PutRunStateAsync(RunKey(run.RunId), run).ConfigureAwait(false);
}
private static AetheriaDroppedPickupSnapshot[] ToDroppedPickups(
IReadOnlyList<AetheriaRuntimeDroppedPickupCommit>? pickups)
{
return (pickups ?? Array.Empty<AetheriaRuntimeDroppedPickupCommit>())
.Select(pickup => new AetheriaDroppedPickupSnapshot
{
PickupIndex = pickup.PickupIndex,
Position = new AetheriaVector3
{
X = pickup.PositionX,
Y = pickup.PositionY,
Z = pickup.PositionZ
},
Velocity = new AetheriaVector3
{
X = pickup.VelocityX,
Y = pickup.VelocityY,
Z = pickup.VelocityZ
},
Item = ToLoadoutItem(pickup.Item)
})
.ToArray();
}
private static AetheriaOrbitSnapshot[] ToOrbitSnapshots(
IReadOnlyList<AetheriaRuntimeOrbitSnapshotCommit>? orbits)
{
return (orbits ?? Array.Empty<AetheriaRuntimeOrbitSnapshotCommit>())
.Select(orbit => new AetheriaOrbitSnapshot
{
OrbitKey = orbit.OrbitKey ?? "",
ParentOrbitKey = orbit.ParentOrbitKey ?? "",
Distance = orbit.Distance,
Phase = orbit.Phase,
FixedPosition = new AetheriaVector2
{
X = orbit.FixedPositionX,
Y = orbit.FixedPositionY
}
})
.ToArray();
}
private static AetheriaBodySnapshot[] ToBodySnapshots(
IReadOnlyList<AetheriaRuntimeBodySnapshotCommit>? bodies,
string runId,
int zoneIndex)
{
return (bodies ?? Array.Empty<AetheriaRuntimeBodySnapshotCommit>())
.Select(body => new AetheriaBodySnapshot
{
BodyKey = body.BodyKey ?? "",
Kind = body.Kind ?? "",
Name = body.Name ?? "",
OrbitKey = body.OrbitKey ?? "",
Mass = body.Mass,
Resources = ToBodyResources(body.Resources),
BodyRadiusMultiplier = body.BodyRadiusMultiplier,
GravityRadiusMultiplier = body.GravityRadiusMultiplier,
GravityDepthMultiplier = body.GravityDepthMultiplier,
GravityDepthExponent = body.GravityDepthExponent,
Asteroids = ToAsteroidSnapshots(body.Asteroids, runId, zoneIndex),
GasGiantVisual = ToGasGiantVisual(body.GasGiantVisual),
SunVisual = ToSunVisual(body.SunVisual)
})
.ToArray();
}
private static AetheriaBodyResource[] ToBodyResources(
IReadOnlyList<AetheriaRuntimeBodyResourceCommit>? resources)
{
return (resources ?? Array.Empty<AetheriaRuntimeBodyResourceCommit>())
.Select(resource => new AetheriaBodyResource
{
ItemKey = resource.ItemKey ?? "",
Amount = resource.Amount
})
.ToArray();
}
private static AetheriaAsteroidSnapshot[] ToAsteroidSnapshots(
IReadOnlyList<AetheriaRuntimeAsteroidCommit>? asteroids,
string runId,
int zoneIndex)
{
return (asteroids ?? Array.Empty<AetheriaRuntimeAsteroidCommit>())
.Select(asteroid => new AetheriaAsteroidSnapshot
{
Distance = asteroid.Distance,
Phase = asteroid.Phase,
Size = asteroid.Size,
RotationSpeed = asteroid.RotationSpeed,
Damage = asteroid.Damage,
RespawnTimer = asteroid.RespawnTimer,
MiningAccumulators = ToAsteroidMiningAccumulators(asteroid.MiningAccumulators, runId, zoneIndex)
})
.ToArray();
}
private static AetheriaAsteroidMiningAccumulatorSnapshot[] ToAsteroidMiningAccumulators(
IReadOnlyList<AetheriaRuntimeAsteroidMiningAccumulatorCommit>? accumulators,
string runId,
int zoneIndex)
{
return (accumulators ?? Array.Empty<AetheriaRuntimeAsteroidMiningAccumulatorCommit>())
.Where(accumulator => accumulator.MinerEntityIndex >= 0)
.Select(accumulator => new AetheriaAsteroidMiningAccumulatorSnapshot
{
MinerEntityKey = EntityKey(runId, zoneIndex, accumulator.MinerEntityIndex).ToString(),
Amount = accumulator.Amount
})
.ToArray();
}
private static AetheriaGasGiantVisualState ToGasGiantVisual(AetheriaRuntimeGasGiantVisualCommit? visual)
{
if (visual == null)
return new AetheriaGasGiantVisualState();
return new AetheriaGasGiantVisualState
{
FirstOffsetDomainRotationSpeed = visual.FirstOffsetDomainRotationSpeed,
FirstOffsetRotationSpeed = visual.FirstOffsetRotationSpeed,
SecondOffsetDomainRotationSpeed = visual.SecondOffsetDomainRotationSpeed,
SecondOffsetRotationSpeed = visual.SecondOffsetRotationSpeed,
AlbedoRotationSpeed = visual.AlbedoRotationSpeed,
WaveRadiusMultiplier = visual.WaveRadiusMultiplier,
WaveDepthMultiplier = visual.WaveDepthMultiplier,
WaveDepthExponent = visual.WaveDepthExponent,
WaveSpeedMultiplier = visual.WaveSpeedMultiplier,
MaterialOverrides = (visual.MaterialOverrides ?? Array.Empty<string>()).ToArray(),
Colors = (visual.Colors ?? Array.Empty<AetheriaRuntimeColorCommit>())
.Select(ToColor)
.ToArray()
};
}
private static AetheriaSunVisualState ToSunVisual(AetheriaRuntimeSunVisualCommit? visual)
{
if (visual == null)
return new AetheriaSunVisualState();
return new AetheriaSunVisualState
{
LightColor = new AetheriaVector3
{
X = visual.LightColorX,
Y = visual.LightColorY,
Z = visual.LightColorZ
},
FogTintColor = new AetheriaVector3
{
X = visual.FogTintColorX,
Y = visual.FogTintColorY,
Z = visual.FogTintColorZ
},
LightRadiusMultiplier = visual.LightRadiusMultiplier
};
}
private static AetheriaColor ToColor(AetheriaRuntimeColorCommit color)
{
return new AetheriaColor
{
X = color.X,
Y = color.Y,
Z = color.Z,
W = color.W
};
}
private static AetheriaEntitySnapshot ToEntitySnapshot(
AetheriaRuntimeEntitySnapshotCommit entity,
string runId,
int zoneIndex)
{
return new AetheriaEntitySnapshot
{
Name = entity.Name ?? "",
Kind = entity.Kind ?? "",
Position = new AetheriaVector3
{
X = entity.PositionX,
Y = entity.PositionY,
Z = entity.PositionZ
},
Direction = new AetheriaVector2
{
X = entity.DirectionX,
Y = entity.DirectionY
},
Velocity = new AetheriaVector2
{
X = entity.VelocityX,
Y = entity.VelocityY
},
TargetEntityKey = entity.TargetEntityIndex >= 0
? EntityKey(runId, zoneIndex, entity.TargetEntityIndex).ToString()
: "",
IsActive = entity.IsActive,
HeatsinksEnabled = entity.HeatsinksEnabled,
OverrideShutdown = entity.OverrideShutdown,
TractorPower = entity.TractorPower,
Heatstroke = entity.Heatstroke,
Hypothermia = entity.Hypothermia,
FactionKey = entity.FactionKey ?? "",
HullItemKey = entity.HullItemKey ?? "",
Equipment = ToEntityItemSlots(entity.Equipment),
CargoBays = ToEntityItemSlots(entity.CargoBays),
DockingBays = ToEntityItemSlots(entity.DockingBays),
ChildEntityKeys = (entity.ChildEntityIndices ?? Array.Empty<int>())
.Select(childIndex => EntityKey(runId, zoneIndex, childIndex).ToString())
.ToArray(),
WeaponGroups = ToIntArrayArray(entity.WeaponGroups)
.Select(group => new AetheriaWeaponGroupSnapshot { EquipmentIndices = group })
.ToArray(),
StatGrids = ToEntityStatGrids(entity.StatGrids),
ActiveConsumables = ToActiveConsumables(entity.ActiveConsumables),
BehaviorProgress = ToBehaviorProgress(entity.BehaviorProgress),
WeaponStates = ToWeaponStates(entity.WeaponStates, runId, zoneIndex),
BehaviorStates = ToBehaviorStates(entity.BehaviorStates),
CargoContents = ToCargoBays(entity.CargoContents),
DockingBayContents = ToCargoBays(entity.DockingBayContents),
DockingBayAssignments = ToIntArray(entity.DockingBayAssignments),
Visibility = entity.Visibility,
VisibilitySourceCount = entity.VisibilitySourceCount,
Contacts = ToEntityContacts(entity.Contacts, runId, zoneIndex)
};
}
private static AetheriaEntityContactSnapshot[] ToEntityContacts(
IReadOnlyList<AetheriaRuntimeEntityContactCommit>? contacts,
string runId,
int zoneIndex)
{
return (contacts ?? Array.Empty<AetheriaRuntimeEntityContactCommit>())
.Where(contact => contact.TargetEntityIndex >= 0)
.Select(contact => new AetheriaEntityContactSnapshot
{
TargetEntityKey = EntityKey(runId, zoneIndex, contact.TargetEntityIndex).ToString(),
InfoGathered = contact.InfoGathered,
Hostile = contact.Hostile,
Visible = contact.Visible
})
.ToArray();
}
private static AetheriaBehaviorStateSnapshot[] ToBehaviorStates(
IReadOnlyList<AetheriaRuntimeBehaviorStateCommit>? states)
{
return (states ?? Array.Empty<AetheriaRuntimeBehaviorStateCommit>())
.Select(state => new AetheriaBehaviorStateSnapshot
{
OwnerKind = state.OwnerKind ?? "",
OwnerIndex = state.OwnerIndex,
BehaviorIndex = state.BehaviorIndex,
BehaviorKind = state.BehaviorKind ?? "",
Pinging = state.Pinging,
PingCooldown = state.PingCooldown,
PingLerp = state.PingLerp,
PingRadius = state.PingRadius,
PingedEntityCount = state.PingedEntityCount,
RadiatorTemperature = state.RadiatorTemperature,
Emissivity = state.Emissivity,
PumpedHeat = state.PumpedHeat,
WasteHeat = state.WasteHeat,
EnergyUsage = state.EnergyUsage,
ReactorDraw = state.ReactorDraw,
ReactorLoadRatio = state.ReactorLoadRatio,
CapacitorCharge = state.CapacitorCharge,
CapacitorCapacity = state.CapacitorCapacity,
CapacitorEfficiency = state.CapacitorEfficiency,
AetherDriveAxisX = state.AetherDriveAxisX,
AetherDriveAxisY = state.AetherDriveAxisY,
AetherDriveAxisZ = state.AetherDriveAxisZ,
AetherDriveThrustX = state.AetherDriveThrustX,
AetherDriveThrustY = state.AetherDriveThrustY,
AetherDriveThrustZ = state.AetherDriveThrustZ,
AetherDriveRpmX = state.AetherDriveRpmX,
AetherDriveRpmY = state.AetherDriveRpmY,
AetherDriveRpmZ = state.AetherDriveRpmZ,
AetherDriveMaximumRpm = state.AetherDriveMaximumRpm,
AetherDriveThrustDirectionX = state.AetherDriveThrustDirectionX,
AetherDriveThrustDirectionY = state.AetherDriveThrustDirectionY,
ResourceScannerTargetBodyKey = state.ResourceScannerTargetBodyKey ?? "",
ResourceScannerAsteroidIndex = state.ResourceScannerAsteroidIndex,
ResourceScannerScanTime = state.ResourceScannerScanTime,
ResourceScannerRange = state.ResourceScannerRange,
ResourceScannerMinimumDensity = state.ResourceScannerMinimumDensity,
ResourceScannerScanDuration = state.ResourceScannerScanDuration,
MiningToolAsteroidBeltKey = state.MiningToolAsteroidBeltKey ?? "",
MiningToolAsteroidIndex = state.MiningToolAsteroidIndex,
MiningToolRange = state.MiningToolRange,
ThrusterAxis = state.ThrusterAxis,
ThrusterThrust = state.ThrusterThrust,
ThrusterTorque = state.ThrusterTorque,
ShieldEfficiency = state.ShieldEfficiency,
ShieldEnergyUsage = state.ShieldEnergyUsage,
VelocityLimit = state.VelocityLimit,
ThermotoggleTargetTemperature = state.ThermotoggleTargetTemperature,
SwitchActivated = state.SwitchActivated,
TriggerPulled = state.TriggerPulled,
StatModifierApplied = state.StatModifierApplied,
StatModifierExecuted = state.StatModifierExecuted,
StatModifierTargetStatCount = state.StatModifierTargetStatCount,
TurretControllerWeaponCount = state.TurretControllerWeaponCount,
TurretControllerShotSpeed = state.TurretControllerShotSpeed,
TurretControllerPredictShots = state.TurretControllerPredictShots
})
.ToArray();
}
private static AetheriaWeaponStateSnapshot[] ToWeaponStates(
IReadOnlyList<AetheriaRuntimeWeaponStateCommit>? states,
string runId,
int zoneIndex)
{
return (states ?? Array.Empty<AetheriaRuntimeWeaponStateCommit>())
.Select(state => new AetheriaWeaponStateSnapshot
{
OwnerKind = state.OwnerKind ?? "",
OwnerIndex = state.OwnerIndex,
BehaviorIndex = state.BehaviorIndex,
BehaviorKind = state.BehaviorKind ?? "",
Firing = state.Firing,
Ammo = state.Ammo,
BurstRemaining = state.BurstRemaining,
BurstTimer = state.BurstTimer,
BurstInterval = state.BurstInterval,
CooldownProgress = state.CooldownProgress,
CoolingDown = state.CoolingDown,
Charging = state.Charging,
Charged = state.Charged,
Charge = state.Charge,
Reloading = state.Reloading,
ReloadProgress = state.ReloadProgress,
AmmoIntervalProgress = state.AmmoIntervalProgress,
LockProgress = state.LockProgress,
LockTargetEntityKey = state.LockTargetEntityIndex >= 0
? EntityKey(runId, zoneIndex, state.LockTargetEntityIndex).ToString()
: ""
})
.ToArray();
}
private static AetheriaBehaviorProgressSnapshot[] ToBehaviorProgress(
IReadOnlyList<AetheriaRuntimeBehaviorProgressCommit>? progress)
{
return (progress ?? Array.Empty<AetheriaRuntimeBehaviorProgressCommit>())
.Select(entry => new AetheriaBehaviorProgressSnapshot
{
OwnerKind = entry.OwnerKind ?? "",
OwnerIndex = entry.OwnerIndex,
BehaviorIndex = entry.BehaviorIndex,
BehaviorKind = entry.BehaviorKind ?? "",
Progress = entry.Progress
})
.ToArray();
}
private static AetheriaActiveConsumableSnapshot[] ToActiveConsumables(
IReadOnlyList<AetheriaRuntimeActiveConsumableCommit>? consumables)
{
return (consumables ?? Array.Empty<AetheriaRuntimeActiveConsumableCommit>())
.Select(consumable => new AetheriaActiveConsumableSnapshot
{
ItemKey = consumable.ItemKey ?? "",
Quality = consumable.Quality,
RemainingDuration = consumable.RemainingDuration,
Duration = consumable.Duration
})
.ToArray();
}
private static AetheriaEntityStatGrid[] ToEntityStatGrids(
IReadOnlyList<AetheriaRuntimeEntityStatGridCommit>? grids)
{
return (grids ?? Array.Empty<AetheriaRuntimeEntityStatGridCommit>())
.Select(grid => new AetheriaEntityStatGrid
{
Name = grid.Name ?? "",
Width = grid.Width,
Height = grid.Height,
Values = (grid.Values ?? Array.Empty<double>()).ToArray()
})
.ToArray();
}
private static int[] ToIntArray(IReadOnlyList<int>? values)
{
return (values ?? Array.Empty<int>()).ToArray();
}
private static int[][] ToIntArrayArray(IReadOnlyList<IReadOnlyList<int>>? values)
{
return (values ?? Array.Empty<IReadOnlyList<int>>())
.Select(ToIntArray)
.ToArray();
}
private static AetheriaActionBarBinding[] ToActionBarBindings(
IReadOnlyList<AetheriaRuntimeActionBarBindingCommit>? bindings)
{
return (bindings ?? Array.Empty<AetheriaRuntimeActionBarBindingCommit>())
.Select(binding => new AetheriaActionBarBinding
{
ControlPath = binding.ControlPath ?? "",
Kind = binding.Kind ?? "",
TargetKey = binding.ItemKey ?? "",
EquipmentIndex = binding.EquipmentIndex,
BehaviorIndex = binding.BehaviorIndex,
WeaponGroup = binding.WeaponGroup
})
.ToArray();
}
private static AetheriaFactionRelationshipState[] ToFactionRelationships(
IReadOnlyList<AetheriaRuntimeFactionRelationshipCommit>? relationships)
{
return (relationships ?? Array.Empty<AetheriaRuntimeFactionRelationshipCommit>())
.Select(relationship => new AetheriaFactionRelationshipState
{
FactionKey = relationship.FactionKey ?? "",
Relationship = relationship.Relationship ?? "",
Standing = relationship.Standing
})
.ToArray();
}
private static CultRecordKey LoadoutKey(string name)
{
return new CultRecordKey($"global:aetheria.loadout_template.{StableToken(name)}.v1");
}
private static CultRecordKey RunKey(string runId)
{
return new CultRecordKey($"global:aetheria.run_state.{StableToken(runId)}.v1");
}
private static CultRecordKey ZoneKey(string runId, int zoneIndex)
{
return new CultRecordKey($"global:aetheria.run_state.{StableToken(runId)}.zone.{zoneIndex}.v1");
}
private static CultRecordKey EntityKey(string runId, int zoneIndex, int entityIndex)
{
return new CultRecordKey($"global:aetheria.run_state.{StableToken(runId)}.zone.{zoneIndex}.entity.{entityIndex}.v1");
}
private static string StableToken(string value)
{
var chars = (string.IsNullOrWhiteSpace(value) ? "unnamed" : value.Trim().ToLowerInvariant())
.Select(ch => char.IsLetterOrDigit(ch) ? ch : '-')
.ToArray();
var token = new string(chars).Trim('-');
while (token.Contains("--", StringComparison.Ordinal))
token = token.Replace("--", "-", StringComparison.Ordinal);
return string.IsNullOrWhiteSpace(token) ? "unnamed" : token;
}
private static InvalidDataException MissingPayload(AetheriaRuntimeStateCommitDocument command)
{
return new InvalidDataException($"Aetheria runtime commit '{command.CommandId}' has no {command.Kind} payload.");
}
}

View file

@ -0,0 +1,55 @@
using Aetheria.State.Documents;
using GameCult.Aetheria.State.Unity;
namespace Aetheria.State;
public static class AetheriaRuntimeEveSurfaceStateProjector
{
public static EveSurfaceState ToState(AetheriaRuntimeSurfaceDocument document)
{
return new EveSurfaceState
{
ProviderId = document.ProviderId,
ProviderKind = document.ProviderKind,
Title = document.Title,
Version = document.Version,
UpdatedAtUtc = document.UpdatedAtUtc,
Surface = ToSurface(document.Surface),
Commands = document.Commands
.Select(command => new EveCommandTemplate
{
Command = command.Command,
Label = command.Label,
Transport = command.Transport
})
.ToArray()
};
}
private static EveSurface ToSurface(AetheriaRuntimeSurfaceTree surface)
{
return new EveSurface
{
Id = surface.Id,
Root = ToComponent(surface.Root),
Styles = surface.Styles
.Select(style => new EveStyleToken
{
Name = style.Name,
Value = style.Value
})
.ToArray()
};
}
private static EveSurfaceComponent ToComponent(AetheriaRuntimeSurfaceComponent component)
{
return new EveSurfaceComponent
{
Id = component.Id,
Kind = component.Kind,
Props = new Dictionary<string, string>(component.Props, StringComparer.Ordinal),
Children = component.Children.Select(ToComponent).ToArray()
};
}
}

View file

@ -0,0 +1,113 @@
using Aetheria.State.Documents;
using GameCult.Aetheria.State.Unity;
using GameCult.Caching;
namespace Aetheria.State;
public static class AetheriaRuntimeStateMapper
{
public static AetheriaLoadoutTemplate ToLoadoutTemplate(
AetheriaRuntimeLoadoutTemplateCommit loadout,
string updatedAtUtc)
{
return new AetheriaLoadoutTemplate
{
Name = loadout.Name ?? "",
OwnerPlayerKey = loadout.OwnerPlayerKey ?? "",
RootEntity = ToEntityLoadout(loadout.RootEntity),
CreatedAtUtc = updatedAtUtc,
UpdatedAtUtc = updatedAtUtc
};
}
public static CultRecordKey LoadoutKey(string name)
{
return new CultRecordKey($"global:aetheria.loadout_template.{StableToken(name)}.v1");
}
private static AetheriaEntityLoadout ToEntityLoadout(AetheriaRuntimeEntityLoadoutCommit? entity)
{
entity ??= new AetheriaRuntimeEntityLoadoutCommit();
return new AetheriaEntityLoadout
{
Name = entity.Name ?? "",
Kind = entity.Kind ?? "",
FactionKey = entity.FactionKey ?? "",
Hull = ToLoadoutItem(entity.Hull),
Equipment = ToItemSlots(entity.Equipment),
CargoBays = ToItemSlots(entity.CargoBays),
DockingBays = ToItemSlots(entity.DockingBays),
CargoContents = ToCargoBays(entity.CargoContents),
DockingBayContents = ToCargoBays(entity.DockingBayContents),
DockingBayAssignments = ToIntArray(entity.DockingBayAssignments),
WeaponGroups = ToIntArrayArray(entity.WeaponGroups),
Children = (entity.Children ?? Array.Empty<AetheriaRuntimeEntityLoadoutCommit>())
.Select(ToEntityLoadout)
.ToArray()
};
}
private static AetheriaLoadoutItem ToLoadoutItem(AetheriaRuntimeLoadoutItemCommit? item)
{
item ??= new AetheriaRuntimeLoadoutItemCommit();
return new AetheriaLoadoutItem
{
ItemKey = item.ItemKey ?? "",
Quality = item.Quality,
Durability = item.Durability,
Quantity = item.Quantity,
Enabled = item.Enabled,
OverrideShutdown = item.OverrideShutdown
};
}
private static AetheriaLoadoutItemSlot[] ToItemSlots(
IReadOnlyList<AetheriaRuntimeLoadoutItemSlotCommit>? slots)
{
return (slots ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>())
.Select(slot => new AetheriaLoadoutItemSlot
{
Position = new AetheriaGridCoord
{
X = slot.X,
Y = slot.Y
},
Item = ToLoadoutItem(slot.Item)
})
.ToArray();
}
private static AetheriaCargoBayLoadout[] ToCargoBays(
IReadOnlyList<AetheriaRuntimeCargoBayLoadoutCommit>? cargoBays)
{
return (cargoBays ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>())
.Select(bay => new AetheriaCargoBayLoadout
{
Items = ToItemSlots(bay.Items)
})
.ToArray();
}
private static int[] ToIntArray(IReadOnlyList<int>? values)
{
return (values ?? Array.Empty<int>()).ToArray();
}
private static int[][] ToIntArrayArray(IReadOnlyList<IReadOnlyList<int>>? values)
{
return (values ?? Array.Empty<IReadOnlyList<int>>())
.Select(ToIntArray)
.ToArray();
}
private static string StableToken(string value)
{
var chars = (string.IsNullOrWhiteSpace(value) ? "unnamed" : value.Trim().ToLowerInvariant())
.Select(ch => char.IsLetterOrDigit(ch) ? ch : '-')
.ToArray();
var token = new string(chars).Trim('-');
while (token.Contains("--", StringComparison.Ordinal))
token = token.Replace("--", "-", StringComparison.Ordinal);
return string.IsNullOrWhiteSpace(token) ? "unnamed" : token;
}
}

View file

@ -2,6 +2,7 @@ using System;
using System.IO;
using System.Threading.Tasks;
using Aetheria.State.Documents;
using GameCult.Aetheria.State.Unity;
using GameCult.Caching;
using GameCult.Caching.MessagePack;
using GameCult.Mesh;
@ -207,6 +208,164 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable
new CultRecordKey(AetheriaProviderAdvertisementProjector.AdvertisementKey));
}
public Task<CultRecordHandle<AetheriaRuntimeDaemonProviderAdvertisementDocument>> PutDaemonProviderAdvertisementAsync(
AetheriaRuntimeDaemonProviderAdvertisementDocument advertisement)
{
return Database.PutAsync(new CultRecordKey("daemon:aetheria.provider_advertisement.v1"), advertisement);
}
public Task<AetheriaRuntimeDaemonProviderAdvertisementDocument?> GetDaemonProviderAdvertisementAsync()
{
return Database.GetAsync<AetheriaRuntimeDaemonProviderAdvertisementDocument>(
new CultRecordKey("daemon:aetheria.provider_advertisement.v1"));
}
public Task<CultRecordHandle<AetheriaRuntimeDaemonHealthDocument>> PutDaemonHealthAsync(
AetheriaRuntimeDaemonHealthDocument health)
{
return Database.PutAsync(new CultRecordKey("daemon:aetheria.health.v1"), health);
}
public Task<AetheriaRuntimeDaemonHealthDocument?> GetDaemonHealthAsync()
{
return Database.GetAsync<AetheriaRuntimeDaemonHealthDocument>(
new CultRecordKey("daemon:aetheria.health.v1"));
}
public Task<CultRecordHandle<AetheriaRuntimeDaemonCommandBoundaryDocument>> PutDaemonCommandBoundaryAsync(
AetheriaRuntimeDaemonCommandBoundaryDocument boundary)
{
return Database.PutAsync(new CultRecordKey("daemon:aetheria.command_boundary.v1"), boundary);
}
public Task<AetheriaRuntimeDaemonCommandBoundaryDocument?> GetDaemonCommandBoundaryAsync()
{
return Database.GetAsync<AetheriaRuntimeDaemonCommandBoundaryDocument>(
new CultRecordKey("daemon:aetheria.command_boundary.v1"));
}
public Task<CultRecordHandle<AetheriaRuntimeDaemonCommandDocument>> SubmitDaemonCommandAsync(
AetheriaRuntimeDaemonCommandDocument command)
{
if (command == null) throw new ArgumentNullException(nameof(command));
command.Schema = AetheriaRuntimeDaemonSchemas.Command;
if (string.IsNullOrWhiteSpace(command.CommandId))
command.CommandId = Guid.NewGuid().ToString("N");
if (string.IsNullOrWhiteSpace(command.IssuedAtUtc))
command.IssuedAtUtc = DateTime.UtcNow.ToString("O");
return Database.PutAsync(DaemonCommandKey(command.CommandId), command);
}
public IReadOnlyList<AetheriaRuntimeDaemonCommandDocument> ReadObservedDaemonCommands()
{
return Cache
.GetAll<AetheriaRuntimeDaemonCommandDocument>()
.OrderBy(command => command.IssuedAtUtc ?? "", StringComparer.Ordinal)
.ThenBy(command => command.CommandId ?? "", StringComparer.Ordinal)
.ToArray();
}
private static CultRecordKey DaemonCommandKey(string commandId)
{
return new CultRecordKey($"daemon:commands:{StableToken(commandId)}:gamecult.aetheria.daemon_command.v1");
}
public Task<CultRecordHandle<AetheriaRuntimeEveCommandDocument>> SubmitEveCommandAsync(
AetheriaRuntimeEveCommandDocument command)
{
if (command == null) throw new ArgumentNullException(nameof(command));
command.Schema = AetheriaRuntimeEveCommandDocument.SchemaId;
if (string.IsNullOrWhiteSpace(command.CommandId))
command.CommandId = Guid.NewGuid().ToString("N");
if (string.IsNullOrWhiteSpace(command.IssuedAtUtc))
command.IssuedAtUtc = DateTime.UtcNow.ToString("O");
return Database.PutAsync(EveCommandKey(command.CommandId), command);
}
public IReadOnlyList<AetheriaRuntimeEveCommandDocument> ReadObservedEveCommands()
{
return Cache
.GetAll<AetheriaRuntimeEveCommandDocument>()
.OrderBy(command => command.IssuedAtUtc ?? "", StringComparer.Ordinal)
.ThenBy(command => command.CommandId ?? "", StringComparer.Ordinal)
.ToArray();
}
private static CultRecordKey EveCommandKey(string commandId)
{
return new CultRecordKey($"eve:commands:{StableToken(commandId)}:gamecult.eve.command.v1");
}
public Task<CultRecordHandle<AetheriaRuntimeDaemonFrameDocument>> PutDaemonFrameAsync(
AetheriaRuntimeDaemonFrameDocument frame)
{
return Database.PutAsync(new CultRecordKey("daemon:aetheria.frame.latest.v1"), frame);
}
public Task<AetheriaRuntimeDaemonFrameDocument?> GetDaemonFrameAsync()
{
return Database.GetAsync<AetheriaRuntimeDaemonFrameDocument>(
new CultRecordKey("daemon:aetheria.frame.latest.v1"));
}
public Task<CultRecordHandle<AetheriaRuntimeDaemonSoaViewDocument>> PutDaemonSoaViewAsync(
AetheriaRuntimeDaemonSoaViewDocument view)
{
return Database.PutAsync(new CultRecordKey("daemon:aetheria.soa_view.latest.v1"), view);
}
public Task<AetheriaRuntimeDaemonSoaViewDocument?> GetDaemonSoaViewAsync()
{
return Database.GetAsync<AetheriaRuntimeDaemonSoaViewDocument>(
new CultRecordKey("daemon:aetheria.soa_view.latest.v1"));
}
public Task<CultRecordHandle<EveSurfaceState>> PutDaemonGameSurfaceAsync(EveSurfaceState surface)
{
return Database.PutAsync(new CultRecordKey(AetheriaProviderAdvertisementProjector.DaemonGameSurfaceKey), surface);
}
public Task<EveSurfaceState?> GetDaemonGameSurfaceAsync()
{
return Database.GetAsync<EveSurfaceState>(
new CultRecordKey(AetheriaProviderAdvertisementProjector.DaemonGameSurfaceKey));
}
public Task<CultRecordHandle<EveSurfaceState>> PutDaemonGameTuiSurfaceAsync(EveSurfaceState surface)
{
return Database.PutAsync(new CultRecordKey(AetheriaProviderAdvertisementProjector.DaemonGameTuiSurfaceKey), surface);
}
public Task<EveSurfaceState?> GetDaemonGameTuiSurfaceAsync()
{
return Database.GetAsync<EveSurfaceState>(
new CultRecordKey(AetheriaProviderAdvertisementProjector.DaemonGameTuiSurfaceKey));
}
public Task<CultRecordHandle<EveSurfaceState>> PutDaemonEditorSurfaceAsync(EveSurfaceState surface)
{
return Database.PutAsync(new CultRecordKey(AetheriaProviderAdvertisementProjector.DaemonEditorSurfaceKey), surface);
}
public Task<EveSurfaceState?> GetDaemonEditorSurfaceAsync()
{
return Database.GetAsync<EveSurfaceState>(
new CultRecordKey(AetheriaProviderAdvertisementProjector.DaemonEditorSurfaceKey));
}
public Task<CultRecordHandle<EveSurfaceState>> PutDaemonEditorTuiSurfaceAsync(EveSurfaceState surface)
{
return Database.PutAsync(new CultRecordKey(AetheriaProviderAdvertisementProjector.DaemonEditorTuiSurfaceKey), surface);
}
public Task<EveSurfaceState?> GetDaemonEditorTuiSurfaceAsync()
{
return Database.GetAsync<EveSurfaceState>(
new CultRecordKey(AetheriaProviderAdvertisementProjector.DaemonEditorTuiSurfaceKey));
}
public Task<CultRecordHandle<AetheriaRuntimeSession>> PutRuntimeSessionAsync(AetheriaRuntimeSession session)
{
return Database.PutAsync(new CultRecordKey($"runtime:{session.RuntimeId}:aetheria.runtime_session.v1"), session);
@ -284,28 +443,16 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable
new CultRecordKey("global:aetheria.verse_host_settings.v1"));
}
public Task<CultRecordHandle<AetheriaRuntimeCommitDrainStatus>> PutRuntimeCommitDrainStatusAsync(
AetheriaRuntimeCommitDrainStatus status)
public Task<CultRecordHandle<AetheriaEveCommandAcceptanceStatus>> PutEveCommandAcceptanceStatusAsync(
AetheriaEveCommandAcceptanceStatus status)
{
return Database.PutAsync(new CultRecordKey("global:aetheria.runtime_commit_drain_status.v1"), status);
return Database.PutAsync(new CultRecordKey("global:aetheria.eve_command_acceptance_status.v1"), status);
}
public Task<AetheriaRuntimeCommitDrainStatus?> GetRuntimeCommitDrainStatusAsync()
public Task<AetheriaEveCommandAcceptanceStatus?> GetEveCommandAcceptanceStatusAsync()
{
return Database.GetAsync<AetheriaRuntimeCommitDrainStatus>(
new CultRecordKey("global:aetheria.runtime_commit_drain_status.v1"));
}
public Task<CultRecordHandle<AetheriaEveCommandDrainStatus>> PutEveCommandDrainStatusAsync(
AetheriaEveCommandDrainStatus status)
{
return Database.PutAsync(new CultRecordKey("global:aetheria.eve_command_drain_status.v1"), status);
}
public Task<AetheriaEveCommandDrainStatus?> GetEveCommandDrainStatusAsync()
{
return Database.GetAsync<AetheriaEveCommandDrainStatus>(
new CultRecordKey("global:aetheria.eve_command_drain_status.v1"));
return Database.GetAsync<AetheriaEveCommandAcceptanceStatus>(
new CultRecordKey("global:aetheria.eve_command_acceptance_status.v1"));
}
public Task FlushAsync(bool soft = false)
@ -313,6 +460,20 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable
return _node.FlushAsync(soft);
}
private static string StableToken(string value)
{
if (string.IsNullOrWhiteSpace(value))
return "empty";
var chars = value
.Select(character => char.IsLetterOrDigit(character) ? character : '-')
.ToArray();
var token = new string(chars).Trim('-').ToLowerInvariant();
while (token.Contains("--", StringComparison.Ordinal))
token = token.Replace("--", "-", StringComparison.Ordinal);
return string.IsNullOrWhiteSpace(token) ? "empty" : token;
}
public void Dispose()
{
_node.Dispose();

View file

@ -3,14 +3,14 @@ using MessagePack;
namespace Aetheria.State.Documents;
[CultDocument("aetheria.eve_command_drain_status", "aetheria.eve_command_drain_status.v1")]
[CultDocument("aetheria.eve_command_acceptance_status", "aetheria.eve_command_acceptance_status.v1")]
[CultGlobal]
[MessagePackObject]
public sealed class AetheriaEveCommandDrainStatus
public sealed class AetheriaEveCommandAcceptanceStatus
{
[Key(0)]
[CultName]
public string Name { get; set; } = "Aetheria Eve command drain";
public string Name { get; set; } = "Aetheria Eve command acceptance";
[Key(1)]
[CultIndex("runtimeId")]
@ -26,7 +26,7 @@ public sealed class AetheriaEveCommandDrainStatus
public string LastAcceptedAtUtc { get; set; } = "";
[Key(5)]
public int PendingBeforeApply { get; set; }
public int ObservedBeforeAccept { get; set; }
[Key(6)]
public int CommandsAccepted { get; set; }
@ -60,4 +60,13 @@ public sealed class AetheriaEveCommandDrainStatus
[Key(16)]
public int AppliedVerseHostCommands { get; set; }
[Key(17)]
public int AppliedInputSettingsCommands { get; set; }
[Key(18)]
public int AppliedLoadoutTemplateCommands { get; set; }
[Key(19)]
public string[] AccountedCommandIds { get; set; } = [];
}

View file

@ -1,51 +0,0 @@
using GameCult.Caching;
using MessagePack;
namespace Aetheria.State.Documents;
[CultDocument("aetheria.runtime_commit_drain_status", "aetheria.runtime_commit_drain_status.v1")]
[CultGlobal]
[MessagePackObject]
public sealed class AetheriaRuntimeCommitDrainStatus
{
[Key(0)]
[CultName]
public string Name { get; set; } = "Aetheria runtime commit drain";
[Key(1)]
[CultIndex("runtimeId")]
public string RuntimeId { get; set; } = "";
[Key(2)]
public string StatePath { get; set; } = "";
[Key(3)]
public string LastPollAtUtc { get; set; } = "";
[Key(4)]
public string LastAppliedAtUtc { get; set; } = "";
[Key(5)]
public int PendingBeforeApply { get; set; }
[Key(6)]
public int CommandsApplied { get; set; }
[Key(7)]
public int AppliedPlayerSettings { get; set; }
[Key(8)]
public int AppliedLoadoutTemplates { get; set; }
[Key(9)]
public int AppliedRunCheckpoints { get; set; }
[Key(10)]
public int ConsecutiveFailures { get; set; }
[Key(11)]
public string LastError { get; set; } = "";
[Key(12)]
public string Status { get; set; } = "ok";
}

View file

@ -38,27 +38,28 @@ The Unity client no longer writes `PlayerSettings.msgpack`, `.loadout`, or
replacement for bespoke loadout files, with structured hull/equipment/cargo/
docking item slots, nested child-entity loadouts, and weapon groups.
`RuntimePlayerSettings` is the Unity session projection for menus, formatting,
and input binding edits; menu and input-screen changes queue typed
`.cc.pending` player-settings commits and are not portable state authority.
Unity settings, loadout templates, and run checkpoints now queue typed
`.cc.pending` runtime commit documents beside
`GameData/aetheria-world.cc`. `Aetheria.State` owns applying those documents
into canonical typed state through `AetheriaRuntimeCommitLogApplier`; the
Unity-side command log is command-only and cannot become durable truth by
itself.
`AetheriaRuntimeSession` is the typed Verse signal for the running state host
or one-shot pending drain. `Economy.Server` owns the long-running
`cultmesh-state-host` heartbeat and publishes `starting`, `running`, and
`stopping` states through `Aetheria.State`; drain status remains operation
telemetry, not daemon-session ownership.
Unity Eve surfaces also emit typed `gamecult.eve.command.v1` documents under
`GameData/aetheria-world.cc.eve.pending`. `AetheriaEveCommandBridge` is the
provider-owned acceptance organ for those commands: it validates the provider,
surface, and command template before running the narrow refresh handlers for
the catalog and operations surfaces plus the typed player-settings mutation
handlers for gameplay/graphics Eve controls and typed Verse-host visibility
commands. Renderer callbacks do not accept commands or mutate game state
locally.
and input binding display. Menu and input-screen changes send typed Eve request
documents to the provider-owned Aetheria Verse service; they are not portable
state authority and must not mutate daemon-owned state inside Unity.
The old `.cc.pending` runtime commit document lane is deleted. Gameplay state,
loadout templates, and run checkpoints are accepted by the daemon/Verse owner
through typed state operations and daemon command documents, not by
renderer-side commit authority.
`AetheriaRuntimeSession` is the typed Verse signal for the running daemon.
`Aetheria.State.Daemon` owns the long-running `aetheria-daemon` heartbeat and
publishes `starting`, `running`, and `stopping` states through `Aetheria.State`;
legacy outbox status remains operation telemetry, not daemon-session ownership.
Unity Eve surfaces construct typed `gamecult.eve.command.v1` command documents.
The neutral `AetheriaCommandPort` owns command submission for headless and
non-Unity clients; Unity input providers reach the same records through the
`AetheriaRuntimeCommandPort` facade. The Verse daemon uses `AetheriaStateNode`
to observe and accept those CultNet records in the Aetheria state graph.
`AetheriaEveCommandBridge` is the provider-owned command acceptor for those
records: it validates the provider, surface, and command template before
running the narrow refresh handlers for the catalog and operations surfaces
plus the typed player-settings mutation handlers for gameplay/graphics Eve
controls and typed Verse-host visibility commands. Renderer callbacks do not
accept commands or mutate game state locally.
`AetheriaEveRuntimeBootstrap` mounts the `aetheria.operations` surface through a
runtime-created `UIDocument` after scene load, with environment and command-line
switches for diagnostics. UI Toolkit is now a live runtime lowering path, not
@ -66,6 +67,15 @@ only a package sample.
The old `SavedGame`/`SavedZone` DTOs and `Galaxy` loader constructor have also
been deleted; the new document family is live Verse run/zone state, not a
bespoke save-file format.
Daemon run frames project into Unity through the explicit
`EntityConstructionBlueprintProjector.ProjectObservedFromBlueprint` lowering
path; authoritative blueprint construction remains named separately as a
migration target for code that still has to move into the daemon.
Main-menu gameplay boot also projects the daemon run through
`Galaxy.ProjectObservedDaemonRun` rather than constructing or generating a
Unity-owned run. The Unity handle for this shell is
`ActionGameManager.ObservedGalaxy` to keep the client-side object named as an
observation.
The dead `SavedStory` JSON DTO is gone. `ZoneConstructionBlueprint` and
`RuntimeEntityBlueprint` remain in-memory construction/loadout projections for
Unity runtime code, with `RuntimeEntityBlueprintProjector` capturing and
@ -104,13 +114,13 @@ lookup overloads and the derived behavior `AmmoType`/`Item` GUID properties
have been deleted. Action-bar consumable bindings now read the target `ItemKey`
directly from typed runtime catalog rows instead of reconstructing it from
legacy item IDs.
Unity runtime commit DTOs carry typed `ItemKey`/`HullItemKey` fields only for
Daemon snapshot DTOs carry typed `ItemKey`/`HullItemKey` fields only for
loadout items, action-bar targets, body resources, entity hulls, and active
consumables; the commit applier no longer reconstructs item keys from legacy
item GUID fields.
Unity runtime commit DTOs now carry typed `FactionKey` only for loadout
entities, run entity snapshots, and faction relationship state. The old
pending-command `CorporationLegacyId` fallback fields are deleted.
consumables; daemon command application no longer reconstructs item keys from
legacy item GUID fields.
Daemon snapshot DTOs now carry typed `FactionKey` only for loadout entities,
run entity snapshots, and faction relationship state. The old command-document
`CorporationLegacyId` fallback fields are deleted.
`EntityConstructionBlueprint` also carries `FactionKey` only. Loadout restore,
entity construction, run entity snapshots, and faction relationship commits no
longer rebuild corporation references from legacy `Faction.ID` GUIDs.
@ -124,10 +134,10 @@ GUID link fields.
sector-map link/influence rendering, zone security ownership checks, and
runtime faction-relationship ordering now use `FactionKey`. The legacy
`Faction.ID` projection field is deleted from the temporary simulation shell.
Run checkpoint commit DTOs carry typed `OrbitKey`, `ParentOrbitKey`, and
`BodyKey` only for zone orbit/body snapshots. The old pending-command
Run checkpoint snapshot DTOs carry typed `OrbitKey`, `ParentOrbitKey`, and
`BodyKey` only for zone orbit/body snapshots. The old command-document
`OrbitLegacyId`, `ParentLegacyId`, and `BodyLegacyId` fallback fields are
deleted. Behavior-state runtime commits and readback snapshots carry typed
deleted. Behavior-state daemon snapshots and readback snapshots carry typed
body-key fields for resource scanner targets and mining tool asteroid belts;
the old raw body ID commit/readback field names are deleted. `ResourceScanner`
and `MiningTool` now retain typed body-key runtime state directly, while
@ -319,8 +329,8 @@ does not write daemon-owned Verse state. Unity can use this as the first
package boundary once CultLib/Eve runtime packaging is available.
Unity `MainMenu` now lowers a typed Verse settings shell over that split:
client-target edits persist locally in `aetheria-client.cc`, while Verse-host
visibility changes queue provider-owned Eve commands against the selected local
Verse state file.
visibility changes append provider-owned Eve request documents against the
selected local Verse state file.
`Galaxy` now consumes this package-owned runtime catalog for faction selection
and name generation, so generated sectors use typed corporation v2 and
`aetheria.name_file.v2` records instead of legacy `Faction`/`NameFile`
@ -346,13 +356,13 @@ reviving `PlayerSettings.msgpack`, `.loadout`, or `.zone` serialization.
Unity `MainMenu` now lowers that same player-settings Eve document contract
locally through UI Toolkit instead of hand-authoring separate gameplay/graphics
widgets; the menu shell keeps name entry and back-navigation while gameplay
still owns command semantics through `ActionGameManager.CommitRuntimePlayerSettingsCommand`.
still owns command semantics through `ActionGameManager.RequestRuntimePlayerSettingsCommand`.
The run document also carries `CurrentEntityKey`, so Continue restore binds the
saved player entity by exact typed record identity instead of reconstructing it
from a transient entity-slot index. Canonical typed run state no longer stores
`CurrentZoneEntityIndex`; older stored runs are translated through the package
reader into `CurrentEntityKey`, and older queued runtime commits are translated
through the pending commit reader when those compatibility seams are needed.
reader into `CurrentEntityKey`. The older runtime commit importer has
been removed from the first-party state host and Unity package.
`Aetheria.State.Verify` opens a materialized state file and checks that the
typed migration ledger matches the actual item, corporation, and name-file
@ -371,38 +381,38 @@ symbols such as MessagePack, Newtonsoft, JsonKnownTypes, RethinkDB, or old
`DatabaseEntry`/`DatabaseLink` references; the remaining MessagePack usage is a
named package/import CultCache transport boundary, not gameplay state authority.
The package boundary is also fenced: MessagePack symbols may only appear in the
runtime catalog reader, pending CultCache envelope store, and typed runtime/Eve
runtime catalog reader, CultCache document store, and typed runtime/Eve
command document files until generated CultCache serializers replace that
temporary Unity-side codec. The verifier also checks that the Eve runtime
bootstrap exists, mounts the operations surface through the UI Toolkit presenter,
and keeps renderer commands routed through the typed Eve command log.
and keeps renderer commands routed through the typed Eve command inbox.
Use it after import when `GameData/aetheria-world.cc` changes.
`Aetheria.State.Unity.Smoke` opens the materialized state through the runtime
facade and proves read-only Unity-facing catalog access without the legacy
catalog reader. It also proves runtime commit envelopes for settings and run
zone/entity snapshots can be queued, applied through `Aetheria.State`, and
cleared. Pending runtime state and Eve command queue files are CultCache-shaped
`.cc` records with embedded schema catalogs; their current record payload codec
is still temporary package code until Unity consumes generated CultCache
catalog reader. Live Unity code uses typed Eve or daemon request documents
instead of runtime commit outboxes. Runtime and Eve requests are typed `.cc`
records with embedded schema catalogs; their current record payload codec is
still temporary package code until Unity consumes generated CultCache
serializers directly.
`Economy.Server` opens the CultMesh state host, applies pending runtime commits
and pending Eve surface commands on startup, and polls for new pending work
while running. For a bounded one-shot drain without leaving the host running:
`Aetheria.State.Daemon` opens the CultMesh Verse daemon, publishes daemon-owned
Verse identity/health/game/editor surfaces, accepts observed Eve command
documents on startup, and executes observed daemon command records each tick.
For a bounded one-shot daemon tick:
```powershell
dotnet run --project .\Economy.Server\Economy.Server.csproj -- --apply-pending-once
dotnet run --project .\Aetheria.State.Daemon\Aetheria.State.Daemon.csproj -- --once
```
The server publishes `AetheriaRuntimeCommitDrainStatus`,
`AetheriaEveCommandDrainStatus`, the `aetheria.operations` Eve surface, and the
typed `aetheria.player_settings` Eve surface after drain attempts, so pending
depth, accepted/applied/rejected counts, failures, timestamps, and player
settings controls are typed state rather than console-only status. It also
The daemon publishes `AetheriaEveCommandAcceptanceStatus`, the
`aetheria.operations` Eve surface, and the
typed `aetheria.player_settings` Eve surface after request acceptance attempts,
so observed depth, accepted/applied/rejected counts, failures, timestamps, and
player settings controls are typed state rather than console-only status. It also
persists daemon-owned `aetheria.verse_host_settings.v1`, with boot-time
defaults or overrides for service id, Verse id, visibility, and CultMesh
address. When visibility is `public`, the long-running daemon also serves a
address. When visibility is `public`, the daemon also serves a
derived CultMesh Verse catalog from that same typed host document, using the
located service name as the discoverable `cultnet://...:3075` endpoint. The
operations surface and provider advertisement project from that typed host
@ -427,19 +437,10 @@ The Unity Verse-settings shell can also trigger the same one-shot sync through
the `Aetheria.State.Replica` bridge when the repo-local tool is available. That
keeps target selection local while the replica organ owns daemon transport.
`Aetheria.State.ApplyPending` is the smaller local operator applicator for
queued Unity runtime commits and Eve commands when the server host is not being
used. It also republishes the typed drain status, operations surface,
player-settings surface, typed `aetheria.verse_host_settings.v1`, and provider
advertisement after applying pending files:
```powershell
dotnet run --project .\Aetheria.State.ApplyPending\Aetheria.State.ApplyPending.csproj -- .
```
Pass an explicit state path as the second argument when applying a non-default
`.cc` file. Use `--keep` only for diagnostics; normal application deletes
successfully applied command files.
The old standalone `Aetheria.State.DrainCommands` applicator is deleted. Command
acceptance is daemon behavior now; bounded operation uses `Aetheria.State.Daemon
--once` so the same Verse member publishes the same provider, health, command
boundary, and Eve surfaces.
Current rebuild notes:

View file

@ -0,0 +1,52 @@
# Aetheria Verse Daemon Shape
Aetheria should follow the Odin/VoidBot daemon shape instead of growing a private Unity-shaped command system.
## Authority
Odin is the all-seer, not the owner. It discovers Aetheria's provider advertisement and interface bindings, indexes the schema/catalog surface, and gives agents a registry overview. Idunn keeps the daemon alive from daemon-published health and command-boundary records. Bifrost hosts the MCP crossing for Codex and other xeno agents. Eve/CultUI defines presentation shape. The Aetheria daemon remains the side-effect owner.
The daemon owns Aetheria simulation state, typed command application, health publication, command boundaries, provider advertisement, and provider-owned Eve GUI/TUI surfaces. Unity is a renderer and input lowerer. A browser, a terminal, Blender, or another runtime should see the same daemon-owned Verse through the same typed publications.
Unity-side entity graph creation must name the boundary it is crossing. `EntityConstructionBlueprintProjector.InstantiateAuthoritativeFromBlueprint` is for local authoritative construction paths that still need to be migrated into the daemon; daemon frames use `EntityConstructionBlueprintProjector.ProjectObservedFromBlueprint` to lower observed state into Unity objects. A Unity frame consumer must not call the authoritative construction entry point.
The same rule applies at the galaxy shell. `Galaxy.ProjectObservedDaemonRun` may build a Unity navigation/rendering projection from an authoritative daemon run frame. Main menu boot must not call a `Galaxy` constructor directly or run procedural generation; the daemon owns the run and Unity lowers it. The Unity-side public handle is `ActionGameManager.ObservedGalaxy`, not `CurrentGalaxy`, because it is a projection of daemon state.
VoidBot's Odin MCP shape is the useful boundary model for agents: list providers through Odin, list Verses through Odin, load a provider-owned interface context through Odin, and invoke only commands that the provider interface advertises. The MCP is not a Brokkr special case and not a raw daemon socket wrapper.
Brokkr is just one daemon that happens to publish Unity editor surfaces. Aetheria should behave the same way: publish provider advertisements and Eve GUI/TUI surfaces into the local Verse, let Odin index them, and let Bifrost expose ergonomic MCP tools that lower or invoke those surfaces without taking ownership.
## Publications
Required Aetheria daemon publications are the live typed schemas in `AetheriaRuntimeDaemonSchemas`:
- `gamecult.aetheria.daemon_provider_advertisement.v1`: Verse id, provider ids, daemon id, schema catalog, state witnesses, Eve surface keys, command boundaries, and transport profile metadata.
- `gamecult.aetheria.daemon_health.v1`: daemon id, Verse id, state path, frame id, command acceptance status, simulation status, publication source, transport, and command-boundary path.
- `gamecult.aetheria.daemon_command_boundary.v1`: typed command schemas accepted by the daemon, authority requirements, receipt schema, and delivery transport.
- `gamecult.aetheria.daemon_frame.v1` plus `gamecult.aetheria.daemon_soa_view.v1`: authoritative simulation state for renderer clients.
- `gamecult.aetheria.daemon_game_surface.v1` and `gamecult.aetheria.daemon_editor_surface.v1`: GUI/TUI views of the same provider-owned state.
The provider advertisement is not a dashboard index. It is the daemon's promise about what it owns, where its witnesses live, what command boundary it accepts, and what transports can reach it.
Aetheria currently has two advertisement layers during the bridge:
- `gamecult.eve.provider_advertisement.v1` is the Odin-visible Eve provider card for the Aetheria Verse daemon and compatibility surfaces such as catalog, operations, and player settings.
- `gamecult.aetheria.daemon_provider_advertisement.v1` is the daemon-owned Aetheria runtime contract for authoritative frames, SoA views, game/editor GUI surfaces, game/editor TUI surfaces, health, and command boundaries.
The bridge layer must point at daemon-owned witnesses instead of becoming a second source of truth. Long term, Odin should discover the daemon-owned provider advertisement and interface bindings directly; the older state-host card should become a compatibility adapter or disappear.
## Queues
Queues are an implementation detail. Eve commands and daemon commands are typed `gamecult.eve.command.v1` and `gamecult.aetheria.daemon_command.v1` records in the Aetheria state graph; the old `.pending` runtime commit outbox is gone. None of those folders are the public API, daemon identity, or a substitute for CultNet/CultMesh command ports. Anything still named `Queue*`, `CommandLog`, `Inbox`, `mailbox`, `.eve.commands`, `.daemon.commands`, `.cc.pending`, or `PendingCultCacheStore` in Unity-facing daemon/Eve command code should lower to typed command documents and be demoted behind typed operation methods as the CultNet command boundaries come online.
`AetheriaCommandPort` is the neutral command submission implementation for typed command records. Unity-side command lowerers may use `AetheriaRuntimeCommandPort` as a facade over that same port. They must not drain those records or mutate state in response; ownership stays with the daemon Verse member.
Migration rule: do not expose string command names and payload maps as public Aetheria APIs. Compatibility adapters may decode external Eve requests at the edge, but persisted Aetheria command documents and Unity gameplay calls should use typed command bodies and typed operation methods.
Daemon-published game surface buttons select typed daemon command documents by command id (`aetheria.daemon.commands.*`). They must not smuggle daemon command fields through generic Eve payload maps such as `commandKind`, `weaponGroup`, or `scalarValue`; richer commands need explicit typed body builders.
Unity menu handlers should not construct Aetheria command bodies by spelunking `EveSurfaceCommandRequest.Payload`. Pass the request to the shared typed command client at the Eve edge, then submit the resulting `gamecult.eve.command.v1` or `gamecult.aetheria.daemon_command.v1` document.
## Cut Line
If a status/control surface matters to operators or agents, publish it as typed CultCache state or a `.cc` witness plus an Eve/CultUI CultMesh surface. Do not add a private HTTP dashboard, Unity-only inspector, JSON status blob, or agent-specific daemon wrapper as canonical truth.

85
Aetheria.sln Normal file
View file

@ -0,0 +1,85 @@

Microsoft Visual Studio Solution File, Format Version 11.00
# Visual Studio 2010
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Assembly-CSharp-firstpass", "Assembly-CSharp-firstpass.csproj", "{7c9c511c-1c6f-9c1f-c455-88359f498803}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Assembly-CSharp", "Assembly-CSharp.csproj", "{47728ed0-fa53-fa99-9dca-8eb78b1dbfe4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Aetheria.State.Unity", "GameCult.Aetheria.State.Unity.csproj", "{d6094cfd-26d1-83d1-2366-5762cca2bc33}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UniRx", "UniRx.csproj", "{99486114-c78b-e9b0-654f-710398497645}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Aetheria.EveRuntime", "GameCult.Aetheria.EveRuntime.csproj", "{a1a8b65b-ca79-2fa2-3803-5f4833778bdb}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aetheria.Shared.Unity", "Aetheria.Shared.Unity.csproj", "{31542707-f1f7-33c1-eef6-37982be14f82}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Assembly-CSharp-Editor-firstpass", "Assembly-CSharp-Editor-firstpass.csproj", "{b690125b-09ac-e423-4873-1648ff205166}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Assembly-CSharp-Editor", "Assembly-CSharp-Editor.csproj", "{6529161a-3c3e-1394-61a8-ab66047d3732}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ink", "Ink.csproj", "{6bae5155-742f-1170-f8b5-ff696c59aad8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Brokkr.Editor", "GameCult.Brokkr.Editor.csproj", "{f041e9dc-dc28-1d6c-75a3-824162b4a1ae}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CultMath", "CultMath.csproj", "{6f7a47c4-21dc-64ef-0969-96b956a000f4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Brokkr", "GameCult.Brokkr.csproj", "{fc28bcbe-9ef4-444f-c1c9-dde8a3a864ad}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Substance.Platform", "Substance.Platform.csproj", "{d6a66898-22ba-8995-ccfd-eb708fdff52a}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests.csproj", "{28568750-512b-4d45-69bf-887882a23da7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Eve.UnityUIToolkit", "GameCult.Eve.UnityUIToolkit.csproj", "{5cdb0413-71d7-c6b3-4ad9-d8e730b55ffb}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Eve.Surface", "GameCult.Eve.Surface.csproj", "{037dacf0-10f4-20ed-7963-3a1e1ac15b64}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Substance.EditorHelper", "Substance.EditorHelper.csproj", "{f12f044e-28bc-36bc-242c-ecd2736d345d}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessagePack.SourceDisabled", "MessagePack.SourceDisabled.csproj", "{b916e744-81e2-a0ba-3884-e845f1c5ef88}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7c9c511c-1c6f-9c1f-c455-88359f498803}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7c9c511c-1c6f-9c1f-c455-88359f498803}.Debug|Any CPU.Build.0 = Debug|Any CPU
{47728ed0-fa53-fa99-9dca-8eb78b1dbfe4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{47728ed0-fa53-fa99-9dca-8eb78b1dbfe4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{d6094cfd-26d1-83d1-2366-5762cca2bc33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{d6094cfd-26d1-83d1-2366-5762cca2bc33}.Debug|Any CPU.Build.0 = Debug|Any CPU
{99486114-c78b-e9b0-654f-710398497645}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{99486114-c78b-e9b0-654f-710398497645}.Debug|Any CPU.Build.0 = Debug|Any CPU
{a1a8b65b-ca79-2fa2-3803-5f4833778bdb}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{a1a8b65b-ca79-2fa2-3803-5f4833778bdb}.Debug|Any CPU.Build.0 = Debug|Any CPU
{31542707-f1f7-33c1-eef6-37982be14f82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{31542707-f1f7-33c1-eef6-37982be14f82}.Debug|Any CPU.Build.0 = Debug|Any CPU
{b690125b-09ac-e423-4873-1648ff205166}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{b690125b-09ac-e423-4873-1648ff205166}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6529161a-3c3e-1394-61a8-ab66047d3732}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6529161a-3c3e-1394-61a8-ab66047d3732}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6bae5155-742f-1170-f8b5-ff696c59aad8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6bae5155-742f-1170-f8b5-ff696c59aad8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{f041e9dc-dc28-1d6c-75a3-824162b4a1ae}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{f041e9dc-dc28-1d6c-75a3-824162b4a1ae}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6f7a47c4-21dc-64ef-0969-96b956a000f4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6f7a47c4-21dc-64ef-0969-96b956a000f4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{fc28bcbe-9ef4-444f-c1c9-dde8a3a864ad}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{fc28bcbe-9ef4-444f-c1c9-dde8a3a864ad}.Debug|Any CPU.Build.0 = Debug|Any CPU
{d6a66898-22ba-8995-ccfd-eb708fdff52a}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{d6a66898-22ba-8995-ccfd-eb708fdff52a}.Debug|Any CPU.Build.0 = Debug|Any CPU
{28568750-512b-4d45-69bf-887882a23da7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{28568750-512b-4d45-69bf-887882a23da7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5cdb0413-71d7-c6b3-4ad9-d8e730b55ffb}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5cdb0413-71d7-c6b3-4ad9-d8e730b55ffb}.Debug|Any CPU.Build.0 = Debug|Any CPU
{037dacf0-10f4-20ed-7963-3a1e1ac15b64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{037dacf0-10f4-20ed-7963-3a1e1ac15b64}.Debug|Any CPU.Build.0 = Debug|Any CPU
{f12f044e-28bc-36bc-242c-ecd2736d345d}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{f12f044e-28bc-36bc-242c-ecd2736d345d}.Debug|Any CPU.Build.0 = Debug|Any CPU
{b916e744-81e2-a0ba-3884-e845f1c5ef88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{b916e744-81e2-a0ba-3884-e845f1c5ef88}.Debug|Any CPU.Build.0 = Debug|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

BIN
Assets/DefaultVolumeProfile.asset (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7830976a04fc12843bb7f63ba390f142
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View file

@ -1,7 +1,7 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was auto-generated by com.unity.inputsystem:InputActionCodeGenerator
// version 1.14.0
// version 1.19.0
// from Assets/Resources/Aetheria.inputactions
//
// Changes to this file may cause incorrect behavior and will be lost if
@ -85,6 +85,7 @@ public partial class @AetheriaInput: IInputActionCollection2, IDisposable
public @AetheriaInput()
{
asset = InputActionAsset.FromJson(@"{
""version"": 1,
""name"": ""Aetheria"",
""maps"": [
{

View file

@ -6,7 +6,7 @@ using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using Random = Unity.Mathematics.Random;
using Random = CultMath.Random;
public class NameTools : EditorWindow
{

View file

@ -73,7 +73,7 @@ public class ActionBarConsumableBinding : ActionBarBinding
public override void Activate()
{
Entity.TryActivateConsumable(Target);
ActionGameManager.Instance.TryRequestDaemonActionBarConsumable(TargetItemKey);
}
public override void Deactivate()
@ -162,13 +162,13 @@ public class ActionBarGearBinding : ActionBarBinding
public override void Activate()
{
Active = true;
Behavior.Activate();
ActionGameManager.Instance.TryRequestDaemonActionBarBehavior(EquipmentIndex, BehaviorIndex, true);
}
public override void Deactivate()
{
Active = false;
Behavior.Deactivate();
ActionGameManager.Instance.TryRequestDaemonActionBarBehavior(EquipmentIndex, BehaviorIndex, false);
}
public override void Update()
@ -191,18 +191,12 @@ public class ActionBarWeaponGroupBinding : ActionBarBinding
public override void Activate()
{
foreach (var weapon in Entity.WeaponGroups[Group].weapons)
{
weapon.Activate();
}
ActionGameManager.Instance.TryRequestDaemonActionBarWeaponGroup(Group, true);
}
public override void Deactivate()
{
foreach (var weapon in Entity.WeaponGroups[Group].weapons)
{
weapon.Deactivate();
}
ActionGameManager.Instance.TryRequestDaemonActionBarWeaponGroup(Group, false);
}
public override void Update()

View file

@ -0,0 +1,184 @@
using System.Collections.Generic;
using GameCult.Aetheria.State.Unity;
using UnityEngine;
using UnityEngine.Rendering;
public sealed class AetheriaDaemonIndirectRenderer : MonoBehaviour
{
[SerializeField]
private AetheriaDaemonObserver observer;
[SerializeField]
private AetheriaDaemonRenderAssetCatalog assetCatalog;
[SerializeField]
private bool logMissingAssets;
[SerializeField]
private Camera targetCamera;
private MaterialPropertyBlock _properties;
private readonly Dictionary<int, AetheriaDaemonRenderGroupBatch> _batchesByGroupId =
new Dictionary<int, AetheriaDaemonRenderGroupBatch>();
private readonly HashSet<int> _seenGroupIds = new HashSet<int>();
private readonly List<int> _staleGroupIds = new List<int>();
private static readonly int MatrixBufferPropertyId =
Shader.PropertyToID(AetheriaDaemonRenderBuffer.MatrixBufferPropertyName);
private static readonly int InstanceCountPropertyId =
Shader.PropertyToID(AetheriaDaemonRenderBuffer.InstanceCountPropertyName);
public int LastDrawnCount { get; private set; }
public bool HasDrawTarget =>
observer != null &&
assetCatalog != null;
private void Reset()
{
observer = GetComponent<AetheriaDaemonObserver>();
}
private void LateUpdate()
{
DrawLatest();
}
public bool DrawLatest()
{
LastDrawnCount = 0;
if (!HasDrawTarget)
{
return false;
}
if (!observer.HasRenderNativeView)
{
return false;
}
_properties ??= new MaterialPropertyBlock();
var drewAny = false;
var index = observer.LastSoaIndex;
var view = observer.LastRenderNativeView;
_seenGroupIds.Clear();
foreach (var renderGroup in index.RenderGroups)
{
_seenGroupIds.Add(renderGroup.GroupId);
if (!assetCatalog.TryResolveMesh(renderGroup.MeshKey, out var mesh) ||
!assetCatalog.TryResolveMaterial(renderGroup.MaterialKey, out var material))
{
if (logMissingAssets)
{
Debug.LogWarning(
$"Could not resolve daemon render group {renderGroup.GroupId} " +
$"mesh='{renderGroup.MeshKey}' material='{renderGroup.MaterialKey}'.");
}
continue;
}
if (!_batchesByGroupId.TryGetValue(renderGroup.GroupId, out var batch))
{
batch = new AetheriaDaemonRenderGroupBatch();
_batchesByGroupId.Add(renderGroup.GroupId, batch);
}
if (batch.LastUploadedGeneration != view.Generation)
{
batch.UploadLatest(view, renderGroup, mesh, renderGroup.DefaultScale);
}
if (!batch.HasGpuBuffers)
{
continue;
}
_properties.Clear();
_properties.SetInt(InstanceCountPropertyId, batch.InstanceCount);
_properties.SetBuffer(MatrixBufferPropertyId, batch.MatrixBuffer);
Graphics.DrawMeshInstancedIndirect(
mesh,
renderGroup.SubMeshIndex,
material,
GetDrawBounds(renderGroup),
batch.IndirectArgsBuffer,
0,
_properties,
GetShadowCastingMode(renderGroup),
renderGroup.ReceiveShadows,
renderGroup.Layer,
targetCamera);
LastDrawnCount += batch.InstanceCount;
drewAny = true;
}
ReleaseStaleBatches();
return drewAny;
}
private Bounds GetDrawBounds(AetheriaRuntimeDaemonRenderGroupDocument renderGroup)
{
return new Bounds(
new Vector3(
renderGroup.BoundsCenterX,
renderGroup.BoundsCenterY,
renderGroup.BoundsCenterZ),
new Vector3(
renderGroup.BoundsSizeX,
renderGroup.BoundsSizeY,
renderGroup.BoundsSizeZ));
}
private static ShadowCastingMode GetShadowCastingMode(AetheriaRuntimeDaemonRenderGroupDocument renderGroup)
{
switch (renderGroup.ShadowMode)
{
case AetheriaRuntimeDaemonRenderShadowModes.Off:
return ShadowCastingMode.Off;
case AetheriaRuntimeDaemonRenderShadowModes.TwoSided:
return ShadowCastingMode.TwoSided;
case AetheriaRuntimeDaemonRenderShadowModes.ShadowsOnly:
return ShadowCastingMode.ShadowsOnly;
default:
return ShadowCastingMode.On;
}
}
private void OnDisable()
{
foreach (var batch in _batchesByGroupId.Values)
{
batch.Dispose();
}
_batchesByGroupId.Clear();
_seenGroupIds.Clear();
}
private void ReleaseStaleBatches()
{
if (_batchesByGroupId.Count == _seenGroupIds.Count)
{
return;
}
_staleGroupIds.Clear();
foreach (var groupId in _batchesByGroupId.Keys)
{
if (!_seenGroupIds.Contains(groupId))
{
_staleGroupIds.Add(groupId);
}
}
for (var i = 0; i < _staleGroupIds.Count; i++)
{
var groupId = _staleGroupIds[i];
_batchesByGroupId[groupId].Dispose();
_batchesByGroupId.Remove(groupId);
}
_staleGroupIds.Clear();
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 66b38a2f45120a041941eae4412f7da1

View file

@ -0,0 +1,158 @@
using System;
using GameCult.Aetheria.State.Unity;
using UnityEngine;
public sealed class AetheriaDaemonObserver : MonoBehaviour
{
[SerializeField]
private bool pollInUpdate = true;
[SerializeField]
private float pollIntervalSeconds = 0.05f;
[SerializeField]
private bool logChanges;
[SerializeField]
private string clientId = "unity-observer";
private readonly AetheriaRuntimeDaemonObservationCursor _cursor = new AetheriaRuntimeDaemonObservationCursor();
private float _nextPollTime;
private AetheriaRuntimeDaemonOperationClient _operationClient;
private AetheriaDaemonOperations _operations;
private AetheriaDaemonSoaMemoryMap _soaMemoryMap;
private AetheriaDaemonRenderNativeView _renderNativeView;
public AetheriaRuntimeObservedDaemonState LastObservedState { get; private set; }
public AetheriaRuntimeDaemonObservationResult LastObservation { get; private set; }
public long LastFrameId => _cursor.LastFrameId;
public long LastSoaGeneration => _cursor.LastSoaGeneration;
public bool HasAuthoritativeState => LastObservedState != null && LastObservedState.IsAuthoritative;
public bool HasSoaView => LastObservedState != null && LastObservedState.HasSoaView;
public AetheriaRuntimeDaemonSoaViewIndex LastSoaIndex =>
LastObservedState?.SoaIndex ?? AetheriaRuntimeDaemonSoaViewIndex.Empty;
public AetheriaDaemonSoaMemoryMap LastSoaMemoryMap => _soaMemoryMap;
public AetheriaDaemonRenderNativeView LastRenderNativeView => _renderNativeView;
public bool HasRenderNativeView => _renderNativeView.IsCreated;
public AetheriaDaemonOperations Operations => _operations ??= new AetheriaDaemonOperations(this);
public event Action<AetheriaRuntimeObservedDaemonState, AetheriaRuntimeDaemonObservationResult> ObservedDaemonStateChanged;
internal AetheriaRuntimeDaemonCommandEnvelope SendOperation(
AetheriaRuntimeDaemonCommandKinds kind,
Action<AetheriaRuntimeDaemonCommandDocument> configure = null)
{
_operationClient ??= new AetheriaRuntimeDaemonOperationClient(
ActionGameManager.RuntimeStateFilePath,
clientId,
LastObservedState?.Frame.SessionId ?? "local");
var command = _operationClient.Create(kind, LastObservedState);
configure?.Invoke(command);
if (!_operationClient.TrySend(command, out var envelope, out var error))
{
throw new InvalidOperationException(
$"Failed to submit Aetheria daemon operation {kind}: {error}");
}
if (logChanges)
{
Debug.Log($"Submitted Aetheria daemon operation {envelope!.Kind}: {envelope.CommandId}");
}
return envelope!;
}
private void Update()
{
if (!pollInUpdate || Time.unscaledTime < _nextPollTime)
{
return;
}
_nextPollTime = Time.unscaledTime + Mathf.Max(0.001f, pollIntervalSeconds);
Poll();
}
public bool Poll()
{
if (!AetheriaRuntimeStateReader.TryReadObservedDaemonState(
ActionGameManager.RuntimeStateFilePath,
out var observed))
{
LastObservation = _cursor.Observe(null);
return false;
}
var result = _cursor.Observe(observed);
LastObservedState = observed;
LastObservation = result;
if (result.SoaViewChanged)
{
RemapSoaView(observed);
}
if (result.Changed)
{
ObservedDaemonStateChanged?.Invoke(observed, result);
if (logChanges)
{
Debug.Log(
$"Observed Aetheria daemon frame {result.FrameId} " +
$"soa generation {result.SoaGeneration} authoritative={result.IsAuthoritative}");
}
}
return true;
}
public void ResetObservation()
{
_cursor.Reset();
DisposeSoaMemoryMap();
LastObservedState = null;
LastObservation = null;
}
private void OnDisable()
{
DisposeSoaMemoryMap();
}
private void RemapSoaView(AetheriaRuntimeObservedDaemonState observed)
{
DisposeSoaMemoryMap();
if (observed == null || !observed.HasSoaView)
{
return;
}
if (AetheriaDaemonSoaMemoryMap.TryOpen(observed.SoaIndex, out var map, out var error))
{
_soaMemoryMap = map;
if (!AetheriaDaemonRenderNativeView.TryCreate(observed.SoaIndex, map, out _renderNativeView))
{
_renderNativeView = default;
if (logChanges)
{
Debug.LogWarning("Mapped Aetheria daemon SoA memory, but no complete render native view was published.");
}
}
return;
}
if (logChanges)
{
Debug.LogWarning($"Could not map Aetheria daemon SoA view read-only: {error}");
}
}
private void DisposeSoaMemoryMap()
{
_renderNativeView = default;
_soaMemoryMap?.Dispose();
_soaMemoryMap = null;
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4f5008a2876ee014fb9e442f1f6103e0

View file

@ -0,0 +1,81 @@
using System;
using UnityEngine;
[CreateAssetMenu(
fileName = "AetheriaDaemonRenderAssetCatalog",
menuName = "Aetheria/Daemon Render Asset Catalog")]
public sealed class AetheriaDaemonRenderAssetCatalog : ScriptableObject
{
[SerializeField]
private MeshEntry[] meshes = Array.Empty<MeshEntry>();
[SerializeField]
private MaterialEntry[] materials = Array.Empty<MaterialEntry>();
public bool TryResolveMesh(string key, out Mesh mesh)
{
mesh = null;
if (string.IsNullOrWhiteSpace(key))
{
return false;
}
for (var i = 0; i < meshes.Length; i++)
{
if (string.Equals(meshes[i].Key, key, StringComparison.Ordinal))
{
mesh = meshes[i].Mesh;
return mesh != null;
}
}
mesh = Resources.Load<Mesh>(NormalizeResourceKey(key));
return mesh != null;
}
public bool TryResolveMaterial(string key, out Material material)
{
material = null;
if (string.IsNullOrWhiteSpace(key))
{
return false;
}
for (var i = 0; i < materials.Length; i++)
{
if (string.Equals(materials[i].Key, key, StringComparison.Ordinal))
{
material = materials[i].Material;
return material != null;
}
}
material = Resources.Load<Material>(NormalizeResourceKey(key));
return material != null;
}
private static string NormalizeResourceKey(string key)
{
const string resourcesPrefix = "resources://";
if (key.StartsWith(resourcesPrefix, StringComparison.OrdinalIgnoreCase))
{
return key.Substring(resourcesPrefix.Length);
}
return key;
}
[Serializable]
private struct MeshEntry
{
public string Key;
public Mesh Mesh;
}
[Serializable]
private struct MaterialEntry
{
public string Key;
public Material Material;
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8fa370b1ea2904d40ae295007ac76b48

View file

@ -0,0 +1,294 @@
using GameCult.Aetheria.State.Unity;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
public sealed class AetheriaDaemonRenderBuffer : MonoBehaviour
{
public const string MatrixBufferPropertyName = "_AetheriaObjectToWorld";
public const string InstanceCountPropertyName = "_AetheriaInstanceCount";
private const int MatrixStrideBytes = 64;
private const int DrawMeshInstancedIndirectArgsCount = 5;
[SerializeField]
private AetheriaDaemonObserver observer;
[SerializeField]
private Mesh indirectArgsMesh;
[SerializeField]
private int innerLoopBatchCount = 64;
[SerializeField]
private bool updateInLateUpdate;
private readonly uint[] _indirectArgs = new uint[DrawMeshInstancedIndirectArgsCount];
private NativeArray<float4x4> _nativeMatrices;
private GraphicsBuffer _matrixBuffer;
private GraphicsBuffer _indirectArgsBuffer;
private long _allocatedGeneration = long.MinValue;
private int _allocatedCount;
public GraphicsBuffer MatrixBuffer => _matrixBuffer;
public GraphicsBuffer IndirectArgsBuffer => _indirectArgsBuffer;
public int InstanceCount { get; private set; }
public long LastUploadedFrameId { get; private set; }
public long LastUploadedGeneration { get; private set; }
public bool HasRenderableView => observer != null && observer.HasRenderNativeView;
public bool HasGpuBuffers =>
_matrixBuffer != null &&
_matrixBuffer.IsValid() &&
InstanceCount > 0;
private static readonly int MatrixBufferPropertyId = Shader.PropertyToID(MatrixBufferPropertyName);
private static readonly int InstanceCountPropertyId = Shader.PropertyToID(InstanceCountPropertyName);
private void Reset()
{
observer = GetComponent<AetheriaDaemonObserver>();
}
private void LateUpdate()
{
if (updateInLateUpdate)
{
UploadLatest();
}
}
public bool UploadLatest()
{
return UploadLatest(indirectArgsMesh);
}
public bool UploadLatest(Mesh argsMesh)
{
return UploadLatest(argsMesh, -1);
}
public bool UploadLatest(Mesh argsMesh, int renderGroupId)
{
return UploadLatest(argsMesh, renderGroupId, 0);
}
public bool UploadLatest(Mesh argsMesh, int renderGroupId, int subMeshIndex)
{
if (!HasRenderableView)
{
InstanceCount = 0;
UpdateIndirectArgs(null, 0);
return false;
}
var view = observer.LastRenderNativeView;
EnsureCapacity(view);
if (!_nativeMatrices.IsCreated || _nativeMatrices.Length == 0)
{
InstanceCount = 0;
UpdateIndirectArgs(argsMesh, 0);
return false;
}
var index = observer.LastSoaIndex;
var defaultScale = ResolveDefaultScale(index, renderGroupId);
var renderLod = ResolveRenderLod(index, renderGroupId);
var count = renderGroupId >= 0 && view.HasRenderGroupId
? BuildRenderGroupMatrices(view, renderGroupId, renderLod, defaultScale)
: BuildAllMatrices(view, renderLod, defaultScale);
_matrixBuffer.SetData(_nativeMatrices, 0, 0, count);
InstanceCount = count;
LastUploadedFrameId = view.FrameId;
LastUploadedGeneration = view.Generation;
UpdateIndirectArgs(argsMesh, count, subMeshIndex);
return true;
}
private int BuildAllMatrices(AetheriaDaemonRenderNativeView view, int renderLod, float defaultScale)
{
if (view.HasRenderVisibility || renderLod >= 0)
{
return BuildCompactedMatrices(view, -1, renderLod, defaultScale);
}
var count = math.min(view.Count, _nativeMatrices.Length);
var handle = view.ScheduleBuildRenderMatrices(
_nativeMatrices,
defaultScale,
innerLoopBatchCount);
handle.Complete();
return count;
}
private int BuildRenderGroupMatrices(
AetheriaDaemonRenderNativeView view,
int renderGroupId,
int renderLod,
float defaultScale)
{
return BuildCompactedMatrices(view, renderGroupId, renderLod, defaultScale);
}
private int BuildCompactedMatrices(
AetheriaDaemonRenderNativeView view,
int renderGroupId,
int renderLod,
float defaultScale)
{
using var writtenCount = new NativeArray<int>(1, Allocator.TempJob);
var job = new AetheriaDaemonRenderGroupMatrixJob
{
PositionX = view.PositionX,
PositionY = view.PositionY,
PositionZ = view.PositionZ,
RotationRadians = view.RotationRadians,
PhysicsBodyRadius = view.PhysicsBodyRadius,
RenderScale = view.RenderScale,
RenderVisibility = view.RenderVisibility,
RenderLod = view.RenderLod,
RenderGroupId = view.RenderGroupId,
Matrices = _nativeMatrices,
WrittenCount = writtenCount,
TargetRenderGroupId = renderGroupId >= 0 ? (uint)renderGroupId : 0,
TargetRenderLod = renderLod,
HasRotation = view.HasRotation,
HasPhysicsRadius = view.HasPhysicsRadius,
HasRenderScale = view.HasRenderScale,
HasRenderVisibility = view.HasRenderVisibility,
HasRenderLod = view.HasRenderLod,
HasRenderGroupFilter = renderGroupId >= 0,
HasRenderLodFilter = renderLod >= 0,
UniformScale = defaultScale
};
job.Schedule().Complete();
return math.clamp(writtenCount[0], 0, _nativeMatrices.Length);
}
private static float ResolveDefaultScale(AetheriaRuntimeDaemonSoaViewIndex index, int renderGroupId)
{
if (index != null &&
renderGroupId >= 0 &&
index.TryGetRenderGroup(renderGroupId, out var renderGroup))
{
return math.max(renderGroup.DefaultScale, 0.0001f);
}
var renderGroups = index?.RenderGroups;
if (renderGroups != null && renderGroups.Count == 1)
{
return math.max(renderGroups[0].DefaultScale, 0.0001f);
}
return 1.0f;
}
private static int ResolveRenderLod(AetheriaRuntimeDaemonSoaViewIndex index, int renderGroupId)
{
if (index != null &&
renderGroupId >= 0 &&
index.TryGetRenderGroup(renderGroupId, out var renderGroup))
{
return renderGroup.Lod;
}
var renderGroups = index?.RenderGroups;
if (renderGroups != null && renderGroups.Count == 1)
{
return renderGroups[0].Lod;
}
return -1;
}
public void ApplyTo(MaterialPropertyBlock properties)
{
if (properties == null)
{
return;
}
properties.SetInt(InstanceCountPropertyId, InstanceCount);
if (HasGpuBuffers)
{
properties.SetBuffer(MatrixBufferPropertyId, _matrixBuffer);
}
}
private void OnDisable()
{
ReleaseBuffers();
InstanceCount = 0;
LastUploadedFrameId = 0;
LastUploadedGeneration = 0;
}
private void EnsureCapacity(AetheriaDaemonRenderNativeView view)
{
var count = math.max(0, view.Count);
if (_nativeMatrices.IsCreated &&
_allocatedGeneration == view.Generation &&
_allocatedCount == count)
{
return;
}
ReleaseBuffers();
_allocatedGeneration = view.Generation;
_allocatedCount = count;
if (count <= 0)
{
return;
}
_nativeMatrices = new NativeArray<float4x4>(
count,
Allocator.Persistent,
NativeArrayOptions.UninitializedMemory);
_matrixBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, count, MatrixStrideBytes);
_indirectArgsBuffer = new GraphicsBuffer(
GraphicsBuffer.Target.IndirectArguments,
DrawMeshInstancedIndirectArgsCount,
sizeof(uint));
}
private void UpdateIndirectArgs(Mesh mesh, int instanceCount)
{
UpdateIndirectArgs(mesh, instanceCount, 0);
}
private void UpdateIndirectArgs(Mesh mesh, int instanceCount, int subMeshIndex)
{
if (_indirectArgsBuffer == null || !_indirectArgsBuffer.IsValid())
{
return;
}
var safeSubMeshIndex = mesh != null
? math.clamp(subMeshIndex, 0, math.max(0, mesh.subMeshCount - 1))
: 0;
_indirectArgs[0] = mesh != null ? mesh.GetIndexCount(safeSubMeshIndex) : 0;
_indirectArgs[1] = (uint)math.max(0, instanceCount);
_indirectArgs[2] = mesh != null ? mesh.GetIndexStart(safeSubMeshIndex) : 0;
_indirectArgs[3] = mesh != null ? mesh.GetBaseVertex(safeSubMeshIndex) : 0;
_indirectArgs[4] = 0;
_indirectArgsBuffer.SetData(_indirectArgs);
}
private void ReleaseBuffers()
{
if (_nativeMatrices.IsCreated)
{
_nativeMatrices.Dispose();
}
_matrixBuffer?.Release();
_matrixBuffer = null;
_indirectArgsBuffer?.Release();
_indirectArgsBuffer = null;
_allocatedGeneration = long.MinValue;
_allocatedCount = 0;
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9cba277ef259bee438796ab4c0f3df64

View file

@ -0,0 +1,177 @@
using System;
using GameCult.Aetheria.State.Unity;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
public sealed class AetheriaDaemonRenderGroupBatch : IDisposable
{
private const int MatrixStrideBytes = 64;
private const int DrawMeshInstancedIndirectArgsCount = 5;
private readonly uint[] _indirectArgs = new uint[DrawMeshInstancedIndirectArgsCount];
private NativeArray<float4x4> _nativeMatrices;
private GraphicsBuffer _matrixBuffer;
private GraphicsBuffer _indirectArgsBuffer;
private int _capacity;
public int GroupId { get; private set; }
public int InstanceCount { get; private set; }
public long LastUploadedFrameId { get; private set; }
public long LastUploadedGeneration { get; private set; }
public GraphicsBuffer MatrixBuffer => _matrixBuffer;
public GraphicsBuffer IndirectArgsBuffer => _indirectArgsBuffer;
public bool HasGpuBuffers =>
_matrixBuffer != null &&
_matrixBuffer.IsValid() &&
_indirectArgsBuffer != null &&
_indirectArgsBuffer.IsValid() &&
InstanceCount > 0;
public bool UploadLatest(
AetheriaDaemonRenderNativeView view,
AetheriaRuntimeDaemonRenderGroupDocument renderGroup,
Mesh mesh,
float uniformScale)
{
if (!view.IsCreated || mesh == null)
{
InstanceCount = 0;
UpdateIndirectArgs(mesh, 0, renderGroup?.SubMeshIndex ?? 0);
return false;
}
var targetCapacity = renderGroup.InstanceCount >= 0
? math.min(renderGroup.InstanceCount, view.Count)
: view.Count;
EnsureCapacity(targetCapacity);
if (!_nativeMatrices.IsCreated || _nativeMatrices.Length == 0)
{
InstanceCount = 0;
UpdateIndirectArgs(mesh, 0, renderGroup.SubMeshIndex);
return false;
}
var count = view.HasRenderGroupId
? BuildRenderGroupMatrices(view, renderGroup.GroupId, renderGroup.Lod, uniformScale)
: BuildAllMatrices(view, renderGroup.Lod, uniformScale);
_matrixBuffer.SetData(_nativeMatrices, 0, 0, count);
GroupId = renderGroup.GroupId;
InstanceCount = count;
LastUploadedFrameId = view.FrameId;
LastUploadedGeneration = view.Generation;
UpdateIndirectArgs(mesh, count, renderGroup.SubMeshIndex);
return count > 0;
}
public void Dispose()
{
if (_nativeMatrices.IsCreated)
{
_nativeMatrices.Dispose();
}
_matrixBuffer?.Release();
_matrixBuffer = null;
_indirectArgsBuffer?.Release();
_indirectArgsBuffer = null;
_capacity = 0;
InstanceCount = 0;
}
private int BuildAllMatrices(AetheriaDaemonRenderNativeView view, int renderLod, float uniformScale)
{
return BuildCompactedMatrices(view, -1, renderLod, uniformScale);
}
private int BuildRenderGroupMatrices(
AetheriaDaemonRenderNativeView view,
int renderGroupId,
int renderLod,
float uniformScale)
{
return BuildCompactedMatrices(view, renderGroupId, renderLod, uniformScale);
}
private int BuildCompactedMatrices(
AetheriaDaemonRenderNativeView view,
int renderGroupId,
int renderLod,
float uniformScale)
{
using var writtenCount = new NativeArray<int>(1, Allocator.TempJob);
var job = new AetheriaDaemonRenderGroupMatrixJob
{
PositionX = view.PositionX,
PositionY = view.PositionY,
PositionZ = view.PositionZ,
RotationRadians = view.RotationRadians,
PhysicsBodyRadius = view.PhysicsBodyRadius,
RenderScale = view.RenderScale,
RenderVisibility = view.RenderVisibility,
RenderLod = view.RenderLod,
RenderGroupId = view.RenderGroupId,
Matrices = _nativeMatrices,
WrittenCount = writtenCount,
TargetRenderGroupId = renderGroupId >= 0 ? (uint)renderGroupId : 0,
TargetRenderLod = renderLod,
HasRotation = view.HasRotation,
HasPhysicsRadius = view.HasPhysicsRadius,
HasRenderScale = view.HasRenderScale,
HasRenderVisibility = view.HasRenderVisibility,
HasRenderLod = view.HasRenderLod,
HasRenderGroupFilter = renderGroupId >= 0,
HasRenderLodFilter = renderLod >= 0,
UniformScale = uniformScale
};
job.Schedule().Complete();
return math.clamp(writtenCount[0], 0, _nativeMatrices.Length);
}
private void EnsureCapacity(int capacity)
{
capacity = math.max(0, capacity);
if (_nativeMatrices.IsCreated && _capacity == capacity)
{
return;
}
Dispose();
_capacity = capacity;
if (capacity <= 0)
{
return;
}
_nativeMatrices = new NativeArray<float4x4>(
capacity,
Allocator.Persistent,
NativeArrayOptions.UninitializedMemory);
_matrixBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, capacity, MatrixStrideBytes);
_indirectArgsBuffer = new GraphicsBuffer(
GraphicsBuffer.Target.IndirectArguments,
DrawMeshInstancedIndirectArgsCount,
sizeof(uint));
}
private void UpdateIndirectArgs(Mesh mesh, int instanceCount, int subMeshIndex)
{
if (_indirectArgsBuffer == null || !_indirectArgsBuffer.IsValid())
{
return;
}
var safeSubMeshIndex = mesh != null
? math.clamp(subMeshIndex, 0, math.max(0, mesh.subMeshCount - 1))
: 0;
_indirectArgs[0] = mesh != null ? mesh.GetIndexCount(safeSubMeshIndex) : 0;
_indirectArgs[1] = (uint)math.max(0, instanceCount);
_indirectArgs[2] = mesh != null ? mesh.GetIndexStart(safeSubMeshIndex) : 0;
_indirectArgs[3] = mesh != null ? mesh.GetBaseVertex(safeSubMeshIndex) : 0;
_indirectArgs[4] = 0;
_indirectArgsBuffer.SetData(_indirectArgs);
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0038a222765ff82468c22a516cfcdbc1

View file

@ -0,0 +1,92 @@
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
[BurstCompile]
public struct AetheriaDaemonRenderGroupMatrixJob : IJob
{
[ReadOnly]
public NativeArray<float> PositionX;
[ReadOnly]
public NativeArray<float> PositionY;
[ReadOnly]
public NativeArray<float> PositionZ;
[ReadOnly]
public NativeArray<float> RotationRadians;
[ReadOnly]
public NativeArray<float> PhysicsBodyRadius;
[ReadOnly]
public NativeArray<float> RenderScale;
[ReadOnly]
public NativeArray<byte> RenderVisibility;
[ReadOnly]
public NativeArray<int> RenderLod;
[ReadOnly]
public NativeArray<uint> RenderGroupId;
[WriteOnly]
public NativeArray<float4x4> Matrices;
public NativeArray<int> WrittenCount;
public uint TargetRenderGroupId;
public int TargetRenderLod;
public bool HasRotation;
public bool HasPhysicsRadius;
public bool HasRenderScale;
public bool HasRenderVisibility;
public bool HasRenderLod;
public bool HasRenderGroupFilter;
public bool HasRenderLodFilter;
public float UniformScale;
public void Execute()
{
var written = 0;
for (var index = 0; index < PositionX.Length; index++)
{
if (HasRenderGroupFilter && RenderGroupId[index] != TargetRenderGroupId)
{
continue;
}
if (HasRenderVisibility && RenderVisibility[index] == 0)
{
continue;
}
if (HasRenderLodFilter && (!HasRenderLod || RenderLod[index] != TargetRenderLod))
{
continue;
}
if (written >= Matrices.Length)
{
break;
}
var position = new float3(PositionX[index], PositionY[index], PositionZ[index]);
var rotation = HasRotation
? quaternion.RotateY(RotationRadians[index])
: quaternion.identity;
var scaleValue = HasRenderScale
? math.max(RenderScale[index], 0.0001f)
: HasPhysicsRadius
? math.max(PhysicsBodyRadius[index] * 2.0f, 0.0001f)
: math.max(UniformScale, 0.0001f);
Matrices[written] = float4x4.TRS(position, rotation, new float3(scaleValue));
written++;
}
WrittenCount[0] = written;
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 146aa8d6d00cc1a44b44a5f83d69edea

View file

@ -0,0 +1,49 @@
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
[BurstCompile]
public struct AetheriaDaemonRenderMatrixJob : IJobParallelFor
{
[ReadOnly]
public NativeArray<float> PositionX;
[ReadOnly]
public NativeArray<float> PositionY;
[ReadOnly]
public NativeArray<float> PositionZ;
[ReadOnly]
public NativeArray<float> RotationRadians;
[ReadOnly]
public NativeArray<float> PhysicsBodyRadius;
[ReadOnly]
public NativeArray<float> RenderScale;
[WriteOnly]
public NativeArray<float4x4> Matrices;
public bool HasRotation;
public bool HasPhysicsRadius;
public bool HasRenderScale;
public float UniformScale;
public void Execute(int index)
{
var position = new float3(PositionX[index], PositionY[index], PositionZ[index]);
var rotation = HasRotation
? quaternion.RotateY(RotationRadians[index])
: quaternion.identity;
var scaleValue = HasRenderScale
? math.max(RenderScale[index], 0.0001f)
: HasPhysicsRadius
? math.max(PhysicsBodyRadius[index] * 2.0f, 0.0001f)
: math.max(UniformScale, 0.0001f);
Matrices[index] = float4x4.TRS(position, rotation, new float3(scaleValue));
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f6b3b3dc2e2a29245a24b772e2cb87b4

View file

@ -0,0 +1,248 @@
using GameCult.Aetheria.State.Unity;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
public struct AetheriaDaemonRenderNativeView
{
public long FrameId;
public long Generation;
public int Count;
public NativeArray<float> PositionX;
public NativeArray<float> PositionY;
public NativeArray<float> PositionZ;
public NativeArray<float> RotationRadians;
public NativeArray<float> VelocityX;
public NativeArray<float> VelocityY;
public NativeArray<float> VelocityZ;
public NativeArray<float> PhysicsBodyRadius;
public NativeArray<float> PhysicsBodyMass;
public NativeArray<float> PhysicsBodyInverseMass;
public NativeArray<float> RenderScale;
public NativeArray<byte> RenderVisibility;
public NativeArray<int> RenderLod;
public NativeArray<uint> RenderGroupId;
public bool IsCreated =>
Count > 0 &&
PositionX.IsCreated &&
PositionY.IsCreated &&
PositionZ.IsCreated;
public bool HasRotation => RotationRadians.IsCreated;
public bool HasVelocity => VelocityX.IsCreated && VelocityY.IsCreated && VelocityZ.IsCreated;
public bool HasPhysicsRadius => PhysicsBodyRadius.IsCreated;
public bool HasPhysicsMass => PhysicsBodyMass.IsCreated;
public bool HasPhysicsInverseMass => PhysicsBodyInverseMass.IsCreated;
public bool HasRenderScale => RenderScale.IsCreated;
public bool HasRenderVisibility => RenderVisibility.IsCreated;
public bool HasRenderLod => RenderLod.IsCreated;
public bool HasRenderGroupId => RenderGroupId.IsCreated;
public JobHandle ScheduleBuildRenderMatrices(
NativeArray<float4x4> matrices,
float uniformScale = 1.0f,
int innerLoopBatchCount = 64,
JobHandle dependsOn = default)
{
if (!IsCreated)
{
return dependsOn;
}
var count = math.min(Count, matrices.IsCreated ? matrices.Length : 0);
if (count <= 0)
{
return dependsOn;
}
var job = new AetheriaDaemonRenderMatrixJob
{
PositionX = PositionX,
PositionY = PositionY,
PositionZ = PositionZ,
RotationRadians = RotationRadians,
PhysicsBodyRadius = PhysicsBodyRadius,
RenderScale = RenderScale,
Matrices = matrices,
HasRotation = HasRotation,
HasPhysicsRadius = HasPhysicsRadius,
HasRenderScale = HasRenderScale,
UniformScale = uniformScale
};
return job.Schedule(count, math.max(1, innerLoopBatchCount), dependsOn);
}
public static bool TryCreate(
AetheriaRuntimeDaemonSoaViewIndex index,
AetheriaDaemonSoaMemoryMap map,
out AetheriaDaemonRenderNativeView view)
{
view = default;
if (index == null || map == null)
{
return false;
}
if (!map.TryCreateFirstNativeArrayOfKind(
index,
AetheriaRuntimeDaemonSoaColumnKinds.PositionX,
out view.PositionX) ||
!map.TryCreateFirstNativeArrayOfKind(
index,
AetheriaRuntimeDaemonSoaColumnKinds.PositionY,
out view.PositionY) ||
!map.TryCreateFirstNativeArrayOfKind(
index,
AetheriaRuntimeDaemonSoaColumnKinds.PositionZ,
out view.PositionZ))
{
view = default;
return false;
}
view.Count = view.PositionX.Length;
if (view.PositionY.Length != view.Count || view.PositionZ.Length != view.Count)
{
view = default;
return false;
}
TryAssignOptionalColumn(index, map, AetheriaRuntimeDaemonSoaColumnKinds.RotationRadians, view.Count, out view.RotationRadians);
TryAssignOptionalColumn(index, map, AetheriaRuntimeDaemonSoaColumnKinds.VelocityX, view.Count, out view.VelocityX);
TryAssignOptionalColumn(index, map, AetheriaRuntimeDaemonSoaColumnKinds.VelocityY, view.Count, out view.VelocityY);
TryAssignOptionalColumn(index, map, AetheriaRuntimeDaemonSoaColumnKinds.VelocityZ, view.Count, out view.VelocityZ);
TryAssignOptionalColumn(index, map, AetheriaRuntimeDaemonSoaColumnKinds.PhysicsBodyRadius, view.Count, out view.PhysicsBodyRadius);
TryAssignOptionalColumn(index, map, AetheriaRuntimeDaemonSoaColumnKinds.PhysicsBodyMass, view.Count, out view.PhysicsBodyMass);
TryAssignOptionalColumn(index, map, AetheriaRuntimeDaemonSoaColumnKinds.PhysicsBodyInverseMass, view.Count, out view.PhysicsBodyInverseMass);
TryAssignOptionalColumn(index, map, AetheriaRuntimeDaemonSoaColumnKinds.RenderScale, view.Count, out view.RenderScale);
TryAssignOptionalByteColumn(index, map, AetheriaRuntimeDaemonSoaColumnKinds.RenderVisibility, view.Count, out view.RenderVisibility);
TryAssignOptionalIntColumn(index, map, AetheriaRuntimeDaemonSoaColumnKinds.RenderLod, view.Count, out view.RenderLod);
TryAssignOptionalUIntColumn(index, map, AetheriaRuntimeDaemonSoaColumnKinds.RenderGroupId, view.Count, out view.RenderGroupId);
if ((view.VelocityX.IsCreated || view.VelocityY.IsCreated || view.VelocityZ.IsCreated) &&
!view.HasVelocity)
{
view.VelocityX = default;
view.VelocityY = default;
view.VelocityZ = default;
}
view.FrameId = index.View.FrameId;
view.Generation = index.View.Generation;
return true;
}
private static bool TryAssignOptionalColumn(
AetheriaRuntimeDaemonSoaViewIndex index,
AetheriaDaemonSoaMemoryMap map,
string kind,
int expectedCount,
out NativeArray<float> array)
{
array = default;
if (!index.TryGetFirstColumnOfKind(kind, out _))
{
return false;
}
if (!map.TryCreateFirstNativeArrayOfKind(index, kind, out array))
{
array = default;
return false;
}
if (array.Length == expectedCount)
{
return true;
}
array = default;
return false;
}
private static bool TryAssignOptionalUIntColumn(
AetheriaRuntimeDaemonSoaViewIndex index,
AetheriaDaemonSoaMemoryMap map,
string kind,
int expectedCount,
out NativeArray<uint> array)
{
array = default;
if (!index.TryGetFirstColumnOfKind(kind, out _))
{
return false;
}
if (!map.TryCreateFirstNativeArrayOfKind(index, kind, out array))
{
array = default;
return false;
}
if (array.Length == expectedCount)
{
return true;
}
array = default;
return false;
}
private static bool TryAssignOptionalIntColumn(
AetheriaRuntimeDaemonSoaViewIndex index,
AetheriaDaemonSoaMemoryMap map,
string kind,
int expectedCount,
out NativeArray<int> array)
{
array = default;
if (!index.TryGetFirstColumnOfKind(kind, out _))
{
return false;
}
if (!map.TryCreateFirstNativeArrayOfKind(index, kind, out array))
{
array = default;
return false;
}
if (array.Length == expectedCount)
{
return true;
}
array = default;
return false;
}
private static bool TryAssignOptionalByteColumn(
AetheriaRuntimeDaemonSoaViewIndex index,
AetheriaDaemonSoaMemoryMap map,
string kind,
int expectedCount,
out NativeArray<byte> array)
{
array = default;
if (!index.TryGetFirstColumnOfKind(kind, out _))
{
return false;
}
if (!map.TryCreateFirstNativeArrayOfKind(index, kind, out array))
{
array = default;
return false;
}
if (array.Length == expectedCount)
{
return true;
}
array = default;
return false;
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 48740dbd37e79954ca00a3f6bd7ccef1

View file

@ -0,0 +1,220 @@
using System;
using System.Collections.Generic;
using System.IO.MemoryMappedFiles;
using GameCult.Aetheria.State.Unity;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
public unsafe sealed class AetheriaDaemonSoaMemoryMap : IDisposable
{
private readonly Dictionary<string, MappedBuffer> _buffersById =
new Dictionary<string, MappedBuffer>(StringComparer.Ordinal);
#if ENABLE_UNITY_COLLECTIONS_CHECKS
private readonly List<AtomicSafetyHandle> _safetyHandles = new List<AtomicSafetyHandle>();
#endif
private bool _disposed;
private AetheriaDaemonSoaMemoryMap()
{
}
public static bool TryOpen(
AetheriaRuntimeDaemonSoaViewIndex index,
out AetheriaDaemonSoaMemoryMap map,
out string error)
{
map = null;
error = "";
if (index == null)
{
error = "No daemon SoA view index was provided.";
return false;
}
if (!index.IsValid)
{
error = index.ValidationErrors.Count > 0
? index.ValidationErrors[0]
: "Daemon SoA view index is invalid.";
return false;
}
var opened = new AetheriaDaemonSoaMemoryMap();
try
{
foreach (var buffer in index.View.Buffers)
{
if (!IsMappableReadOnlyBuffer(buffer))
{
continue;
}
opened.OpenBuffer(buffer);
}
map = opened;
return true;
}
catch (Exception exception)
{
opened.Dispose();
error = exception.Message;
return false;
}
}
public bool TryCreateNativeArray<T>(
AetheriaRuntimeDaemonSoaColumnBinding binding,
out NativeArray<T> array)
where T : struct
{
ThrowIfDisposed();
array = default;
if (binding == null ||
!binding.DirectMemoryCompatible ||
!_buffersById.TryGetValue(binding.Buffer.BufferId, out var buffer))
{
return false;
}
var byteOffset = checked((ulong)binding.AbsoluteByteOffset);
var byteLength = checked((ulong)Math.Max(0, binding.ByteLength));
if (byteOffset + byteLength > buffer.ByteLength)
{
return false;
}
var pointer = buffer.Pointer + byteOffset;
array = NativeArrayUnsafeUtility.ConvertExistingDataToNativeArray<T>(
pointer,
binding.Column.ElementCount,
Allocator.None);
#if ENABLE_UNITY_COLLECTIONS_CHECKS
var safetyHandle = AtomicSafetyHandle.Create();
_safetyHandles.Add(safetyHandle);
NativeArrayUnsafeUtility.SetAtomicSafetyHandle(ref array, safetyHandle);
#endif
return true;
}
public bool TryCreateFirstNativeArrayOfKind<T>(
AetheriaRuntimeDaemonSoaViewIndex index,
string kind,
out NativeArray<T> array)
where T : struct
{
ThrowIfDisposed();
array = default;
if (index == null || !index.TryGetFirstColumnOfKind(kind, out var binding))
{
return false;
}
return TryCreateNativeArray(binding, out array);
}
public void Dispose()
{
if (_disposed)
{
return;
}
#if ENABLE_UNITY_COLLECTIONS_CHECKS
for (var i = 0; i < _safetyHandles.Count; i++)
{
AtomicSafetyHandle.Release(_safetyHandles[i]);
}
_safetyHandles.Clear();
#endif
foreach (var buffer in _buffersById.Values)
{
buffer.Dispose();
}
_buffersById.Clear();
_disposed = true;
}
private void OpenBuffer(AetheriaRuntimeDaemonSoaBufferDocument buffer)
{
if (string.IsNullOrWhiteSpace(buffer.BufferId) ||
_buffersById.ContainsKey(buffer.BufferId))
{
return;
}
var byteLength = checked((ulong)(buffer.ByteOffset + buffer.ByteLength));
var memoryMappedFile = MemoryMappedFile.OpenExisting(
buffer.Location,
MemoryMappedFileRights.Read);
var accessor = memoryMappedFile.CreateViewAccessor(0, (long)byteLength, MemoryMappedFileAccess.Read);
byte* pointer = null;
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer);
_buffersById.Add(
buffer.BufferId,
new MappedBuffer(memoryMappedFile, accessor, pointer, byteLength));
}
private static bool IsMappableReadOnlyBuffer(AetheriaRuntimeDaemonSoaBufferDocument buffer)
{
return buffer != null &&
!buffer.ObserverWritable &&
!string.IsNullOrWhiteSpace(buffer.Location) &&
string.Equals(buffer.Backend, AetheriaRuntimeDaemonSoaBackends.MemoryMappedFile, StringComparison.Ordinal);
}
private void ThrowIfDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(AetheriaDaemonSoaMemoryMap));
}
}
private sealed class MappedBuffer : IDisposable
{
private readonly MemoryMappedFile _memoryMappedFile;
private readonly MemoryMappedViewAccessor _accessor;
private bool _disposed;
public MappedBuffer(
MemoryMappedFile memoryMappedFile,
MemoryMappedViewAccessor accessor,
byte* pointer,
ulong byteLength)
{
_memoryMappedFile = memoryMappedFile;
_accessor = accessor;
Pointer = pointer;
ByteLength = byteLength;
}
public byte* Pointer { get; }
public ulong ByteLength { get; }
public void Dispose()
{
if (_disposed)
{
return;
}
_accessor.SafeMemoryMappedViewHandle.ReleasePointer();
_accessor.Dispose();
_memoryMappedFile.Dispose();
_disposed = true;
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d8988c7d796df504dab64fd2c64eb868

View file

@ -1,15 +1,11 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Mathematics;
using static Unity.Mathematics.math;
public class ArticulationPoint : MonoBehaviour
{
public Transform Target;
public int Group;
public float YawMin;
public float YawMax;
@ -26,26 +22,26 @@ public class ArticulationPoint : MonoBehaviour
if (Target)
{
var targetLocal = transform.InverseTransformPoint(Target.position);
var yaw = Vector2.SignedAngle(new Vector2(0, 1), new Vector2(targetLocal.x, targetLocal.z));
var pitch = Vector2.SignedAngle(new Vector2(1, 0), new Vector2(targetLocal.z, targetLocal.y));
var targetYaw = clamp(_yaw - yaw, YawMin, YawMax);
var targetPitch = clamp(_pitch - pitch, PitchMin, PitchMax);
var targetYaw = Mathf.Clamp(_yaw - yaw, YawMin, YawMax);
var targetPitch = Mathf.Clamp(_pitch - pitch, PitchMin, PitchMax);
if (abs(targetYaw - _yaw) < Speed * Time.deltaTime)
if (Mathf.Abs(targetYaw - _yaw) < Speed * Time.deltaTime)
_yaw = targetYaw;
else _yaw = _yaw + sign(targetYaw - _yaw) * Speed * Time.deltaTime;
else _yaw = _yaw + Mathf.Sign(targetYaw - _yaw) * Speed * Time.deltaTime;
if(_yaw < -360)
if (_yaw < -360)
_yaw += 360;
if(_yaw > 360)
if (_yaw > 360)
_yaw -= 360;
if (abs(targetPitch - _pitch) < Speed * Time.deltaTime)
if (Mathf.Abs(targetPitch - _pitch) < Speed * Time.deltaTime)
_pitch = targetPitch;
else _pitch = _pitch + sign(targetPitch - _pitch) * Speed * Time.deltaTime;
else _pitch = _pitch + Mathf.Sign(targetPitch - _pitch) * Speed * Time.deltaTime;
transform.localRotation = Quaternion.Euler(_pitch, _yaw, 0);
}
}

View file

@ -5,17 +5,13 @@ using System.Linq;
using GameCult.Aetheria.State.Unity;
using UniRx;
using UnityEngine;
using Unity.Mathematics;
using static Unity.Mathematics.math;
using float2 = Unity.Mathematics.float2;
using cfloat3 = CultMath.float3;
public class EntityInstance : MonoBehaviour
{
public MeshRenderer MapIcon;
public Transform InfluencePrefab;
public Transform PingPrefab;
public Material InvisibleMaterial;
public static Transform EffectManagerParent;
public ShieldManager Shield;
public HullCollider[] HullColliders;
@ -25,7 +21,6 @@ public class EntityInstance : MonoBehaviour
public WeaponHardpoint[] WeaponHardpoints;
public ArticulationPoint[] ArticulationPoints;
public GameObject DestroyEffect;
public event Action OnFadedOut;
public event Action OnFadedIn;
@ -40,41 +35,21 @@ public class EntityInstance : MonoBehaviour
private bool _fadedElementsVisible = false;
private bool _unfadedElementsVisible = false;
private float _fadeTime;
private bool _destroyed;
private (Transform transform, MeshRenderer meshRenderer) _currentPing;
private float _pingBrightness;
private Sensor _sensor;
private Transform _influenceInstance;
//private List<(GameObject source, EquippedItem item)> _audioSources = new List<(GameObject source, EquippedItem item)>();
// private (Reactor reactor, GameObject sfxSource) _reactor;
// private Dictionary<Radiator, GameObject> _radiatorSfx = new Dictionary<Radiator, GameObject>();
private static Dictionary<string, InstantWeaponEffectManager> _instantWeaponManagers = new Dictionary<string, InstantWeaponEffectManager>();
private static Dictionary<string, ConstantWeaponEffectManager> _constantWeaponManagers = new Dictionary<string, ConstantWeaponEffectManager>();
public static void ClearWeaponManagers()
{
_instantWeaponManagers.Clear();
_constantWeaponManagers.Clear();
}
private static AetheriaRuntimeCatalogItem FindTypedHull(ItemInstance hull)
{
return ActionGameManager.RuntimeCatalog?.FindItem(hull?.ItemKey ?? "");
}
private static Shape ToShape(int width, int height, IReadOnlyList<AetheriaRuntimeShapeCell> cells)
{
var shape = new Shape(width, height);
foreach (var cell in cells)
{
if (cell.X >= 0 && cell.Y >= 0 && cell.X < width && cell.Y < height)
shape[int2(cell.X, cell.Y)] = true;
}
private static Vector2 ToVector2(CultMath.float2 value) => new Vector2(value.x, value.y);
private static Vector2 ToVector2(Unity.Mathematics.float2 value) => new Vector2(value.x, value.y);
return shape;
}
private static cfloat3 ToCult(Vector3 value) => new cfloat3(value.x, value.y, value.z);
public CompassIcon CompassIcon { get; set; }
public Dictionary<HardpointData, Transform[]> Barrels { get; private set; }
@ -186,81 +161,10 @@ public class EntityInstance : MonoBehaviour
return;
}
var hullShape = ToShape(typedHull.ShapeWidth, typedHull.ShapeHeight, typedHull.ShapeCells);
var typedHardpoints = typedHull.Hardpoints.ToArray();
if(Shield)
Shield.Entity = entity;
foreach (var hullCollider in HullColliders) hullCollider.Entity = entity;
foreach (var item in entity.Equipment)
{
foreach (var behavior in item.Behaviors)
{
if (behavior is Sensor sensor)
{
_sensor = sensor;
sensor.OnPingStart += () =>
{
var pingInstance = Instantiate(PingPrefab);
var pingMesh = pingInstance.GetComponent<MeshRenderer>();
pingInstance.position = entity.Position;
_pingBrightness = pingMesh.material.GetFloat("_Depth");
_currentPing = (pingInstance, pingMesh);
};
sensor.OnPingEnd += OnSensorPingEnd;
}
if (behavior is InstantWeapon instantWeapon)
{
var effectPrefab = instantWeapon.EffectPrefab;
if (!_instantWeaponManagers.ContainsKey(effectPrefab))
{
var managerPrefab = UnityHelpers.LoadAsset<InstantWeaponEffectManager>(effectPrefab);
if(managerPrefab)
{
_instantWeaponManagers.Add(effectPrefab, Instantiate(managerPrefab, EffectManagerParent));
}
else Debug.LogError($"No InstantWeaponEffectManager prefab found at path {effectPrefab}");
}
instantWeapon.OnFire += () =>
_instantWeaponManagers[effectPrefab].Fire(instantWeapon, item, this, entity.Target.Value != null && ZoneRenderer.EntityInstances.ContainsKey(entity.Target.Value) ? ZoneRenderer.EntityInstances[entity.Target.Value] : null);
if (behavior is ChargedWeapon chargedWeapon)
{
var chargeManager = _instantWeaponManagers[effectPrefab].GetComponent<ChargeEffectManager>();
if (chargeManager)
{
chargedWeapon.OnStartCharging += () => chargeManager.StartCharging(chargedWeapon, item, this);
chargedWeapon.OnStopCharging += () => chargeManager.StopCharging(chargedWeapon);
chargedWeapon.OnCharged += () => chargeManager.Charged(chargedWeapon);
chargedWeapon.OnFailed += () => chargeManager.Failed(chargedWeapon);
}
}
}
if (behavior is ConstantWeapon constantWeapon)
{
var effectPrefab = constantWeapon.EffectPrefab;
if (!_constantWeaponManagers.ContainsKey(effectPrefab))
{
var managerPrefab = UnityHelpers.LoadAsset<ConstantWeaponEffectManager>(effectPrefab);
if(managerPrefab)
{
_constantWeaponManagers.Add(effectPrefab, Instantiate(managerPrefab, EffectManagerParent));
}
else Debug.LogError($"No ConstantWeaponEffectManager prefab found at path {effectPrefab}");
}
constantWeapon.OnStartFiring += () =>
_constantWeaponManagers[effectPrefab].StartFiring(constantWeapon, item, this, entity.Target.Value != null ? ZoneRenderer.EntityInstances[entity.Target.Value] : null);
constantWeapon.OnStopFiring += () =>
_constantWeaponManagers[effectPrefab].StopFiring(item);
}
}
}
RadiatorMeshes = new Dictionary<Radiator, MeshRenderer>();
Barrels = new Dictionary<HardpointData, Transform[]>();
BarrelIndices = new Dictionary<HardpointData, int>();
@ -289,128 +193,6 @@ public class EntityInstance : MonoBehaviour
}
}
void DamageSchematic(float damage, Shape hitShape)
{
foreach (var v in hitShape.Coordinates)
hitShape[v] = hitShape[v] && hullShape[v];
float hullDamage = 0;
var damagePerCell = damage / hitShape.Coordinates.Length;
foreach (var v in hitShape.Coordinates)
{
var d = damagePerCell;
// Subtract surface damage from armor, passing on the remainder to the item and then to the hull
var prev = entity.Armor[v.x, v.y];
entity.Armor[v.x, v.y] = max(prev - d, 0);
entity.ArmorDamage.OnNext((v, d));
d = max(d - prev, 0);
if (d > 0.1f)
{
var item = entity.GearOccupancy[v.x, v.y];
if (item != null)
{
prev = item.EquippableItem.Durability;
item.EquippableItem.Durability = max(prev - d, 0);
entity.ItemDamage.OnNext((item, d));
d = max(d - prev, 0);
}
}
hullDamage += d;
}
if(hullDamage > .1f)
{
entity.Hull.Durability -= hullDamage;
entity.HullDamage.OnNext(hullDamage);
}
}
foreach (var collider in HullColliders)
{
collider.Splash.Subscribe(splash =>
{
var hitShape = new Shape(hullShape.Width, hullShape.Height);
foreach (var v in hullShape.Coordinates)
{
var localHitDirection = transform.InverseTransformDirection(splash.Direction);
var direction = normalize(float2(localHitDirection.x, localHitDirection.z));
var cellDot = dot(normalize(v - hullShape.CenterOfMass), direction);
if (cellDot < 0) hitShape[v] = true;
}
DamageSchematic(splash.Damage, hitShape);
});
collider.Hit.Subscribe(hit =>
{
Entity.IncomingHit.OnNext(hit.Source);
var hardpointIndex = (int) hit.TexCoord.x - 1;
var hitShape = new Shape(hullShape.Width, hullShape.Height);
// U coordinate between 0-1 indicates a hit that didn't land directly on a hardpoint
// Find the 2D position of the hit scaled to the schematic
float2 hitPos = float2.zero;
if (hardpointIndex < 0 || hardpointIndex >= typedHardpoints.Length)
{
hitPos = float2(hit.TexCoord.x * hullShape.Width, hit.TexCoord.y * hullShape.Height);
// Search all schematic border cells for the cell which is closest to the hit position
var hitCell = int2(-1);
var distance = float.MaxValue;
foreach (var v in hullShape.Coordinates)
{
var cellDist = lengthsq(hitPos - v);
if (cellDist < distance)
{
distance = cellDist;
hitCell = v;
hitPos = v + float2(.5f);
}
}
hitShape[hitCell] = true;
}
else
{
// Collider UV coordinates starting with 1 correspond to hardpoint index
var hardpoint = typedHardpoints[hardpointIndex];
// Obtain the hull coordinates of all cells occupied by the hardpoint
var hardpointShape = ToShape(hardpoint.ShapeWidth, hardpoint.ShapeHeight, hardpoint.ShapeCells);
var hardpointCells = hullShape.Inset(hardpointShape, int2(hardpoint.PositionX, hardpoint.PositionY));
hitPos = hardpointCells.CenterOfMass;
foreach (var v in hardpointCells.Coordinates)
hitShape[v] = true;
}
for (int i = 0; i < Mathf.RoundToInt(hit.Spread); i++)
{
hitShape = hitShape.Expand();
}
if (hit.Penetration > .5f)
{
// Find the local 2D vector corresponding to the direction of the incoming hit
var localHitDirection = transform.InverseTransformDirection(hit.Direction);
var penetrationVector = normalize(float2(localHitDirection.x, localHitDirection.z));
// TODO: Bresenham's line algorithm
// March a ray through the ship from the hit position
var penetrationPoint = hitPos;
var penetrationDistance = 0f;
while (penetrationDistance < hit.Penetration && hullShape[int2(penetrationPoint)])
{
penetrationDistance += .5f;
hitShape[int2(penetrationPoint)] = true;
penetrationPoint += penetrationVector * .5f;
}
}
DamageSchematic(hit.Damage, hitShape);
});
}
LookAtPoint = new GameObject($"{entity.Name} Look Point").transform;
foreach (var articulationPoint in ArticulationPoints)
@ -418,23 +200,6 @@ public class EntityInstance : MonoBehaviour
articulationPoint.Target = LookAtPoint;
}
_subscriptions.Add(Entity.HullDamage.Subscribe(_ =>
{
if (!_destroyed && Entity.Hull.Durability < .01f)
{
if (!this) return;
if (ActionGameManager.Instance?.CommitEntityDestroyed(entity, ZoneRenderer) != true)
return;
_destroyed = true;
if (DestroyEffect != null)
{
var t = Instantiate(DestroyEffect).transform;
t.position = transform.position;
}
}
}));
if (entity is OrbitalEntity orbital && orbital.IsSecureArea)
{
_influenceInstance = Instantiate(InfluencePrefab);
@ -442,11 +207,6 @@ public class EntityInstance : MonoBehaviour
}
}
private void OnSensorPingEnd()
{
Destroy(_currentPing.transform.gameObject);
}
public Transform GetBarrel(HardpointData hardpoint)
{
if (Barrels.ContainsKey(hardpoint))
@ -461,11 +221,6 @@ public class EntityInstance : MonoBehaviour
public virtual void Update()
{
if (_currentPing.transform)
{
_currentPing.transform.localScale = _sensor.PingRadius * Vector3.one;
_currentPing.meshRenderer.material.SetFloat("_Depth", _pingBrightness * _sensor.PingBrightness);
}
if (_fading)
{
if (_fadingIn)
@ -510,20 +265,23 @@ public class EntityInstance : MonoBehaviour
foreach (var x in Barrels)
{
Entity.HardpointTransforms[x.Key] = (x.Value[0].position, x.Value[0].forward);
Entity.HardpointTransforms[x.Key] = (
ToCult(x.Value[0].position),
ToCult(x.Value[0].forward));
}
LookAtPoint.position = transform.position + (Vector3) Entity.LookDirection *
(Entity.Target.Value != null ? max(Entity.TargetRange,Entity.ItemManager.GameplaySettings.ConvergenceMinimumDistance) : 10000);
LocalSpace.localPosition = transform.position = Entity.Position;
var entityPosition = (Vector3)AetheriaMath.ToUnity(Entity.CultPosition);
var entityLookDirection = (Vector3)AetheriaMath.ToUnity(Entity.CultLookDirection);
LookAtPoint.position = transform.position + entityLookDirection *
(Entity.Target.Value != null ? Mathf.Max(Entity.TargetRange, Entity.ItemManager.GameplaySettings.ConvergenceMinimumDistance) : 10000);
LocalSpace.localPosition = transform.position = entityPosition;
if (_influenceInstance)
_influenceInstance.position = new Vector3(Entity.Position.x, 0, Entity.Position.z);
_influenceInstance.position = new Vector3(entityPosition.x, 0, entityPosition.z);
}
public virtual void OnDestroy()
{
if(_sensor!=null)
_sensor.OnPingEnd -= OnSensorPingEnd;
if (_influenceInstance) Destroy(_influenceInstance.gameObject);
Destroy(LocalSpace.gameObject);
foreach(var x in _subscriptions)

View file

@ -1,15 +1,11 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
using Unity.Mathematics;
using static Unity.Mathematics.math;
public class FieldDriver : MonoBehaviour
{
public Camera Camera;
public float2 Push;
public Vector2 Push;
public float FrontTwist;
public float RearTwist;
public float TestMagnitude;
@ -28,19 +24,6 @@ public class FieldDriver : MonoBehaviour
public float MeleeAngle;
public float MeleeAngleExponent;
public float TendrilBaseRadius = 3;
public float TendrilTipRadius = .1f;
public float TendrilExtensionExponent = .75f;
public float TendrilExtendBaseAnimationExponent = .5f;
public float TendrilBaseDamping = 2;
public float TendrilTipRadiusAnimationExponent = 2f;
public float TendrilFadeExponent = 2;
// public float GrabScaleAnimationExponent = .5f;
public float GrabExtendTime;
public float GrabEnvelopTime;
public float GrabPullTime;
public RectTransform.Axis RefractionAxis = RectTransform.Axis.Horizontal;
private Material _field;
@ -49,57 +32,36 @@ public class FieldDriver : MonoBehaviour
private float _waveOffset;
private float _meleeTime;
private bool _meleeActive;
private MeshCollider _collider;
private Transform _grabObject;
private Vector3 _grabObjectStartPos;
private Vector3 _grabObjectStartTendrilBasePos;
private Vector3 _grabObjectEndPos;
private Vector3 _grabObjectVelocity;
private float _grabObjectScale;
private float _grabTime;
private GrabPhase _grabPhase;
private Vector3 _tendrilBasePos;
private Vector3 _tendrilBendTarget;
private Vector3 _tendrilTargetPos;
private float _fadePoint;
private enum GrabPhase
{
Extend,
Envelop,
Pull
}
private struct FieldHit
{
public float3 Position;
public float3 Direction;
public Vector3 Position;
public Vector3 Direction;
public float Magnitude;
public float Time;
}
void Start()
{
_collider = GetComponent<MeshCollider>();
_field = GetComponent<MeshRenderer>().material;
var clickableCollider = GetComponent<ClickableCollider>();
if(clickableCollider!=null)
{
clickableCollider.OnClick += (_, _, ray, hit) =>
{
AddHit(hit.point, ray.direction, TestMagnitude);
AddHit(hit.Point, ray.direction, TestMagnitude);
};
}
_hitBuffer = new ComputeBuffer(MaxHits, 32);
}
public void AddHit(float3 position, float3 direction, float magnitude)
public void AddHit(Vector3 position, Vector3 direction, float magnitude)
{
if (_hits.Count >= MaxHits) return;
var hit = new FieldHit
{
Position = Vector3.Scale(normalize(transform.InverseTransformPoint(position)), transform.localScale),
Direction = normalize(transform.rotation * direction),
Position = Vector3.Scale(transform.InverseTransformPoint(position).normalized, transform.localScale),
Direction = (transform.rotation * direction).normalized,
Magnitude = magnitude,
Time = 0
};
@ -115,31 +77,21 @@ public class FieldDriver : MonoBehaviour
_meleeTime = 0;
}
public bool CanGrab => _grabObject == null;
public void GrabObject(Transform t, Vector3 v)
{
_grabPhase = GrabPhase.Extend;
_grabObject = t;
_grabObjectStartPos = t.position;
_grabObjectVelocity = v;
_grabObjectScale = t.localScale.x;
_tendrilBasePos = _grabObjectStartTendrilBasePos = _collider.ClosestPoint(_grabObjectStartPos);
_fadePoint = .999f;
}
void Update()
{
var pushMag = min(length(Push), 1);
var pushMag = Mathf.Min(Push.magnitude, 1);
_waveOffset = frac(_waveOffset + Time.deltaTime * FlowSpeed * pow(max(pushMag, max(abs(FrontTwist), abs(RearTwist))), FlowSpeedThrottleExponent));
_waveOffset = Mathf.Repeat(
_waveOffset + Time.deltaTime * FlowSpeed *
Mathf.Pow(Mathf.Max(pushMag, Mathf.Max(Mathf.Abs(FrontTwist), Mathf.Abs(RearTwist))), FlowSpeedThrottleExponent),
1);
var refractionRotation = Matrix4x4.Rotate(Camera.transform.rotation).inverse;
if (RefractionAxis == RectTransform.Axis.Vertical) refractionRotation =
Matrix4x4.Rotate(Quaternion.Euler(90, 0, 0)) * refractionRotation;
_field.SetFloat("_WaveOffset", _waveOffset * PI * 2);
_field.SetFloat("_WaveOffset", _waveOffset * Mathf.PI * 2);
_field.SetFloat("_Push", pushMag);
_field.SetVector("_PushDirection", new Vector4(-Push.x, 0, -Push.y));
_field.SetFloat("_TwistFront", FrontTwist);
@ -168,8 +120,8 @@ public class FieldDriver : MonoBehaviour
_meleeTime += Time.deltaTime / MeleeDuration;
if (_meleeTime < 1)
{
_field.SetVector("_MeleeDirection", Quaternion.AngleAxis(sign(_meleeTime*2-1) * MeleeAngle * pow(abs(_meleeTime * 2 - 1), MeleeAngleExponent), Vector3.up) * -Vector3.forward);
_field.SetFloat("_MeleeDisplacement", pow(1 - 2 * abs(_meleeTime - .5f), MeleeRangeExponent) * MeleeRange);
_field.SetVector("_MeleeDirection", Quaternion.AngleAxis(Mathf.Sign(_meleeTime * 2 - 1) * MeleeAngle * Mathf.Pow(Mathf.Abs(_meleeTime * 2 - 1), MeleeAngleExponent), Vector3.up) * -Vector3.forward);
_field.SetFloat("_MeleeDisplacement", Mathf.Pow(1 - 2 * Mathf.Abs(_meleeTime - .5f), MeleeRangeExponent) * MeleeRange);
_field.SetFloat("_MeleeShape", MeleeShaping);
_field.SetFloat("_MeleeFlattening", MeleeFlatness);
}
@ -179,73 +131,6 @@ public class FieldDriver : MonoBehaviour
_field.SetFloat("_MeleeDisplacement", 0);
}
}
if(_grabObject != null)
{
_grabTime += Time.deltaTime / _grabPhase switch
{
GrabPhase.Extend => GrabExtendTime,
GrabPhase.Envelop => GrabEnvelopTime,
GrabPhase.Pull => GrabPullTime,
_ => throw new ArgumentOutOfRangeException()
};
if (_grabTime > 1)
{
_grabTime = 0;
if (_grabPhase == GrabPhase.Envelop)
{
_grabObjectEndPos = _grabObject.position;
}
if (_grabPhase == GrabPhase.Pull)
{
Destroy(_grabObject.gameObject);
_grabObject = null;
_field.SetFloat("_TendrilInfluence", 0);
}
else _grabPhase++;
}
if (_grabObject != null)
{
_tendrilBasePos = AetheriaMath.Damp(_tendrilBasePos, _grabObject.position, TendrilBaseDamping, Time.deltaTime);
switch (_grabPhase)
{
case GrabPhase.Extend:
_tendrilBendTarget = _grabObjectStartPos;
_grabObject.position += _grabObjectVelocity * Time.deltaTime;
_tendrilTargetPos = lerp(
lerp(_grabObjectStartTendrilBasePos, _grabObjectStartPos, _grabTime),
lerp(_grabObjectStartPos, _grabObject.position, _grabTime),
pow(_grabTime, TendrilExtensionExponent));
_field.SetFloat("_TendrilInfluence", pow(_grabTime, TendrilExtendBaseAnimationExponent));
_field.SetFloat("_TendrilSize", lerp(TendrilBaseRadius/2,TendrilBaseRadius,pow(_grabTime, TendrilExtendBaseAnimationExponent)));
_field.SetFloat("_TendrilRadius", _grabObjectScale * pow(_grabTime, TendrilTipRadiusAnimationExponent) * TendrilTipRadius);
break;
case GrabPhase.Envelop:
_tendrilBendTarget = _grabObjectStartPos;
_tendrilTargetPos = _grabObject.position += _grabObjectVelocity * Time.deltaTime * (1-_grabTime);
_field.SetFloat("_TendrilInfluence", 1);
_field.SetFloat("_TendrilSize", TendrilBaseRadius);
_field.SetFloat("_TendrilRadius", _grabObjectScale * TendrilTipRadius);
break;
case GrabPhase.Pull:
_tendrilTargetPos = _grabObject.position = lerp(_grabObjectEndPos, transform.position, _grabTime*_grabTime);
_tendrilBendTarget = lerp(_grabObjectStartPos, _grabObjectEndPos, _grabTime*_grabTime);
if (_fadePoint > .99 && transform.InverseTransformPoint(_grabObject.position).sqrMagnitude < 1)
_fadePoint = _grabTime;
_field.SetFloat("_TendrilInfluence", pow(smoothstep(1, _fadePoint, _grabTime), TendrilFadeExponent));
_field.SetFloat("_TendrilSize", TendrilBaseRadius);
_field.SetFloat("_TendrilRadius", _grabObjectScale * TendrilTipRadius);
break;
default:
throw new ArgumentOutOfRangeException();
}
_field.SetVector("_TendrilBase", Vector3.Scale(transform.InverseTransformPoint(_tendrilBasePos).normalized, transform.localScale));
_field.SetVector("_TendrilBend", Vector3.Scale(transform.InverseTransformPoint(_tendrilBendTarget), transform.localScale));
_field.SetVector("_TendrilTarget", Vector3.Scale(transform.InverseTransformPoint(_tendrilTargetPos), transform.localScale));
}
}
}
private float almostIdentity( float x )
@ -258,20 +143,10 @@ public class FieldDriver : MonoBehaviour
return t * t * (3.0f - 2.0f * t);
}
private const int GIZMO_STEPS = 16;
private void OnDrawGizmosSelected()
private float SmoothStep(float edge0, float edge1, float x)
{
if (_grabObject != null)
{
Vector3 previous = _tendrilBasePos;
for (int i = 1; i <= GIZMO_STEPS; i++)
{
var l = (float)i / GIZMO_STEPS;
var next = AetheriaMath.GetQuadraticSplinePosition(_tendrilBasePos, _tendrilBendTarget, _tendrilTargetPos, l);
Gizmos.DrawLine(previous, next);
previous = next;
}
}
var t = Mathf.Clamp01((x - edge0) / (edge1 - edge0));
return t * t * (3.0f - 2.0f * t);
}
private void OnDestroy()

View file

@ -15,7 +15,6 @@ public class GameSettings : ScriptableObject
public string DefaultBossSoundbank = "Boss_Metal";
public string AmbienceSoundBank = "Ambience";
public string StartingHullName = "Longinus";
public float PickupLifetime = 30;
public float LootDropProbability = .25f;
public float LootDropVelocity = 25;
public float HeatstrokePhasingFloor = 0;
@ -86,4 +85,4 @@ public class BodySettingsCollection
{
public float MinimumMass;
public CelestialBodySettings[] BodySettings;
}
}

View file

@ -1,7 +1,5 @@
using System;
using Unity.Mathematics;
using UnityEngine;
using static Unity.Mathematics.math;
using Random = UnityEngine.Random;
public class GridObject : MonoBehaviour
@ -13,7 +11,6 @@ public class GridObject : MonoBehaviour
public float Drag = .1f;
public float LaunchDrag;
private float2 _selfVelocity;
private float _timeOffset;
public Zone Zone { get; set; }
@ -27,24 +24,6 @@ public class GridObject : MonoBehaviour
void Update()
{
var t = transform;
t.localRotation = Quaternion.Euler(sin(Time.time - _timeOffset * RotationSpeed) * 90, 0, cos(Time.time - _timeOffset * RotationSpeed) * 90);
if (Zone == null) return;
var position = t.position;
var gridHeight = Zone.GetHeight(position.Flatland()) + GridOffset;
Velocity += Vector3.up * (sign(gridHeight - position.y) * GridAttraction * Time.deltaTime);
Velocity *= max(0, 1 - LaunchDrag * Time.deltaTime);
var normal = Zone.GetNormal(position.Flatland());
var force = new float2(normal.x, normal.z);
var forceMagnitude = lengthsq(force);
if (forceMagnitude > .001f)
{
var fa = 1 / (1 - forceMagnitude) - 1;
_selfVelocity += normalize(force) * Zone.Settings.GravityStrength * fa * Gravity;
}
_selfVelocity *= max(0, 1 - Drag * Time.deltaTime);
t.position = position + (Velocity + new Vector3(_selfVelocity.x, 0, _selfVelocity.y)) * Time.deltaTime;
t.localRotation = Quaternion.Euler(Mathf.Sin(Time.time - _timeOffset * RotationSpeed) * 90, 0, Mathf.Cos(Time.time - _timeOffset * RotationSpeed) * 90);
}
}
}

View file

@ -1,65 +1,65 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UniRx;
using Unity.Mathematics;
using static Unity.Mathematics.math;
using UnityEngine;
using float2 = Unity.Mathematics.float2;
public class HullCollider : MonoBehaviour
{
public Subject<HullHitEventArgs> Hit = new Subject<HullHitEventArgs>();
public Subject<HullSplashEventArgs> Splash = new Subject<HullSplashEventArgs>();
private Renderer[] _renderers;
public Entity Entity { get; set; }
public void SendHit(float damage, float penetration, float spread, DamageType damageType, Entity source, Vector2 texCoord, Vector3 direction)
public Bounds YmirBounds => CalculateBounds(transform.position);
public float YmirPlanarRadius => Mathf.Max(YmirBounds.extents.x, YmirBounds.extents.z, 0.001f);
public void RefreshYmirBoundsSource()
{
Hit.OnNext(new HullHitEventArgs
{
Damage = damage,
Penetration = penetration,
Spread = spread,
DamageType = damageType,
Source = source,
TexCoord = texCoord,
Direction = direction
});
}
public void SendSplash(float damage, DamageType damageType, Entity source, Vector3 direction)
{
Splash.OnNext(new HullSplashEventArgs
{
Damage = damage,
DamageType = damageType,
Source = source,
Direction = direction
});
_renderers = GetComponentsInChildren<Renderer>();
}
private void OnCollisionEnter(Collision other)
private void Awake()
{
Entity.Velocity += ((float3) other.impulse).xz / Time.deltaTime;
RefreshYmirBoundsSource();
}
public class HullHitEventArgs
private void OnEnable()
{
public float Damage;
public float Penetration;
public float Spread;
public DamageType DamageType;
public Entity Source;
public float2 TexCoord;
public float3 Direction;
if (_renderers == null || _renderers.Length == 0)
RefreshYmirBoundsSource();
}
public class HullSplashEventArgs
private Bounds CalculateBounds(Vector3 fallbackCenter)
{
public float Damage;
public DamageType DamageType;
public Entity Source;
public float3 Direction;
if (_renderers == null)
RefreshYmirBoundsSource();
if (_renderers == null || _renderers.Length == 0)
return new Bounds(fallbackCenter, Vector3.one);
var firstRenderer = FirstLiveRenderer();
if (firstRenderer == null)
return new Bounds(fallbackCenter, Vector3.one);
var bounds = firstRenderer.bounds;
for (var i = 0; i < _renderers.Length; i++)
{
var renderer = _renderers[i];
if (renderer == null || renderer == firstRenderer)
continue;
bounds.Encapsulate(renderer.bounds);
}
return bounds;
}
}
private Renderer FirstLiveRenderer()
{
for (var i = 0; i < _renderers.Length; i++)
{
if (_renderers[i] != null)
return _renderers[i];
}
return null;
}
}

View file

@ -1,8 +1,8 @@
using System;
using TMPro;
using Unity.Mathematics;
using UnityEngine;
using static Unity.Mathematics.math;
using static CultMath.math;
using cfloat3 = CultMath.float3;
public class ItemPickup : MonoBehaviour
{
@ -15,27 +15,28 @@ public class ItemPickup : MonoBehaviour
public ItemInstance Item { get; set; }
public ZoneRenderer ZoneRenderer { get; set; }
public float3 ViewOrigin { get; set; }
public float3 ViewDirection { get; set; }
public cfloat3 ViewOrigin { get; set; }
public cfloat3 ViewDirection { get; set; }
private float _displayTime;
private void Update()
{
var diff = (float3) transform.position - ViewOrigin;
var position = transform.position;
var diff = new cfloat3(position.x, position.y, position.z) - ViewOrigin;
var toThis = normalize(diff);
var viewAngle = acos(Vector3.Dot(toThis, ViewDirection)) * Mathf.Rad2Deg;
var viewAngle = acos(dot(toThis, ViewDirection)) * AetheriaMath.Rad2Deg;
if (length(diff) < LabelDisplayMaxDistance && viewAngle < LabelDisplayAngle)
_displayTime = Time.time;
var targetAlpha = Time.time - _displayTime < LabelPersistDuration ? 1 : 0;
var c = ScanLabel.color;
c.a = c.a + sign(targetAlpha - c.a) * (Time.deltaTime / LabelFadeDuration);
ScanLabel.color = c;
ScanLabelContainer.rotation = Quaternion.LookRotation(-toThis);
ScanLabelContainer.rotation = Quaternion.LookRotation((Vector3)AetheriaMath.ToUnity(-toThis));
}
private void OnDestroy()
{
ZoneRenderer?.DestroyLoot(this);
}
}
}

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: baa9b1a7f453a7a4aafe9c44c9fba7c3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,865 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using UnityEngine;
public sealed class AetheriaYmirPhysicsBridge : MonoBehaviour
{
private const string ProjectileBodyId = "aetheria.projectile";
private const string TargetBodyPrefix = "aetheria.target.";
private static AetheriaYmirPhysicsBridge _instance;
public string ServiceUrl = "http://127.0.0.1:8877/simulate/step";
public string OverlapSphereUrl = "http://127.0.0.1:8877/query/overlap-sphere";
public string OverlapCircleUrl = "http://127.0.0.1:8877/query/overlap-circle";
public string CastCircleUrl = "http://127.0.0.1:8877/query/cast-circle";
public string CastSphereUrl = "http://127.0.0.1:8877/query/cast-sphere";
public bool EnableProjectileCutover = true;
public float ProjectileRadius = 0.1f;
public float ProjectileMass = 1.0f;
public float TargetRadiusPadding = 0.1f;
public int TimeoutMilliseconds = 20;
public AetheriaDaemonObserver DaemonObserver;
private readonly List<YmirPhysicsBody> _projectileBodies = new List<YmirPhysicsBody>();
private readonly List<YmirPhysicsBody> _targetBodies = new List<YmirPhysicsBody>();
private readonly List<YmirPhysicsBody> _zoneBodies = new List<YmirPhysicsBody>();
private readonly List<YmirPhysicsBody> _daemonBodies = new List<YmirPhysicsBody>();
private readonly List<YmirSphereQueryBody> _clickableBodies = new List<YmirSphereQueryBody>();
private readonly Dictionary<string, HullCollider> _hullBodyMap = new Dictionary<string, HullCollider>(StringComparer.Ordinal);
private readonly Dictionary<string, ClickableCollider> _clickableBodyMap = new Dictionary<string, ClickableCollider>(StringComparer.Ordinal);
public static AetheriaYmirPhysicsBridge Instance
{
get
{
if (_instance != null) return _instance;
_instance = FindAnyObjectByType<AetheriaYmirPhysicsBridge>();
if (_instance != null) return _instance;
var bridge = new GameObject("Aetheria Ymir Physics Bridge");
DontDestroyOnLoad(bridge);
_instance = bridge.AddComponent<AetheriaYmirPhysicsBridge>();
return _instance;
}
}
public bool TryStepProjectile(Projectile projectile, float deltaTime, out AetheriaYmirProjectileStep step)
{
step = default;
if (!EnableProjectileCutover || projectile == null || deltaTime <= 0)
return false;
var request = BuildProjectileRequest(projectile, deltaTime);
if (request.world.bodies.Length == 0)
return false;
YmirStepResult result;
try
{
result = PostJson<YmirStepRequest, YmirStepResult>(ServiceUrl, request);
}
catch (Exception error)
{
Debug.LogWarning($"Ymir projectile step failed: {error.Message}");
return false;
}
if (result == null || result.world == null)
return false;
var projectileBody = FindBody(result.world, ProjectileBodyId);
if (projectileBody == null)
return false;
step.Position = new Vector3(projectileBody.position.x, projectile.transform.position.y, projectileBody.position.y);
step.Velocity = new Vector3(projectileBody.velocity.x, projectile.Velocity.y, projectileBody.velocity.y);
foreach (var contact in result.contacts ?? Array.Empty<YmirContactEvent>())
{
var otherBody = string.Equals(contact.bodyA, ProjectileBodyId, StringComparison.Ordinal)
? contact.bodyB
: string.Equals(contact.bodyB, ProjectileBodyId, StringComparison.Ordinal)
? contact.bodyA
: "";
if (string.IsNullOrWhiteSpace(otherBody))
continue;
var hull = ResolveTargetHull(projectile.TargetInstance, otherBody);
if (hull == null)
continue;
step.Hit = new AetheriaYmirProjectileHit(
hull,
new Vector3(contact.point.x, projectile.transform.position.y, contact.point.y),
new Vector3(contact.normal.x, 0, contact.normal.y).normalized);
break;
}
return true;
}
public bool TryOverlapDaemonBodies(
Vector3 center,
float radius,
out IReadOnlyList<YmirCircleOverlapHit> hits)
{
hits = Array.Empty<YmirCircleOverlapHit>();
if (!TryBuildDaemonWorld(out var world) || radius <= 0)
return false;
var request = new YmirCircleOverlapRequest
{
world = world,
center = ToVec2(center),
radius = radius
};
return TryOverlapCircle(request, out hits);
}
public bool TryCastDaemonBodies(
Vector3 origin,
Vector3 direction,
float distance,
float radius,
out IReadOnlyList<YmirCircleCastHit> hits)
{
hits = Array.Empty<YmirCircleCastHit>();
if (!TryBuildDaemonWorld(out var world) || distance <= 0 || radius < 0)
return false;
var planarDirection = new Vector3(direction.x, 0, direction.z);
if (planarDirection.sqrMagnitude <= float.Epsilon)
return false;
var request = new YmirCircleCastRequest
{
world = world,
origin = ToVec2(origin),
direction = ToVec2(planarDirection.normalized),
distance = distance,
radius = radius
};
return TryCastCircle(request, "Ymir daemon body circle cast query", out hits);
}
public bool TryOverlapTargetHulls(
EntityInstance target,
Vector3 center,
float radius,
out IReadOnlyList<AetheriaYmirOverlapHit> hits)
{
hits = Array.Empty<AetheriaYmirOverlapHit>();
if (!EnableProjectileCutover || target == null || radius <= 0)
return false;
var world = BuildTargetWorld(target);
if (world.bodies.Length == 0)
return false;
var request = new YmirCircleOverlapRequest
{
center = ToVec2(center),
radius = radius,
world = world
};
if (!TryOverlapCircle(request, out var ymirHits))
return false;
var resolved = new List<AetheriaYmirOverlapHit>();
foreach (var hit in ymirHits)
{
var hull = ResolveTargetHull(target, hit.bodyId);
if (hull == null)
continue;
resolved.Add(new AetheriaYmirOverlapHit(
hull,
new Vector3(hit.point.x, center.y, hit.point.y),
new Vector3(hit.normal.x, 0, hit.normal.y).normalized,
hit.penetration,
hit.distance));
}
hits = resolved;
return true;
}
public bool TryOverlapSphere(
IReadOnlyList<YmirSphereQueryBody> bodies,
Vector3 center,
float radius,
out IReadOnlyList<YmirSphereOverlapHit> hits)
{
hits = Array.Empty<YmirSphereOverlapHit>();
if (bodies == null || bodies.Count == 0)
return false;
var bodyArray = bodies as YmirSphereQueryBody[] ?? CopySphereQueryBodies(bodies);
return TryOverlapSphere(bodyArray, center, radius, "Ymir sphere overlap query", out hits);
}
public bool TryCastTargetHulls(
EntityInstance target,
Vector3 origin,
Vector3 direction,
float distance,
float radius,
out IReadOnlyList<AetheriaYmirCastHit> hits)
{
hits = Array.Empty<AetheriaYmirCastHit>();
if (!EnableProjectileCutover || target == null || distance <= 0 || direction.sqrMagnitude <= 0.000001f)
return false;
var world = BuildTargetWorld(target);
if (world.bodies.Length == 0)
return false;
var request = new YmirCircleCastRequest
{
origin = ToVec2(origin),
direction = ToVec2(direction.normalized),
distance = distance,
radius = Mathf.Max(0, radius),
world = world
};
if (!TryCastCircle(request, "Ymir target cast query", out var ymirHits))
return false;
var resolved = new List<AetheriaYmirCastHit>();
foreach (var hit in ymirHits)
{
var hull = ResolveTargetHull(target, hit.bodyId);
if (hull == null)
continue;
resolved.Add(new AetheriaYmirCastHit(
hull,
ResolveShield(hull),
new Vector3(hit.point.x, origin.y, hit.point.y),
new Vector3(hit.normal.x, 0, hit.normal.y).normalized,
hit.distance));
}
hits = resolved;
return true;
}
public bool TryCastClickables(
IReadOnlyList<ClickableCollider> clickables,
Ray ray,
float distance,
out AetheriaYmirClickableHit hit)
{
hit = default;
if (!EnableProjectileCutover || clickables == null || clickables.Count == 0 || distance <= 0)
return false;
_clickableBodyMap.Clear();
_clickableBodies.Clear();
for (var i = 0; i < clickables.Count; i++)
{
var clickable = clickables[i];
if (clickable == null || !clickable.isActiveAndEnabled)
continue;
var bounds = clickable.ClickBounds;
var bodyId = $"aetheria.clickable.{i}";
_clickableBodyMap[bodyId] = clickable;
_clickableBodies.Add(new YmirSphereQueryBody
{
id = bodyId,
position = ToVec3(bounds.center),
radius = Mathf.Max(0.001f, bounds.extents.magnitude)
});
}
if (_clickableBodies.Count == 0)
return false;
var bodyArray = _clickableBodies.ToArray();
var request = new YmirSphereCastRequest
{
origin = ToVec3(ray.origin),
direction = ToVec3(ray.direction.normalized),
distance = distance,
radius = 0,
bodies = bodyArray
};
if (!TryCastSphere(request, "Ymir clickable cast query", out var ymirHits))
return false;
foreach (var resultHit in ymirHits)
{
if (!_clickableBodyMap.TryGetValue(resultHit.bodyId, out var clickable) || clickable == null)
continue;
hit = new AetheriaYmirClickableHit(
clickable,
ToVector3(resultHit.point),
ToVector3(resultHit.normal).normalized,
resultHit.distance);
return true;
}
return false;
}
public bool TryOverlapZoneHulls(
ZoneRenderer zoneRenderer,
Entity sourceEntity,
Vector3 center,
float radius,
out IReadOnlyList<AetheriaYmirOverlapHit> hits)
{
hits = Array.Empty<AetheriaYmirOverlapHit>();
if (!EnableProjectileCutover || zoneRenderer == null || radius <= 0)
return false;
_hullBodyMap.Clear();
var world = BuildZoneWorld(zoneRenderer, sourceEntity, _hullBodyMap);
if (world.bodies.Length == 0)
return false;
var request = new YmirCircleOverlapRequest
{
center = ToVec2(center),
radius = radius,
world = world
};
if (!TryOverlapCircle(request, out var ymirHits))
return false;
var resolved = new List<AetheriaYmirOverlapHit>();
foreach (var hit in ymirHits)
{
if (!_hullBodyMap.TryGetValue(hit.bodyId ?? "", out var hull))
continue;
resolved.Add(new AetheriaYmirOverlapHit(
hull,
new Vector3(hit.point.x, center.y, hit.point.y),
new Vector3(hit.normal.x, 0, hit.normal.y).normalized,
hit.penetration,
hit.distance));
}
hits = resolved;
return true;
}
public bool TryCastZoneHulls(
ZoneRenderer zoneRenderer,
Entity sourceEntity,
Vector3 origin,
Vector3 direction,
float distance,
float radius,
out IReadOnlyList<AetheriaYmirCastHit> hits)
{
hits = Array.Empty<AetheriaYmirCastHit>();
if (!EnableProjectileCutover || zoneRenderer == null || distance <= 0 || direction.sqrMagnitude <= 0.000001f)
return false;
_hullBodyMap.Clear();
var world = BuildZoneWorld(zoneRenderer, sourceEntity, _hullBodyMap);
if (world.bodies.Length == 0)
return false;
var request = new YmirCircleCastRequest
{
origin = ToVec2(origin),
direction = ToVec2(direction.normalized),
distance = distance,
radius = Mathf.Max(0, radius),
world = world
};
if (!TryCastCircle(request, "Ymir zone cast query", out var ymirHits))
return false;
var resolved = new List<AetheriaYmirCastHit>();
foreach (var hit in ymirHits)
{
if (!_hullBodyMap.TryGetValue(hit.bodyId ?? "", out var hull))
continue;
resolved.Add(new AetheriaYmirCastHit(
hull,
ResolveShield(hull),
new Vector3(hit.point.x, origin.y, hit.point.y),
new Vector3(hit.normal.x, 0, hit.normal.y).normalized,
hit.distance));
}
hits = resolved;
return true;
}
private YmirStepRequest BuildProjectileRequest(Projectile projectile, float deltaTime)
{
var transform = projectile.transform;
_projectileBodies.Clear();
_projectileBodies.Add(new YmirPhysicsBody
{
id = ProjectileBodyId,
position = ToVec2(transform.position),
velocity = ToVec2(projectile.Velocity),
direction = ToVec2(transform.forward),
angularVelocity = 0,
torque = 0,
momentOfInertia = 1,
radius = Mathf.Max(ProjectileRadius, 0.001f),
mass = Mathf.Max(ProjectileMass, 0.001f),
isStatic = false,
restitution = 0
});
var target = projectile.TargetInstance;
if (target == null)
{
return new YmirStepRequest
{
deltaTime = deltaTime,
world = new YmirWorld
{
time = projectile.YmirWorldTime,
bodies = _projectileBodies.ToArray(),
fields = Array.Empty<YmirRadialField>()
}
};
}
_projectileBodies.AddRange(BuildTargetWorld(target).bodies);
return new YmirStepRequest
{
deltaTime = deltaTime,
world = new YmirWorld
{
time = projectile.YmirWorldTime,
bodies = _projectileBodies.ToArray(),
fields = Array.Empty<YmirRadialField>()
}
};
}
private YmirWorld BuildTargetWorld(EntityInstance target)
{
_targetBodies.Clear();
for (var i = 0; i < target.HullColliders.Length; i++)
{
var hull = target.HullColliders[i];
if (hull == null)
continue;
var bounds = hull.YmirBounds;
_targetBodies.Add(new YmirPhysicsBody
{
id = TargetBodyPrefix + i,
position = ToVec2(bounds.center),
velocity = default,
direction = ToVec2(hull.transform.forward),
angularVelocity = 0,
torque = 0,
momentOfInertia = 1,
radius = hull.YmirPlanarRadius + TargetRadiusPadding,
mass = 1,
isStatic = true,
restitution = 0
});
}
return new YmirWorld
{
time = 0,
bodies = _targetBodies.ToArray(),
fields = Array.Empty<YmirRadialField>()
};
}
private YmirWorld BuildZoneWorld(
ZoneRenderer zoneRenderer,
Entity sourceEntity,
Dictionary<string, HullCollider> bodyMap)
{
_zoneBodies.Clear();
var entityIndex = 0;
foreach (var entityInstance in zoneRenderer.EntityInstances.Values)
{
if (entityInstance == null || entityInstance.Entity == sourceEntity)
continue;
for (var hullIndex = 0; hullIndex < entityInstance.HullColliders.Length; hullIndex++)
{
var hull = entityInstance.HullColliders[hullIndex];
if (hull == null)
continue;
var bounds = hull.YmirBounds;
var bodyId = $"aetheria.entity.{entityIndex}.hull.{hullIndex}";
bodyMap[bodyId] = hull;
_zoneBodies.Add(new YmirPhysicsBody
{
id = bodyId,
position = ToVec2(bounds.center),
velocity = default,
direction = ToVec2(hull.transform.forward),
angularVelocity = 0,
torque = 0,
momentOfInertia = 1,
radius = hull.YmirPlanarRadius + TargetRadiusPadding,
mass = 1,
isStatic = true,
restitution = 0
});
}
entityIndex++;
}
return new YmirWorld
{
time = 0,
bodies = _zoneBodies.ToArray(),
fields = Array.Empty<YmirRadialField>()
};
}
private bool TryBuildDaemonWorld(out YmirWorld world)
{
world = null;
var observer = ResolveDaemonObserver();
if (observer == null || !observer.HasRenderNativeView)
return false;
var view = observer.LastRenderNativeView;
if (!view.IsCreated || !view.HasPhysicsRadius)
return false;
_daemonBodies.Clear();
for (var i = 0; i < view.Count; i++)
{
var radius = view.PhysicsBodyRadius[i];
if (radius <= 0)
continue;
var inverseMass = view.HasPhysicsInverseMass ? view.PhysicsBodyInverseMass[i] : 1.0f;
var mass = view.HasPhysicsMass && view.PhysicsBodyMass[i] > 0
? view.PhysicsBodyMass[i]
: inverseMass > 0
? 1.0f / inverseMass
: 1.0f;
var rotation = view.HasRotation ? view.RotationRadians[i] : 0;
_daemonBodies.Add(new YmirPhysicsBody
{
id = $"aetheria.daemon.body.{i}",
position = new YmirVec2 { x = view.PositionX[i], y = view.PositionZ[i] },
velocity = view.HasVelocity
? new YmirVec2 { x = view.VelocityX[i], y = view.VelocityZ[i] }
: default,
direction = new YmirVec2 { x = Mathf.Sin(rotation), y = Mathf.Cos(rotation) },
angularVelocity = 0,
torque = 0,
momentOfInertia = 1,
radius = radius,
mass = Mathf.Max(mass, 0.001f),
isStatic = view.HasPhysicsInverseMass && inverseMass <= 0,
restitution = 0
});
}
if (_daemonBodies.Count == 0)
return false;
world = new YmirWorld
{
time = Time.time,
bodies = _daemonBodies.ToArray(),
fields = Array.Empty<YmirRadialField>()
};
return true;
}
private AetheriaDaemonObserver ResolveDaemonObserver()
{
if (DaemonObserver != null)
return DaemonObserver;
DaemonObserver = FindAnyObjectByType<AetheriaDaemonObserver>();
return DaemonObserver;
}
private bool TryOverlapSphere(
YmirSphereQueryBody[] bodies,
Vector3 center,
float radius,
string failureContext,
out IReadOnlyList<YmirSphereOverlapHit> hits)
{
hits = Array.Empty<YmirSphereOverlapHit>();
if (!EnableProjectileCutover || bodies == null || bodies.Length == 0 || radius <= 0)
return false;
try
{
var result = PostJson<YmirSphereOverlapRequest, YmirSphereOverlapResult>(OverlapSphereUrl, new YmirSphereOverlapRequest
{
center = ToVec3(center),
radius = radius,
bodies = bodies
});
hits = result.hits ?? Array.Empty<YmirSphereOverlapHit>();
return true;
}
catch (Exception error)
{
Debug.LogWarning($"{failureContext} failed: {error.Message}");
return false;
}
}
private bool TryCastSphere(
YmirSphereCastRequest request,
string failureContext,
out IReadOnlyList<YmirSphereCastHit> hits)
{
hits = Array.Empty<YmirSphereCastHit>();
if (!EnableProjectileCutover || request == null || request.bodies == null || request.bodies.Length == 0 || request.distance <= 0 || request.radius < 0)
return false;
try
{
var result = PostJson<YmirSphereCastRequest, YmirSphereCastResult>(CastSphereUrl, request);
hits = result.hits ?? Array.Empty<YmirSphereCastHit>();
return true;
}
catch (Exception error)
{
Debug.LogWarning($"{failureContext} failed: {error.Message}");
return false;
}
}
private bool TryOverlapCircle(
YmirCircleOverlapRequest request,
out IReadOnlyList<YmirCircleOverlapHit> hits)
{
hits = Array.Empty<YmirCircleOverlapHit>();
if (!EnableProjectileCutover || request == null || request.world == null || request.world.bodies == null || request.world.bodies.Length == 0 || request.radius <= 0)
return false;
if (string.IsNullOrWhiteSpace(OverlapCircleUrl))
{
Debug.LogWarning("Ymir circle overlap query failed: daemon URL is not configured.");
return false;
}
try
{
var result = PostJson<YmirCircleOverlapRequest, YmirCircleOverlapResult>(OverlapCircleUrl, request);
hits = result.hits ?? Array.Empty<YmirCircleOverlapHit>();
return true;
}
catch (Exception error)
{
Debug.LogWarning($"Ymir circle overlap query failed: {error.Message}");
return false;
}
}
private bool TryCastCircle(
YmirCircleCastRequest request,
string failureContext,
out IReadOnlyList<YmirCircleCastHit> hits)
{
hits = Array.Empty<YmirCircleCastHit>();
if (!EnableProjectileCutover || request == null || request.world == null || request.world.bodies == null || request.world.bodies.Length == 0 || request.distance <= 0 || request.radius < 0)
return false;
try
{
var result = PostJson<YmirCircleCastRequest, YmirCircleCastResult>(CastCircleUrl, request);
hits = result.hits ?? Array.Empty<YmirCircleCastHit>();
return true;
}
catch (Exception error)
{
Debug.LogWarning($"{failureContext} failed: {error.Message}");
return false;
}
}
private static YmirSphereQueryBody[] CopySphereQueryBodies(IReadOnlyList<YmirSphereQueryBody> bodies)
{
var copy = new YmirSphereQueryBody[bodies.Count];
for (var i = 0; i < copy.Length; i++)
copy[i] = bodies[i];
return copy;
}
private TResult PostJson<TRequest, TResult>(string url, TRequest request)
where TResult : class
{
if (string.IsNullOrWhiteSpace(url))
throw new InvalidOperationException("Ymir daemon URL is not configured.");
var json = JsonUtility.ToJson(request);
var bytes = Encoding.UTF8.GetBytes(json);
var http = (HttpWebRequest)WebRequest.Create(url);
http.Method = "POST";
http.ContentType = "application/json; charset=utf-8";
http.ContentLength = bytes.Length;
http.Timeout = TimeoutMilliseconds;
http.ReadWriteTimeout = TimeoutMilliseconds;
using (var stream = http.GetRequestStream())
stream.Write(bytes, 0, bytes.Length);
using var response = (HttpWebResponse)http.GetResponse();
using var reader = new StreamReader(response.GetResponseStream());
var payload = reader.ReadToEnd();
if (string.IsNullOrWhiteSpace(payload))
throw new InvalidDataException("Ymir daemon returned an empty response.");
var result = JsonUtility.FromJson<TResult>(payload);
if (result == null)
throw new InvalidDataException("Ymir daemon returned a response that could not be parsed.");
return result;
}
private static YmirPhysicsBody FindBody(YmirWorld world, string bodyId)
{
foreach (var body in world.bodies ?? Array.Empty<YmirPhysicsBody>())
{
if (string.Equals(body.id, bodyId, StringComparison.Ordinal))
return body;
}
return null;
}
private static HullCollider ResolveTargetHull(EntityInstance target, string bodyId)
{
if (!bodyId.StartsWith(TargetBodyPrefix, StringComparison.Ordinal))
return null;
return int.TryParse(bodyId.Substring(TargetBodyPrefix.Length), out var index) &&
index >= 0 &&
index < target.HullColliders.Length
? target.HullColliders[index]
: null;
}
private static ShieldManager ResolveShield(HullCollider hull)
{
var instance = hull != null ? hull.GetComponentInParent<EntityInstance>() : null;
return instance != null ? instance.Shield : null;
}
private static YmirVec2 ToVec2(Vector3 value) => new YmirVec2 { x = value.x, y = value.z };
private static YmirVec2 ToVec2(CultMath.float2 value) => new YmirVec2 { x = value.x, y = value.y };
private static YmirVec3 ToVec3(Vector3 value) => new YmirVec3 { x = value.x, y = value.y, z = value.z };
private static Vector3 ToVector3(YmirVec3 value) => new Vector3(value.x, value.y, value.z);
private static Bounds ObjectBounds(GameObject gameObject, Vector3 fallbackCenter)
{
var renderers = gameObject.GetComponentsInChildren<Renderer>();
if (renderers.Length == 0)
return new Bounds(fallbackCenter, Vector3.one);
var bounds = renderers[0].bounds;
for (var i = 1; i < renderers.Length; i++)
bounds.Encapsulate(renderers[i].bounds);
return bounds;
}
}
public struct AetheriaYmirProjectileStep
{
public Vector3 Position;
public Vector3 Velocity;
public AetheriaYmirProjectileHit Hit;
public bool HasHit => Hit.Hull != null;
}
public readonly struct AetheriaYmirProjectileHit
{
public AetheriaYmirProjectileHit(HullCollider hull, Vector3 point, Vector3 normal)
{
Hull = hull;
Point = point;
Normal = normal;
}
public HullCollider Hull { get; }
public Vector3 Point { get; }
public Vector3 Normal { get; }
}
public readonly struct AetheriaYmirOverlapHit
{
public AetheriaYmirOverlapHit(HullCollider hull, Vector3 point, Vector3 normal, float penetration, float distance)
{
Hull = hull;
Point = point;
Normal = normal;
Penetration = penetration;
Distance = distance;
}
public HullCollider Hull { get; }
public Vector3 Point { get; }
public Vector3 Normal { get; }
public float Penetration { get; }
public float Distance { get; }
}
public readonly struct AetheriaYmirCastHit
{
public AetheriaYmirCastHit(HullCollider hull, ShieldManager shield, Vector3 point, Vector3 normal, float distance)
{
Hull = hull;
Shield = shield;
Point = point;
Normal = normal;
Distance = distance;
}
public HullCollider Hull { get; }
public ShieldManager Shield { get; }
public Vector3 Point { get; }
public Vector3 Normal { get; }
public float Distance { get; }
}
public readonly struct AetheriaYmirClickableHit
{
public AetheriaYmirClickableHit(ClickableCollider clickable, Vector3 point, Vector3 normal, float distance)
{
Clickable = clickable;
Point = point;
Normal = normal;
Distance = distance;
}
public ClickableCollider Clickable { get; }
public Vector3 Point { get; }
public Vector3 Normal { get; }
public float Distance { get; }
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8d2521ff2dd1835498a2c00656d4bff6

View file

@ -1,79 +1,42 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Unity.Mathematics;
using static Unity.Mathematics.math;
using UnityEngine;
public class ShieldManager : MonoBehaviour
{
public Prototype ShieldPrototype;
public float CollisionHitDuration = 3;
public float LootInteractionCooldown = 0.25f;
public float ShieldCollisionCooldown = 0.1f;
public Entity Entity { get; set; }
private void OnCollisionEnter(Collision other)
private bool IsShieldActive()
{
var otherShield = other.collider.GetComponent<ShieldManager>();
if (!otherShield)
{
var gridObject = other.collider.GetComponent<GridObject>();
if (gridObject)
{
var itemPickup = other.collider.GetComponent<ItemPickup>();
var mine = other.collider.GetComponent<Mine>();
if (itemPickup)
{
if (ActionGameManager.Instance != null &&
ActionGameManager.Instance.CommitLootPickup(Entity, itemPickup.Item))
{
// TODO: Pickup notification!
Destroy(itemPickup.gameObject);
}
else
{
// TODO: Pickup failed notification!
var cp = other.GetContact(0);
gridObject.Velocity += cp.normal * 25;
Debug.Log("Attempted item pickup, but no space in cargo bay!");
}
}
else if (mine)
{
mine.Explode();
}
else
{
Debug.Log("Shield collision occurred with grid object, but other collider isn't a mine or an item pickup!");
}
}
else
Debug.Log("Shield collision occurred, but other collider isn't a shield or a grid object!");
return;
}
var contact = other.GetContact(0);
var normal = normalize(float2(contact.normal.x, contact.normal.z));
var tangent = normal.Rotate(ItemRotation.CounterClockwise);
var v1n = dot(normal, Entity.Velocity);
var v1t = dot(tangent, Entity.Velocity);
var v2n = dot(normal, otherShield.Entity.Velocity);
//var v2t = dot(tangent, otherShield.Entity.Velocity);
var v1np = PostCollisionVelocity(v1n, Entity.Mass, v2n, otherShield.Entity.Mass);
Entity.Velocity = tangent * v1t + normal * v1np;
if(Entity.Shield != null && Entity.Shield.Item.Active.Value) ShowHit(contact.point, CollisionHitDuration);
return Entity?.Shield != null && Entity.Shield.Item.Active.Value;
}
private float PostCollisionVelocity(float v1, float m1, float v2, float m2)
private Bounds ShieldBounds()
{
return (v1 * (m1 - m2) + m2 * v2) / (m1 + m2);
var renderers = GetComponentsInChildren<Renderer>();
if (renderers.Length == 0)
return new Bounds(transform.position, Vector3.one);
var bounds = renderers[0].bounds;
for (var i = 1; i < renderers.Length; i++)
bounds.Encapsulate(renderers[i].bounds);
return bounds;
}
private static float ShieldPlanarRadius(Bounds bounds)
{
return Mathf.Max(bounds.extents.x, bounds.extents.z, 0.001f);
}
public void ShowHit(Vector3 point, float duration)
{
var shield = ShieldPrototype.Instantiate<ShieldAnimation>();
shield.Direction = normalize(shield.transform.InverseTransformPoint(point));
shield.Direction = shield.transform.InverseTransformPoint(point).normalized;
shield.Duration = duration;
}
}

View file

@ -1,8 +1,4 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Mathematics;
using static Unity.Mathematics.math;
public class ShieldTest : MonoBehaviour
{
@ -12,12 +8,13 @@ public class ShieldTest : MonoBehaviour
// Update is called once per frame
void Update()
{
_velocity += -sign(transform.position.x) * Acceleration * Time.deltaTime;
var previousX = transform.position.x;
_velocity += -Mathf.Sign(transform.position.x) * Acceleration * Time.deltaTime;
transform.position += Vector3.right * (_velocity * Time.deltaTime);
}
private void OnCollisionEnter(Collision other)
{
_velocity = -_velocity;
if (Mathf.Sign(transform.position.x) != Mathf.Sign(previousX))
{
_velocity = -_velocity;
}
}
}

View file

@ -1,10 +1,7 @@
using System;
using System.Collections;
using System.Linq;
using GameCult.Aetheria.State.Unity;
using UnityEngine;
using Unity.Mathematics;
using static Unity.Mathematics.math;
public class ShipInstance : EntityInstance
{
@ -108,11 +105,11 @@ public class ShipInstance : EntityInstance
base.Update();
TractorBeam.Power = Entity.TractorPower;
TractorBeam.Direction = Entity.LookDirection;
TractorBeam.Direction = (Vector3)AetheriaMath.ToUnity(Entity.CultLookDirection);
if (_aetherDrive != null)
{
var thrust = length(_aetherDrive.Drive.ThrustDirection);
var thrust = CultMath.math.length(_aetherDrive.Drive.ThrustDirection);
var forceOverLifetime = _aetherDrive.Particles.forceOverLifetime;
forceOverLifetime.xMultiplier = _aetherDrive.Drive.ThrustDirection.x * _aetherDrive.BaseForce;
forceOverLifetime.zMultiplier = _aetherDrive.Drive.ThrustDirection.y * _aetherDrive.BaseForce;
@ -128,7 +125,8 @@ public class ShipInstance : EntityInstance
emissionModule.rateOverTimeMultiplier = thrusterInstance.BaseEmission * thrusterInstance.Thruster.Axis * (item.Durability / GetMaxDurability(item));
}
transform.rotation = Ship.Rotation;
var rotation = Ship.Rotation;
transform.rotation = new Quaternion(rotation.x, rotation.y, rotation.z, rotation.w);
}
private float GetMaxDurability(ItemInstance item)

View file

@ -1,19 +0,0 @@
using System;
using UnityEngine;
public class TimedDestroy : MonoBehaviour
{
public float Duration;
private float _startTime;
private void Start()
{
_startTime = Time.time;
}
public void Update()
{
if(Time.time - _startTime > Duration) Destroy(gameObject);
}
}

View file

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 5d5539a23b814254b038a16f32f1d8a9
timeCreated: 1618453298

View file

@ -15,19 +15,6 @@ public class TractorBeam : MonoBehaviour
{
transform.rotation = Quaternion.LookRotation(Direction);
var emission = ParticleSystem.emission;
if (Power > .01f)
{
var objectFound = false;
foreach (var o in Physics.SphereCastAll(transform.position, Radius, Direction, Distance, 1 << 21))
{
var gridObject = o.collider.GetComponent<GridObject>();
if (gridObject == null) continue;
objectFound = true;
gridObject.Velocity += Vector3.Normalize(transform.position - gridObject.transform.position) * (Traction * Time.deltaTime);
}
emission.rateOverTimeMultiplier = objectFound ? Power : 0;
}
else emission.rateOverTimeMultiplier = 0;
emission.rateOverTimeMultiplier = Power > .01f ? Power : 0;
}
}
}

View file

@ -1,7 +1,4 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Unity.Mathematics.math;
public class ConstantLaser : MonoBehaviour
{
@ -70,34 +67,34 @@ public class ConstantLaser : MonoBehaviour
}
LineRenderer.SetPosition(0, transform.position);
bool hitFound = false;
foreach (var hit in Physics.RaycastAll(new Ray(transform.position, transform.forward), Range, 1 | (1 << 17)))
var hitFound = false;
if (AetheriaYmirPhysicsBridge.Instance.TryCastZoneHulls(
ActionGameManager.Instance?.ZoneRenderer,
SourceEntity,
transform.position,
transform.forward,
Range,
0,
out var hits))
{
var shield = hit.collider.GetComponent<ShieldManager>();
if (shield)
foreach (var hit in hits)
{
if (!(shield.Entity.Shield != null && shield.Entity.Shield.Item.Active.Value && shield.Entity.Shield.CanTakeHit(DamageType, Damage))) continue;
if (shield.Entity != SourceEntity)
var hull = hit.Hull;
var entity = hull.Entity;
if (entity.Shield != null && entity.Shield.Item.Active.Value)
{
shield.Entity.Shield.TakeHit(DamageType, Damage);
shield.ShowHit(hit.point, sqrt(Damage));
LineRenderer.SetPosition(1, hit.point);
hitFound = true;
break;
}
}
var hull = hit.collider.GetComponent<HullCollider>();
if (hull && !(hull.Entity.Shield != null && hull.Entity.Shield.Item.Active.Value && hull.Entity.Shield.CanTakeHit(DamageType, Damage)))
{
if (hull.Entity != SourceEntity)
{
hull.SendHit(Damage * Time.deltaTime, Penetration, Spread, DamageType, SourceEntity, hit.textureCoord, transform.forward);
LineRenderer.SetPosition(1, hit.point);
hit.Shield?.ShowHit(hit.Point, Mathf.Sqrt(Damage));
LineRenderer.SetPosition(1, hit.Point);
hitFound = true;
break;
}
LineRenderer.SetPosition(1, hit.Point);
hitFound = true;
break;
}
}
if(!hitFound)
LineRenderer.SetPosition(1, transform.position + transform.forward * Range);
}

View file

@ -1,7 +1,4 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Unity.Mathematics.math;
public class ConstantLightning : MonoBehaviour
{
@ -51,28 +48,34 @@ public class ConstantLightning : MonoBehaviour
}
Lightning.FixedEndpoint = false;
var hits = Physics.SphereCastAll(Barrel.position, HitRadius, Barrel.forward, Range, 1 | (1 << 17));
foreach (var hit in hits)
if (AetheriaYmirPhysicsBridge.Instance.TryCastZoneHulls(
Source?.ZoneRenderer,
Source?.Entity,
Barrel.position,
Barrel.forward,
Range,
HitRadius,
out var hits))
{
var shield = hit.collider.GetComponent<ShieldManager>();
if (shield && (shield.Entity.Shield != null && shield.Entity.Shield.Item.Active.Value && shield.Entity.Shield.CanTakeHit(DamageType, Damage)))
foreach (var hit in hits)
{
if (shield.Entity == Source.Entity) continue;
shield.Entity.Shield.TakeHit(DamageType, Damage * Time.deltaTime);
shield.ShowHit(hit.point, sqrt(Damage));
}
var hull = hit.collider.GetComponent<HullCollider>();
if (hull && !(hull.Entity.Shield != null && hull.Entity.Shield.Item.Active.Value && hull.Entity.Shield.CanTakeHit(DamageType, Damage)))
{
if (hull.Entity == Source.Entity) continue;
hull.SendHit(Damage * Time.deltaTime, Penetration, Spread, DamageType, Source.Entity, hit.textureCoord, Barrel.forward);
}
var hull = hit.Hull;
var entity = hull.Entity;
if (entity.Shield != null && entity.Shield.Item.Active.Value)
{
hit.Shield?.ShowHit(hit.Point, Mathf.Sqrt(Damage));
}
else
{
}
Lightning.FixedEndpoint = true;
Lightning.EndPosition = hit.point;
Lightning.FixedEndpoint = true;
Lightning.EndPosition = hit.Point;
break;
break;
}
}
if(!Lightning.FixedEndpoint)
Lightning.EndPosition = Barrel.position + Barrel.forward * Range;

View file

@ -1,32 +1,26 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class ConstantParticleWeapon : MonoBehaviour
{
public ParticleSystem[] Particles;
public float CastRadius = 0.25f;
public float Damage { get; set; }
public float Range { get; set; }
public DamageType DamageType { get; set; }
public EntityInstance Source { get; set; }
public EntityInstance Target { get; set; }
private bool _stopping;
private float _emission;
private List<ParticleSystem.Particle> _collisionParticles = new List<ParticleSystem.Particle>();
private HullCollider _hull;
private void Start()
{
_emission = Particles[0].emission.rateOverTime.constant;
}
private void OnEnable()
{
foreach(var p in Particles)
p.enableEmission = true;
{
var emission = p.emission;
emission.enabled = true;
}
_stopping = false;
}
@ -38,18 +32,6 @@ public class ConstantParticleWeapon : MonoBehaviour
main.simulationSpace = ParticleSystemSimulationSpace.Custom;
main.customSimulationSpace = Source.LocalSpace;
}
var trigger = Particles[0].trigger;
while(trigger.colliderCount > 0)
trigger.RemoveCollider(0);
if (Target == null) return;
foreach (var collider in Target.HullColliders)
{
_hull = collider;
trigger.AddCollider(collider.GetComponent<Collider>());
}
}
private void Update()
@ -60,27 +42,32 @@ public class ConstantParticleWeapon : MonoBehaviour
GetComponent<Prototype>().ReturnToPool();
return;
}
}
private void OnParticleTrigger()
{
if (Target == null || !_hull) return;
_collisionParticles.Clear();
Particles[0].GetTriggerParticles(ParticleSystemTriggerEventType.Enter, _collisionParticles);
if(_collisionParticles.Count > 0)
if (_stopping || Target == null || Range <= 0)
return;
if (AetheriaYmirPhysicsBridge.Instance.TryCastTargetHulls(
Target,
transform.position,
transform.forward,
Range,
CastRadius,
out var hits))
{
_hull.SendSplash(
Damage * Time.deltaTime,
DamageType,
Source.Entity,
(transform.position - Target.transform.position).normalized);
foreach (var hit in hits)
{
break;
}
}
}
public void Stop()
{
foreach(var p in Particles)
p.enableEmission = false;
{
var emission = p.emission;
emission.enabled = false;
}
_stopping = true;
}
}

View file

@ -21,6 +21,7 @@ public class ConstantParticleWeaponManager : ConstantWeaponEffectManager
t.position = barrel.position;
p.Damage = weapon.Damage;
p.DamageType = weapon.DamageType;
p.Range = weapon.Range;
_weapons.Add(item, p);
p.Initialize();
}

View file

@ -1,13 +1,8 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Mathematics;
using static Unity.Mathematics.math;
using static Unity.Mathematics.noise;
using Random = UnityEngine.Random;
using static Noise1D;
using float3 = Unity.Mathematics.float3;
public class GuidedProjectile : MonoBehaviour
{
@ -36,7 +31,7 @@ public class GuidedProjectile : MonoBehaviour
private Vector3 _previousTargetPosition;
public Transform Target { get; set; }
public float3 StartPosition { get; set; }
public Vector3 StartPosition { get; set; }
public float Range { get; set; }
public Vector3 Velocity { get; set; }
public float Damage { get; set; }
@ -52,9 +47,7 @@ public class GuidedProjectile : MonoBehaviour
_active = _alive = true;
_phase = Random.value * 100;
_prevDist = Single.MaxValue;
Particles.startColor = Color.white;
//var main = Particles.main;
//main.startColor = new ParticleSystem.MinMaxGradient { mode = ParticleSystemGradientMode.Color, color = Color.white };
SetParticleColor(Color.white);
Particles.Clear(true);
Particles.Play(true);
}
@ -72,18 +65,18 @@ public class GuidedProjectile : MonoBehaviour
StartCoroutine(FadeOut());
return;
}
var position = (float3) t.position;
var position = t.position;
var targetPosition = TargetPosition?.Invoke() ?? Target.position;
_targetVelocity = lerp(_targetVelocity, targetPosition - _previousTargetPosition, saturate(Time.deltaTime * 5));
_targetVelocity = Vector3.Lerp(_targetVelocity, targetPosition - _previousTargetPosition, Mathf.Clamp01(Time.deltaTime * 5));
_previousTargetPosition = targetPosition;
targetPosition = AetheriaMath.FirstOrderIntercept(position,float3.zero, TopSpeed, targetPosition, _targetVelocity);
targetPosition = AetheriaMath.FirstOrderIntercept(position, Vector3.zero, TopSpeed, targetPosition, _targetVelocity);
var diff = targetPosition - transform.position;
var targetDist = diff.magnitude;
var sourceDist = length(StartPosition.xz - position.xz);
var sourceDist = Vector2.Distance(new Vector2(StartPosition.x, StartPosition.z), new Vector2(position.x, position.z));
if (sourceDist > Range || dot(diff,Velocity) < 0)
if (sourceDist > Range || Vector3.Dot(diff, Velocity) < 0)
{
StartCoroutine(FadeOut());
if (HitEffect != null)
@ -98,8 +91,8 @@ public class GuidedProjectile : MonoBehaviour
var targetDistFlat = diff.Flatland().magnitude;
var curveLerp = 1 - targetDistFlat / (sourceDist + targetDistFlat);
var dir = diff.normalized;
var right = cross(dir, float3(0, 1, 0));
var up = cross(dir, right);
var right = Vector3.Cross(dir, Vector3.up);
var up = Vector3.Cross(dir, right);
if (Children > 0 && SplitTime < curveLerp)
{
@ -108,9 +101,9 @@ public class GuidedProjectile : MonoBehaviour
var child = ChildProjectile.Instantiate<GuidedProjectile>();
child.transform.position = t.position;
child.StartPosition = StartPosition;
var randomDirection = normalize(Random.insideUnitCircle);
var randomDirection = Random.insideUnitCircle.normalized;
var perpendicularRandom = randomDirection.x * right + randomDirection.y * up;
child.Velocity = normalize(lerp(perpendicularRandom, dir, SplitSeparationForwardness)) * length(Velocity) * SplitSeparationVelocity;
child.Velocity = Vector3.Lerp(perpendicularRandom, dir, SplitSeparationForwardness).normalized * Velocity.magnitude * SplitSeparationVelocity;
child.Range = Range;
child.Damage = Damage / Children;
child.Penetration = Penetration;
@ -135,56 +128,53 @@ public class GuidedProjectile : MonoBehaviour
}
}
var dodge = normalize(lerp(
normalize(right * noise(Time.time * Frequency + _phase) + up * noise(Time.time * Frequency + (100 + _phase))),
Vector3.up, LiftCurve.Evaluate(curveLerp)));
var dodge = Vector3.Lerp(
(right * noise(Time.time * Frequency + _phase) + up * noise(Time.time * Frequency + (100 + _phase))).normalized,
Vector3.up,
LiftCurve.Evaluate(curveLerp)).normalized;
var desired = Vector3.Slerp(dodge, dir, GuidanceCurve.Evaluate(curveLerp)).normalized * TopSpeed;
var thrustCurve = ThrustCurve.Evaluate(curveLerp);
var thrust = Thrust * thrustCurve;
var c = Color.white * thrustCurve;
c.a = 1;
Particles.startColor = c;
SetParticleColor(c);
Velocity += (desired-Velocity).normalized * (thrust * Time.deltaTime);
}
if(_alive)
{
var ray = new Ray(t.position, Velocity);
foreach (var hit in Physics.RaycastAll(ray, Velocity.magnitude * Time.deltaTime, 1 | (1 << 17)))
if (AetheriaYmirPhysicsBridge.Instance.TryCastZoneHulls(
ActionGameManager.Instance?.ZoneRenderer,
SourceEntity,
t.position,
Velocity,
Velocity.magnitude * Time.deltaTime,
0,
out var hits) &&
hits.Count > 0)
{
var shield = hit.collider.GetComponent<ShieldManager>();
if (shield)
var hit = hits[0];
var hull = hit.Hull;
var entity = hull.Entity;
if (entity.Shield != null && entity.Shield.Item.Active.Value)
{
if (!(shield.Entity.Shield != null && shield.Entity.Shield.Item.Active.Value && shield.Entity.Shield.CanTakeHit(DamageType, Damage))) continue;
if (shield.Entity != SourceEntity)
{
shield.Entity.Shield.TakeHit(DamageType, Damage);
shield.ShowHit(hit.point, sqrt(Damage));
}
hit.Shield?.ShowHit(hit.Point, Mathf.Sqrt(Damage));
}
var hull = hit.collider.GetComponent<HullCollider>();
if (hull && !(hull.Entity.Shield != null && hull.Entity.Shield.Item.Active.Value && hull.Entity.Shield.CanTakeHit(DamageType, Damage)))
else
{
if (hull.Entity != SourceEntity)
{
hull.SendHit(Damage, Penetration, Spread, DamageType, SourceEntity, hit.textureCoord, transform.forward);
transform.position = hit.point;
StartCoroutine(Kill());
}
}
else// if (hit.transform.gameObject.layer == 1)
{
StartCoroutine(Kill());
return;
}
transform.position = hit.Point;
StartCoroutine(Kill());
if (HitEffect != null)
{
var ht = HitEffect.Instantiate<Transform>();
ht.SetParent(hit.collider.transform);
ht.position = hit.point;
return;
ht.SetParent(hull.transform);
ht.position = hit.Point;
}
return;
}
t.position += Velocity * Time.deltaTime;
@ -200,15 +190,19 @@ public class GuidedProjectile : MonoBehaviour
var lerp = 1 - (Time.time - startTime) / FadeOutTime;
var c = Color.white * lerp;
c.a = 1;
Particles.startColor = c;
//var main = Particles.main;
//main.startColor = new ParticleSystem.MinMaxGradient { mode = ParticleSystemGradientMode.Color, color = Color.white * lerp };
SetParticleColor(c);
yield return null;
}
StartCoroutine(Kill());
}
private void SetParticleColor(Color color)
{
var main = Particles.main;
main.startColor = color;
}
IEnumerator Kill()
{
_active = false;
@ -223,4 +217,4 @@ public class GuidedProjectile : MonoBehaviour
OnKill?.Invoke();
GetComponent<Prototype>().ReturnToPool();
}
}
}

View file

@ -2,9 +2,8 @@
using System.Collections;
using System.Collections.Generic;
using UniRx;
using Unity.Mathematics;
using UnityEngine;
using static Unity.Mathematics.math;
using cfloat3 = CultMath.float3;
public class GuidedProjectileManager : InstantWeaponEffectManager
{
@ -64,7 +63,13 @@ public class GuidedProjectileManager : InstantWeaponEffectManager
p.Velocity = barrel.forward * weapon.Velocity;
p.Thrust = weapon.EvaluateGuidedProjectileThrust();
p.TopSpeed = weapon.EvaluateGuidedProjectileVelocity();
p.TargetPosition = () => source.Entity.Position + length( (float3)source.LookAtPoint.position - source.Entity.Position) * source.Entity.LookDirection;
p.TargetPosition = () =>
{
var lookPosition = source.LookAtPoint.position;
var lookPoint = new cfloat3(lookPosition.x, lookPosition.y, lookPosition.z);
var lookDistance = CultMath.math.length(lookPoint - source.Entity.CultPosition);
return (Vector3)AetheriaMath.ToUnity(source.Entity.CultPosition + lookDistance * source.Entity.CultLookDirection);
};
}
}
}

View file

@ -1,7 +1,4 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Unity.Mathematics.math;
public class HitscanEffect : MonoBehaviour
{
@ -27,45 +24,47 @@ public class HitscanEffect : MonoBehaviour
{
_startTime = Time.time;
var hitFound = false;
var ray = new Ray(transform.position, transform.forward);
foreach (var hit in Physics.RaycastAll(ray, Range, 1 | (1 << 17)))
if (AetheriaYmirPhysicsBridge.Instance.TryCastZoneHulls(
ActionGameManager.Instance?.ZoneRenderer,
SourceEntity,
transform.position,
transform.forward,
Range,
0,
out var hits))
{
var shield = hit.collider.GetComponent<ShieldManager>();
if (shield)
foreach (var hit in hits)
{
if (!(shield.Entity.Shield != null && shield.Entity.Shield.Item.Active.Value && shield.Entity.Shield.CanTakeHit(DamageType, Damage))) continue;
if (shield.Entity != SourceEntity)
var hull = hit.Hull;
var entity = hull.Entity;
if (entity.Shield != null && entity.Shield.Item.Active.Value)
{
shield.Entity.Shield.TakeHit(DamageType, Damage);
shield.ShowHit(hit.point, sqrt(Damage));
hit.Shield?.ShowHit(hit.Point, Mathf.Sqrt(Damage));
hitFound = true;
}
}
var hull = hit.collider.GetComponent<HullCollider>();
if (hull)
{
if (hull.Entity != SourceEntity)
else
{
hull.SendHit(Damage, Penetration, Spread, DamageType, SourceEntity, hit.textureCoord, transform.forward);
hitFound = true;
}
}
if (hitFound && HitEffect != null)
{
var ht = HitEffect.Instantiate<Transform>();
ht.SetParent(hit.collider.transform);
ht.position = hit.point;
if (hitFound && HitEffect != null)
{
var ht = HitEffect.Instantiate<Transform>();
ht.SetParent(hull.transform);
ht.position = hit.Point;
}
var length = (hit.Point - transform.position).magnitude;
Line.SetPosition(1, Vector3.forward * length);
var emission = LineEffect.emission;
emission.rateOverTimeMultiplier = length;
var shape = LineEffect.shape;
shape.position = Vector3.forward * (length / 2);
shape.scale = Vector3.one * (length / 2);
break;
}
var length = (hit.point - transform.position).magnitude;
Line.SetPosition(1, Vector3.forward * length);
var emission = LineEffect.emission;
emission.rateOverTimeMultiplier = length;
var shape = LineEffect.shape;
shape.position = Vector3.forward * (length / 2);
shape.scale = Vector3.one * (length / 2);
}
if(!hitFound)
{
Line.SetPosition(1, Vector3.forward * Range);

View file

@ -1,8 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Unity.Mathematics.math;
public class Laser : MonoBehaviour
{
@ -36,34 +34,34 @@ public class Laser : MonoBehaviour
}
LineRenderer.SetPosition(0, transform.position);
bool hitFound = false;
foreach (var hit in Physics.RaycastAll(new Ray(transform.position, transform.forward), Range, 1 | (1 << 17)))
var hitFound = false;
if (AetheriaYmirPhysicsBridge.Instance.TryCastZoneHulls(
ActionGameManager.Instance?.ZoneRenderer,
SourceEntity,
transform.position,
transform.forward,
Range,
0,
out var hits))
{
var shield = hit.collider.GetComponent<ShieldManager>();
if (shield)
foreach (var hit in hits)
{
if (!(shield.Entity.Shield != null && shield.Entity.Shield.Item.Active.Value && shield.Entity.Shield.CanTakeHit(DamageType, Damage))) continue;
if (shield.Entity != SourceEntity)
var hull = hit.Hull;
var entity = hull.Entity;
if (entity.Shield != null && entity.Shield.Item.Active.Value)
{
shield.Entity.Shield.TakeHit(DamageType, Damage);
shield.ShowHit(hit.point, sqrt(Damage));
LineRenderer.SetPosition(1, hit.point);
hitFound = true;
break;
}
}
var hull = hit.collider.GetComponent<HullCollider>();
if (hull && !(hull.Entity.Shield != null && hull.Entity.Shield.Item.Active.Value && hull.Entity.Shield.CanTakeHit(DamageType, Damage)))
{
if (hull.Entity != SourceEntity)
{
hull.SendHit(Damage * (Time.deltaTime / Duration), Penetration, Spread, DamageType, SourceEntity, hit.textureCoord, transform.forward);
LineRenderer.SetPosition(1, hit.point);
hit.Shield?.ShowHit(hit.Point, Mathf.Sqrt(Damage));
LineRenderer.SetPosition(1, hit.Point);
hitFound = true;
break;
}
LineRenderer.SetPosition(1, hit.Point);
hitFound = true;
break;
}
}
if(!hitFound)
LineRenderer.SetPosition(1, transform.position + transform.forward * Range);

View file

@ -1,12 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Mathematics;
using static Unity.Mathematics.math;
using static Unity.Mathematics.noise;
using Random = UnityEngine.Random;
using static Unity.Mathematics.math;
public class Lightning : MonoBehaviour
{
@ -32,37 +25,44 @@ public class Lightning : MonoBehaviour
_colliderHit = false;
LightningCompute.OnLeaderComplete = null;
LightningCompute.FixedEndpoint = false;
var hits = Physics.SphereCastAll(Barrel.position, HitRadius, Barrel.forward, Range, 1 | (1 << 17));
foreach (var hit in hits)
_endpoint = Barrel.position + Barrel.forward * Range;
if (AetheriaYmirPhysicsBridge.Instance.TryCastZoneHulls(
Source?.ZoneRenderer,
Source?.Entity,
Barrel.position,
Barrel.forward,
Range,
HitRadius,
out var hits))
{
var shield = hit.collider.GetComponent<ShieldManager>();
if (shield && (shield.Entity.Shield != null && shield.Entity.Shield.Item.Active.Value && shield.Entity.Shield.CanTakeHit(DamageType, Damage)))
foreach (var hit in hits)
{
if (shield.Entity == Source.Entity) continue;
LightningCompute.OnLeaderComplete = () =>
var hull = hit.Hull;
var entity = hull.Entity;
if (entity.Shield != null && entity.Shield.Item.Active.Value)
{
shield.Entity.Shield.TakeHit(DamageType, Damage);
shield.ShowHit(hit.point, sqrt(Damage));
};
}
var hull = hit.collider.GetComponent<HullCollider>();
if (hull && !(hull.Entity.Shield != null && hull.Entity.Shield.Item.Active.Value && hull.Entity.Shield.CanTakeHit(DamageType, Damage)))
{
if (hull.Entity == Source.Entity) continue;
LightningCompute.OnLeaderComplete = () =>
hull.SendHit(Damage, Penetration, Spread, DamageType, Source.Entity, hit.textureCoord, Barrel.forward);
}
LightningCompute.OnLeaderComplete = () =>
{
hit.Shield?.ShowHit(hit.Point, Mathf.Sqrt(Damage));
};
}
else
{
LightningCompute.OnLeaderComplete = null;
}
_colliderHit = true;
_colliderLocalPosition = hit.collider.transform.InverseTransformPoint(hit.point);
_colliderTransform = hit.collider.transform;
LightningCompute.FixedEndpoint = true;
break;
_colliderHit = true;
_colliderLocalPosition = hull.transform.InverseTransformPoint(hit.Point);
_colliderTransform = hull.transform;
LightningCompute.FixedEndpoint = true;
_endpoint = hit.Point;
break;
}
}
LightningCompute.OnPulseComplete = () =>
GetComponent<Prototype>().ReturnToPool();
LightningCompute.StartAnimation();
_endpoint = Barrel.position + Barrel.forward * Range;
}
private void Update()

View file

@ -1,8 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Mathematics;
using static Unity.Mathematics.math;
using UnityEngine;
using Random = UnityEngine.Random;
@ -51,8 +49,6 @@ public class Mine : MonoBehaviour
// Update is called once per frame
void Update()
{
var position = transform.position;
if (Time.time - _startTime > ActivationDelay && !_active)
{
_active = true;
@ -60,25 +56,11 @@ public class Mine : MonoBehaviour
if(_active)
{
_emission = lerp(_emission, _blastCountdown ? ArmedEmission : ActiveEmission, Time.deltaTime * 10);
_emission = Mathf.Lerp(_emission, _blastCountdown ? ArmedEmission : ActiveEmission, Time.deltaTime * 10);
_pulseLerp += Time.deltaTime / (_blastCountdown ? ArmedCycleDuration : ActiveCycleDuration);
_pulseLerp %= 1;
if(!_blastCountdown)
{
foreach (var collider in Physics.OverlapSphere(position, BlastRange, 1))
{
var hull = collider.GetComponent<HullCollider>();
if (hull)
{
_blastCountdown = true;
_blastTime = Time.time + BlastDelay;
}
}
}
if ((position - Source.transform.position).magnitude > Range ||
Time.time - _startTime > Lifetime ||
_blastCountdown && Time.time > _blastTime)
if (Time.time - _startTime > Lifetime)
{
Explode();
}
@ -89,21 +71,6 @@ public class Mine : MonoBehaviour
public void Explode()
{
var position = transform.position;
foreach (var collider in Physics.OverlapSphere(position, BlastRange, 1 | (1 << 17)))
{
var shield = collider.GetComponent<ShieldManager>();
if (shield && (shield.Entity.Shield != null && shield.Entity.Shield.Item.Active.Value && shield.Entity.Shield.CanTakeHit(DamageType, Damage)))
{
shield.Entity.Shield.TakeHit(DamageType, Damage);
shield.ShowHit(position, sqrt(Damage));
}
var hull = collider.GetComponent<HullCollider>();
if (hull && !(hull.Entity.Shield != null && hull.Entity.Shield.Item.Active.Value && hull.Entity.Shield.CanTakeHit(DamageType, Damage)))
{
hull.SendSplash(Damage, DamageType, Source.Entity, (collider.transform.position - position).normalized);
}
}
var ht = HitEffect.Instantiate<Transform>();
ht.position = position;
GetComponent<Prototype>().ReturnToPool();

View file

@ -1,14 +1,11 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Mathematics;
using static Unity.Mathematics.math;
using Random = UnityEngine.Random;
public class MineManager : InstantWeaponEffectManager
{
public Prototype ProjectilePrototype;
public bool InheritVelocity;
public override void Fire(InstantWeapon weapon, EquippedItem item, EntityInstance source, EntityInstance target)
{
@ -21,11 +18,9 @@ public class MineManager : InstantWeaponEffectManager
p.GridObject.Velocity = Quaternion.Euler(
Random.Range(-angle, angle),
Random.Range(-angle, angle),
Random.Range(-angle, angle)) *
Random.Range(-angle, angle)) *
barrel.forward *
weapon.Velocity;
if(InheritVelocity)
p.GridObject.Velocity += new Vector3(source.Entity.Velocity.x, 0, source.Entity.Velocity.y);
p.Damage = weapon.Damage;
p.Range = weapon.Range;
p.DamageType = weapon.DamageType;

View file

@ -1,6 +1,4 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ParticleChargeEffect : WeaponChargeEffect
@ -21,7 +19,7 @@ public class ParticleChargeEffect : WeaponChargeEffect
OverchargeEffect.Clear(true);
FailureEffect.Clear(true);
ChargeEffect.Play(true);
ChargeEffect.enableEmission = true;
SetEmission(ChargeEffect, true);
_overloaded = false;
_charge = 0;
@ -30,23 +28,23 @@ public class ParticleChargeEffect : WeaponChargeEffect
public override void StopCharging()
{
if (_overloaded)
OverchargeEffect.enableEmission = false;
SetEmission(OverchargeEffect, false);
else
ChargeEffect.enableEmission = false;
SetEmission(ChargeEffect, false);
StartCoroutine(Kill());
}
public override void Charged()
{
ChargeEffect.enableEmission = false;
SetEmission(ChargeEffect, false);
OverchargeEffect.Play(true);
OverchargeEffect.enableEmission = true;
SetEmission(OverchargeEffect, true);
_overloaded = true;
}
public override void Failed()
{
OverchargeEffect.enableEmission = false;
SetEmission(OverchargeEffect, false);
FailureEffect.Play(true);
}
@ -57,11 +55,18 @@ public class ParticleChargeEffect : WeaponChargeEffect
if (!_overloaded)
{
ChargeEffect.playbackSpeed = _charge;
var main = ChargeEffect.main;
main.simulationSpeed = _charge;
}
}
IEnumerator Kill()
private static void SetEmission(ParticleSystem particleSystem, bool enabled)
{
var emission = particleSystem.emission;
emission.enabled = enabled;
}
private IEnumerator Kill()
{
while (ChargeEffect.particleCount > 0 || OverchargeEffect.particleCount > 0 || FailureEffect.particleCount > 0)
{

View file

@ -1,8 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Mathematics;
using static Unity.Mathematics.math;
using UnityEngine;
using Random = UnityEngine.Random;
@ -28,7 +26,9 @@ public class Projectile : MonoBehaviour
public float Spread { get; set; }
public DamageType DamageType { get; set; }
public Entity SourceEntity { get; set; }
public EntityInstance TargetInstance { get; set; }
public float Range { get; set; }
public float YmirWorldTime { get; private set; }
// Start is called before the first frame update
void Start()
@ -51,68 +51,73 @@ public class Projectile : MonoBehaviour
var t = transform;
var position = t.position;
Velocity -= Vector3.up * (Gravity * Time.deltaTime);
Velocity *= max(0, 1 - Drag * Time.deltaTime);
Velocity *= Mathf.Max(0, 1 - Drag * Time.deltaTime);
var forward = Velocity.normalized;
t.forward = forward;
var ray = new Ray(position, Velocity);
foreach (var hit in Physics.RaycastAll(ray, Velocity.magnitude * Time.deltaTime, 1 | (1 << 17)))
if (!AetheriaYmirPhysicsBridge.Instance.TryStepProjectile(this, Time.deltaTime, out var ymirStep))
{
var shield = hit.collider.GetComponent<ShieldManager>();
var hull = hit.collider.GetComponent<HullCollider>();
if (shield)
{
if (!(shield.Entity.Shield != null && shield.Entity.Shield.Item.Active.Value && shield.Entity.Shield.CanTakeHit(DamageType, Damage))) continue;
if (shield.Entity != SourceEntity)
{
shield.Entity.Shield.TakeHit(DamageType, Damage*DirectHitDamageMultiplier);
shield.ShowHit(hit.point, sqrt(Damage * DirectHitDamageMultiplier));
}
}
else if (hull && !(hull.Entity.Shield != null && hull.Entity.Shield.Item.Active.Value && hull.Entity.Shield.CanTakeHit(DamageType, Damage)))
{
if (hull.Entity != SourceEntity)
{
hull.SendHit(Damage*DirectHitDamageMultiplier, Penetration, Spread, DamageType, SourceEntity, hit.textureCoord, forward);
transform.position = hit.point;
StartCoroutine(Kill());
}
}
else
{
StartCoroutine(Kill());
return;
}
if (HitEffect != null)
{
var ht = HitEffect.Instantiate<Transform>();
ht.SetParent(hit.collider.transform);
ht.position = hit.point;
return;
}
Debug.LogWarning("Ymir projectile step unavailable; projectile killed instead of falling back to Unity physics.");
StartCoroutine(Kill());
return;
}
transform.position += Velocity * Time.deltaTime;
YmirWorldTime += Time.deltaTime;
Velocity = ymirStep.Velocity;
t.position = ymirStep.Position;
if (ymirStep.HasHit)
{
ApplyYmirHit(ymirStep, forward);
return;
}
var distanceTraveled = (transform.position - StartPosition).magnitude;
if(distanceTraveled > Range)
StartCoroutine(Kill());
if (AirburstRange > 1 && distanceTraveled > AirburstDistance)
{
StartCoroutine(Kill());
var ht = HitEffect.Instantiate<Transform>();
ht.position = t.position;
foreach (var collider in Physics.OverlapSphere(t.position, AirburstRange, 1))
if (HitEffect != null)
{
var hull = collider.GetComponent<HullCollider>();
if (hull)
var ht = HitEffect.Instantiate<Transform>();
ht.position = t.position;
}
if (AetheriaYmirPhysicsBridge.Instance.TryOverlapTargetHulls(TargetInstance, t.position, AirburstRange, out var hits))
{
foreach (var hit in hits)
{
hull.SendSplash(Damage, DamageType, SourceEntity, (collider.transform.position - t.position).normalized);
var direction = (hit.Hull.transform.position - t.position).normalized;
}
}
}
}
}
private void ApplyYmirHit(AetheriaYmirProjectileStep step, Vector3 forward)
{
var hull = step.Hit.Hull;
if (hull == null || hull.Entity == SourceEntity)
return;
if (hull.Entity.Shield != null &&
hull.Entity.Shield.Item.Active.Value)
{
}
else
{
}
transform.position = step.Hit.Point;
if (HitEffect != null)
{
var ht = HitEffect.Instantiate<Transform>();
ht.SetParent(hull.transform);
ht.position = step.Hit.Point;
}
StartCoroutine(Kill());
}
IEnumerator Kill()
{
_alive = false;

View file

@ -1,8 +1,6 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Mathematics;
using static Unity.Mathematics.math;
using Random = UnityEngine.Random;
public class ProjectileManager : InstantWeaponEffectManager
@ -17,6 +15,7 @@ public class ProjectileManager : InstantWeaponEffectManager
var barrel = source.GetBarrel(hp);
var angle = weapon.Spread / 2;
p.SourceEntity = source.Entity;
p.TargetInstance = target;
p.Velocity = Quaternion.Euler(
Random.Range(-angle, angle),
Random.Range(-angle, angle),
@ -25,14 +24,16 @@ public class ProjectileManager : InstantWeaponEffectManager
weapon.Velocity;
p.StartPosition = p.transform.position = barrel.position + p.Velocity * (Random.value * Time.deltaTime);
if(InheritVelocity)
p.Velocity += new Vector3(source.Entity.Velocity.x, 0, source.Entity.Velocity.y);
p.Velocity += (Vector3)AetheriaMath.ToUnityXZ(source.Entity.CultVelocity);
p.Damage = weapon.Damage;
p.Range = weapon.Range;
p.Penetration = weapon.Penetration;
p.Spread = weapon.DamageSpread;
p.DamageType = weapon.DamageType;
p.Zone = source.Entity.Zone;
p.AirburstDistance = target != null ? length(source.Entity.Position - target.Entity.Position) : (weapon.Range * .75f);
p.AirburstDistance = target != null
? CultMath.math.length(source.Entity.CultPosition - target.Entity.CultPosition)
: (weapon.Range * .75f);
p.Trail.Clear();
}
}

View file

@ -24,7 +24,7 @@ public class Gravity : MonoBehaviour
if (_instance != null)
return _instance;
_instance = (Gravity)FindObjectOfType(typeof(Gravity));
_instance = FindAnyObjectByType<Gravity>();
return _instance;
}
}
@ -47,7 +47,6 @@ public class Gravity : MonoBehaviour
if (Instance.ForceMode == GravityForceMode.Sign)
{
var hits = Instance.GravityObjects.Where(go => (go.transform.position.Flatland() - position).sqrMagnitude < go.transform.lossyScale.x)
// var hits = Physics.RaycastAll(new Ray(new Vector3(position.x, 10, position.y), Vector3.down), 20, LayerMask.GetMask("Displacement"))
.Where(h => !h.CompareTag("IgnoreGravity"));
Vector2 force = Vector2.zero;
@ -130,4 +129,4 @@ public enum GravityForceMode
{
Sign,
Gradient
}
}

View file

@ -2,6 +2,7 @@
"name": "Aetheria.Shared.Unity",
"rootNamespace": "",
"references": [
"CultMath",
"Unity.Mathematics",
"UniRx",
"Ink",

View file

@ -1,13 +1,78 @@
using System.Collections;
using System.Collections.Generic;
using Unity.Mathematics;
using static Unity.Mathematics.math;
using CultMath;
using static CultMath.math;
using double2 = CultMath.double2;
using double3 = CultMath.double3;
using float2 = CultMath.float2;
using float3 = CultMath.float3;
using unityfloat2 = Unity.Mathematics.float2;
using unityfloat3 = Unity.Mathematics.float3;
public static class AetheriaMath
{
public const float Deg2Rad = 0.01745329f;
public const float Rad2Deg = 57.29578f;
public static float2 ToCult(unityfloat2 value) => float2(value.x, value.y);
public static float3 ToCult(unityfloat3 value) => float3(value.x, value.y, value.z);
public static float2 ToCultXZ(unityfloat3 value) => float2(value.x, value.z);
public static unityfloat2 ToUnity(float2 value) => new(value.x, value.y);
public static unityfloat3 ToUnity(float3 value) => new(value.x, value.y, value.z);
public static unityfloat3 ToUnityXZ(float2 value) => new(value.x, 0, value.y);
public static float AngleDegrees(float2 from, float2 to)
{
return AngleRadians(from, to) * Rad2Deg;
}
public static float AngleDegrees(float3 from, float3 to)
{
return AngleRadians(from, to) * Rad2Deg;
}
public static float AngleDegrees(unityfloat2 from, unityfloat2 to)
{
return AngleDegrees(ToCult(from), ToCult(to));
}
public static float AngleDegrees(unityfloat3 from, unityfloat3 to)
{
return AngleDegrees(ToCult(from), ToCult(to));
}
public static float AngleRadians(float2 from, float2 to)
{
var denom = sqrt(lengthsq(from) * lengthsq(to));
if (denom <= 0.000001f)
{
return 0;
}
return acos(clamp(dot(from, to) / denom, -1.0f, 1.0f));
}
public static float AngleRadians(float3 from, float3 to)
{
var denom = sqrt(lengthsq(from) * lengthsq(to));
if (denom <= 0.000001f)
{
return 0;
}
return acos(clamp(dot(from, to) / denom, -1.0f, 1.0f));
}
public static float AngleRadians(unityfloat2 from, unityfloat2 to)
{
return AngleRadians(ToCult(from), ToCult(to));
}
public static float AngleRadians(unityfloat3 from, unityfloat3 to)
{
return AngleRadians(ToCult(from), ToCult(to));
}
public static float Decay(float source, float lambda, float dt)
{
return source * exp(-lambda * dt);
@ -18,10 +83,20 @@ public static class AetheriaMath
return source * exp(-lambda * dt);
}
public static unityfloat3 Decay(unityfloat3 source, float lambda, float dt)
{
return ToUnity(Decay(ToCult(source), lambda, dt));
}
public static float3 Decay(float3 source, float3 lambda, float dt)
{
return source * exp(-lambda * dt);
}
public static unityfloat3 Decay(unityfloat3 source, unityfloat3 lambda, float dt)
{
return ToUnity(Decay(ToCult(source), ToCult(lambda), dt));
}
public static float Damp(float a, float b, float lambda, float dt)
{
@ -32,11 +107,21 @@ public static class AetheriaMath
{
return lerp(a, b, 1 - exp(-lambda * dt));
}
public static unityfloat2 Damp(unityfloat2 a, unityfloat2 b, float lambda, float dt)
{
return ToUnity(Damp(ToCult(a), ToCult(b), lambda, dt));
}
public static float3 Damp(float3 a, float3 b, float lambda, float dt)
{
return lerp(a, b, 1 - exp(-lambda * dt));
}
public static unityfloat3 Damp(unityfloat3 a, unityfloat3 b, float lambda, float dt)
{
return ToUnity(Damp(ToCult(a), ToCult(b), lambda, dt));
}
public static double Damp(double a, double b, double lambda, double dt)
{
@ -72,6 +157,22 @@ public static class AetheriaMath
);
return targetPosition + t*targetRelativeVelocity;
}
public static unityfloat3 FirstOrderIntercept
(
unityfloat3 shooterPosition,
unityfloat3 shooterVelocity,
float shotSpeed,
unityfloat3 targetPosition,
unityfloat3 targetVelocity
) {
return ToUnity(FirstOrderIntercept(
ToCult(shooterPosition),
ToCult(shooterVelocity),
shotSpeed,
ToCult(targetPosition),
ToCult(targetVelocity)));
}
//first-order intercept using relative target position
public static float FirstOrderInterceptTime
@ -112,6 +213,15 @@ public static class AetheriaMath
else //determinant = 0; one intercept path, pretty much never happens
return max(-b/(2f*a), 0f); //don't shoot back in time
}
public static float FirstOrderInterceptTime
(
float shotSpeed,
unityfloat3 targetRelativePosition,
unityfloat3 targetRelativeVelocity
) {
return FirstOrderInterceptTime(shotSpeed, ToCult(targetRelativePosition), ToCult(targetRelativeVelocity));
}
// http://csharphelper.com/blog/2016/09/find-the-shortest-distance-between-a-point-and-a-line-segment-in-c/
// Calculate the distance between
@ -240,4 +350,38 @@ public static class AetheriaMath
x = saturate(x);
return x * x * x * (x * (x * 6 - 15) + 10);
}
public static float2 Rotate(float2 value, float radians)
{
var s = (float)System.Math.Sin(radians);
var c = (float)System.Math.Cos(radians);
return float2(value.x * c - value.y * s, value.x * s + value.y * c);
}
public static float2 Rotate(float2 value, ItemRotation rotation)
{
switch (rotation)
{
case ItemRotation.None:
return value;
case ItemRotation.CounterClockwise:
return float2(-value.y, value.x);
case ItemRotation.Reversed:
return float2(-value.x, -value.y);
case ItemRotation.Clockwise:
return float2(value.y, -value.x);
default:
throw new System.ArgumentOutOfRangeException(nameof(rotation), rotation, null);
}
}
public static unityfloat2 Rotate(unityfloat2 value, float radians)
{
return ToUnity(Rotate(ToCult(value), radians));
}
public static unityfloat2 Rotate(unityfloat2 value, ItemRotation rotation)
{
return ToUnity(Rotate(ToCult(value), rotation));
}
}

View file

@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
using Unity.Mathematics;
using cfloat3 = CultMath.float3;
public interface INamedEntry
{
@ -26,5 +26,5 @@ public class AetheriaRuntimeItemReference
public interface ITintInspector
{
float3 TintColor { get; }
cfloat3 TintColor { get; }
}

View file

@ -7,9 +7,8 @@ using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UniRx;
using Unity.Mathematics;
using static Unity.Mathematics.math;
using float2 = Unity.Mathematics.float2;
using static CultMath.math;
using cfloat2 = CultMath.float2;
public class Agent
{
@ -55,26 +54,27 @@ public class Agent
}
}
public void Accelerate(float2 targetVelocity, bool noTurn = false)
public void Accelerate(cfloat2 targetVelocity, bool noTurn = false)
{
var deltaV = targetVelocity - Ship.Velocity;
var deltaV = targetVelocity - Ship.CultVelocity;
var deltaVMag = length(deltaV);
var deltaVDirection = normalize(deltaV);
// If Delta V is above the threshold, direct the ship towards the delta and use only main thrusters
if (!noTurn && deltaVMag > FORWARD_DELTA_THRESHOLD)
{
Ship.LookDirection = float3(deltaVDirection.x, 0, deltaVDirection.y);
Ship.MovementDirection = float2(0, pow(dot(Ship.Direction, deltaVDirection), 2));
Ship.CultLookDirectionXZ = deltaVDirection;
Ship.MovementDirection = new cfloat2(0, pow(dot(Ship.CultDirection, deltaVDirection), 2));
}
// If Delta V is low, direct the ship towards the target and use all thrusters
else if(deltaVMag > THRUST_DELTA_THRESHOLD)
{
var right = Ship.Direction.Rotate(ItemRotation.Clockwise);
Ship.MovementDirection = float2(dot(right, deltaVDirection), dot(Ship.Direction, deltaVDirection));
var shipDirection = Ship.CultDirection;
var right = AetheriaMath.Rotate(shipDirection, ItemRotation.Clockwise);
Ship.MovementDirection = new cfloat2(dot(right, deltaVDirection), dot(shipDirection, deltaVDirection));
}
else
{
Ship.MovementDirection = float2.zero;
Ship.MovementDirection = cfloat2.zero;
}
}
}

View file

@ -2,10 +2,8 @@ using System;
using System.Collections.Generic;
using System.Linq;
using UniRx;
using static Unity.Mathematics.math;
using float2 = Unity.Mathematics.float2;
using float3 = Unity.Mathematics.float3;
using float2x2 = Unity.Mathematics.float2x2;
using static CultMath.math;
using cfloat3 = CultMath.float3;
public class CombatState : BaseState
{
@ -13,7 +11,7 @@ public class CombatState : BaseState
private float _optimumRange;
private readonly List<(int index, float dps)> _availableGroups = new List<(int index, float dps)>();
private readonly List<LockWeapon> _availableLockingWeapons = new List<LockWeapon>();
public CombatState(Agent agent) : base(agent)
{
SampleDps();
@ -31,7 +29,9 @@ public class CombatState : BaseState
_availableGroups.Clear();
_availableLockingWeapons.Clear();
var toTarget = target.Position - _agent.Ship.Position;
var shipPosition = _agent.Ship.CultPosition;
var targetPosition = target.CultPosition;
var toTarget = targetPosition - shipPosition;
var targetDistance = length(toTarget);
for (var i = 0; i < _agent.Ship.WeaponGroups.Length; i++)
{
@ -66,16 +66,17 @@ public class CombatState : BaseState
}
}
var targetRight = target.Direction.Rotate(ItemRotation.Clockwise);
var targetDirection = target.CultDirection;
var targetRight = AetheriaMath.Rotate(targetDirection, ItemRotation.Clockwise);
var optimumRangeDelta = abs(_optimumRange - targetDistance);
var directionToTarget = normalize(toTarget.xz);
var targetPortAlignment = dot(targetRight, directionToTarget);
var targetForeAlignment = dot(-target.Direction, directionToTarget);
var targetForeAlignment = dot(-targetDirection, directionToTarget);
var forwardness = saturate(optimumRangeDelta / _agent.Settings.AgentMaxForwardDistance) * _agent.Settings.AgentForwardLerp;
if (targetForeAlignment > 0) forwardness = lerp(forwardness, 1, pow(targetPortAlignment, 2));
var movementDirection = normalize(lerp(
directionToTarget.Rotate(targetPortAlignment > 0 ? ItemRotation.Clockwise : ItemRotation.CounterClockwise),
AetheriaMath.Rotate(directionToTarget, targetPortAlignment > 0 ? ItemRotation.Clockwise : ItemRotation.CounterClockwise),
targetDistance > _optimumRange ? directionToTarget : -directionToTarget, forwardness));
if (selectedGroup >= 0)
@ -84,17 +85,16 @@ public class CombatState : BaseState
if(testWeapon.Velocity > 1)
{
var targetHull = _agent.ItemManager.GetRuntimeItem(target.Hull);
var targetVelocity = float3(target.Velocity.x, 0, target.Velocity.y);
var shipVelocity = float3(_agent.Ship.Velocity.x, 0, _agent.Ship.Velocity.y);
var targetVelocity = new cfloat3(target.CultVelocity.x, 0, target.CultVelocity.y);
var predictedPosition = AetheriaMath.FirstOrderIntercept(
_agent.Ship.Position,
float3.zero,
shipPosition,
cfloat3.zero,
testWeapon.Velocity,
target.Position,
targetPosition,
targetVelocity
);
predictedPosition.y = _agent.Ship.Zone.GetHeight(predictedPosition.xz) + (float)(targetHull?.HullGridOffset ?? 0);
toTarget = normalize(predictedPosition - _agent.Ship.Position);
toTarget = normalize(predictedPosition - shipPosition);
}
var shouldFire = dot(
@ -108,7 +108,7 @@ public class CombatState : BaseState
weapon.Deactivate();
}
}
_agent.Ship.LookDirection = toTarget;
_agent.Ship.CultLookDirection = toTarget;
_agent.Accelerate(movementDirection * _agent.TopSpeed, true);//selectedGroup >= 0);
// Fire charged guns!

View file

@ -1,10 +1,9 @@
using Unity.Mathematics;
using static Unity.Mathematics.math;
using float2 = Unity.Mathematics.float2;
using static CultMath.math;
using cfloat2 = CultMath.float2;
public abstract class MatchVelocityState : BaseState
{
protected abstract float2 TargetVelocity { get; }
protected abstract cfloat2 TargetVelocity { get; }
protected MatchVelocityState(Agent agent) : base(agent) { }
@ -13,12 +12,12 @@ public abstract class MatchVelocityState : BaseState
_agent.Accelerate(TargetVelocity);
}
public float2 MatchDistanceTime
public cfloat2 MatchDistanceTime
{
get
{
var velocity = length(_agent.Ship.Velocity);
var deltaV = TargetVelocity - _agent.Ship.Velocity;
var velocity = length(_agent.Ship.CultVelocity);
var deltaV = TargetVelocity - _agent.Ship.CultVelocity;
var stoppingTime = length(deltaV) / (_agent.Ship.ForwardThrust / _agent.Ship.Mass);
var stoppingDistance = stoppingTime * (velocity / 2);
@ -26,7 +25,7 @@ public abstract class MatchVelocityState : BaseState
var turnaroundTime = _agent.Ship.TurnTime(deltaV);
var turnaroundDistance = turnaroundTime * velocity;
return float2(stoppingDistance + turnaroundDistance, stoppingTime + turnaroundTime);
return new cfloat2(stoppingDistance + turnaroundDistance, stoppingTime + turnaroundTime);
}
}
}
}

View file

@ -1,13 +1,12 @@
using System;
using Unity.Mathematics;
using static Unity.Mathematics.math;
using float2 = Unity.Mathematics.float2;
using static CultMath.math;
using cfloat2 = CultMath.float2;
public abstract class MoveToState : BaseState
{
private VelocityLimit _velocityLimit;
protected abstract float2 TargetPosition { get; }
protected abstract cfloat2 TargetPosition { get; }
public float Distance { get; private set; }
protected MoveToState(Agent agent) : base(agent)
@ -16,13 +15,13 @@ public abstract class MoveToState : BaseState
public override void Update(float delta)
{
var diff = TargetPosition - _agent.Ship.Position.xz;
var diff = TargetPosition - _agent.Ship.CultPositionXZ;
var dir = normalize(diff);
Distance = length(diff);
// We want to go top speed in the direction of our target
var desiredVelocity = dir * _agent.TopSpeed;
_agent.Ship.LookDirection = float3(dir.x, 0, dir.y);
_agent.Ship.CultLookDirectionXZ = dir;
_agent.Accelerate(desiredVelocity);
}
}
@ -30,7 +29,7 @@ public abstract class MoveToState : BaseState
public class MoveToEntityState : MoveToState
{
public Entity TargetEntity { get; set; }
protected override float2 TargetPosition => TargetEntity?.Position.xz ?? float2.zero;
protected override cfloat2 TargetPosition => TargetEntity != null ? TargetEntity.CultPositionXZ : cfloat2.zero;
public MoveToEntityState(Agent agent) : base(agent) { }
}
@ -40,5 +39,5 @@ public class MoveToOrbitState : MoveToState
public string OrbitKey { get; set; } = "";
public MoveToOrbitState(Agent agent) : base(agent) { }
protected override float2 TargetPosition => _agent.Ship.Zone.GetOrbitPosition(OrbitKey);
protected override cfloat2 TargetPosition => AetheriaMath.ToCult(_agent.Ship.Zone.GetOrbitPosition(OrbitKey));
}

View file

@ -2,15 +2,16 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
using Unity.Mathematics;
using static Unity.Mathematics.math;
using static CultMath.math;
using cfloat2 = CultMath.float2;
using cfloat3 = CultMath.float3;
public class AetherDrive : Behavior
{
private readonly float3 _rotorDiameter;
private readonly float3 _rotorMass;
private readonly cfloat3 _rotorDiameter;
private readonly cfloat3 _rotorMass;
private readonly PerformanceStat _maximumRpm;
private readonly float3 _couplingLambda;
private readonly cfloat3 _couplingLambda;
private readonly PerformanceStat _lambdaMultiplier;
private readonly PerformanceStat _couplingEfficiency;
private readonly PerformanceStat _torque;
@ -19,15 +20,15 @@ public class AetherDrive : Behavior
private readonly PerformanceStat _passiveCoupling;
private readonly uint _rpmAudioParameter;
private readonly uint _torqueRatioAudioParameter;
private float3 _axis;
private cfloat3 _axis;
public float3 Thrust { get; private set; }
public float3 Rpm { get; private set; }
public cfloat3 Thrust { get; private set; }
public cfloat3 Rpm { get; private set; }
public float MaximumRpm { get; private set; }
public float2 ThrustDirection { get; private set; }
public cfloat2 ThrustDirection { get; private set; }
public string Particles { get; }
public float3 Axis
public cfloat3 Axis
{
get => _axis;
set => _axis = clamp(value, -1, 1);
@ -83,16 +84,17 @@ public class AetherDrive : Behavior
{
var rotorSpeed = Rpm * _rotorDiameter / 100;
var forward = normalize(Entity.Direction);
var right = forward.Rotate(ItemRotation.Clockwise);
var forward = normalize(Entity.CultDirection);
var right = AetheriaMath.Rotate(Entity.CultDirection, ItemRotation.Clockwise);
var velocity = Entity.CultVelocity;
var speed = float2(dot(Entity.Velocity, forward), dot(Entity.Velocity, right));
var speed = float2(dot(velocity, forward), dot(velocity, right));
var couplingEfficiency = Evaluate(_couplingEfficiency);
var efficiency = float3(saturate(1 - speed / max(rotorSpeed.xy, 1) * sign(_axis.xy)) * couplingEfficiency, 1);
Thrust = (Rpm - AetheriaMath.Decay(Rpm, _couplingLambda, dt)) * _rotorMass * efficiency;
var couplingLambda = _couplingLambda * Item.Evaluate(_lambdaMultiplier) * max(abs(_axis), Evaluate(_passiveCoupling));
var couplingLambda = _couplingLambda * Evaluate(_lambdaMultiplier) * max(abs(_axis), Evaluate(_passiveCoupling));
var previousRpm = Rpm;
Rpm = AetheriaMath.Decay(Rpm, couplingLambda, dt);
var rpmLoss = previousRpm - Rpm;
@ -102,13 +104,14 @@ public class AetherDrive : Behavior
AddHeat((heat.x + heat.y + heat.z)*ItemManager.GameplaySettings.AetherHeatMultiplier);
ThrustDirection = forward * (_axis.x * force.x / Entity.Mass) + right * (_axis.y * force.y / Entity.Mass);
Entity.Velocity += ThrustDirection;
Entity.CultVelocity += ThrustDirection;
Entity.Direction = mul(Entity.Direction,
Unity.Mathematics.float2x2.Rotate(force.z * _axis.z * ItemManager.GameplaySettings.AetherTorqueMultiplier / Entity.Mass));
Entity.CultDirection = AetheriaMath.Rotate(
Entity.CultDirection,
force.z * _axis.z * ItemManager.GameplaySettings.AetherTorqueMultiplier / Entity.Mass);
if(float.IsNaN(Entity.Velocity.x))
ItemManager.Log("FUCK FUCK FUCK FUCK");
if(float.IsNaN(Entity.CultVelocity.x))
ItemManager.Log("AetherDrive produced NaN velocity.");
MaximumRpm = Evaluate(_maximumRpm);
var torqueProfile = float3(
@ -136,11 +139,11 @@ public class AetherDrive : Behavior
}
public void RestoreRuntimeState(
float3 axis,
float3 thrust,
float3 rpm,
cfloat3 axis,
cfloat3 thrust,
cfloat3 rpm,
float maximumRpm,
float2 thrustDirection)
cfloat2 thrustDirection)
{
Axis = axis;
Thrust = thrust;

View file

@ -1,9 +1,3 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Mathematics;
using static Unity.Mathematics.math;
public class AutoWeapon : InstantWeapon
{

View file

@ -7,9 +7,9 @@ using System.Collections;
using System.Collections.Generic;
using System.Linq;
using GameCult.Aetheria.State.Unity;
using Unity.Mathematics;
using static Unity.Mathematics.math;
using static Unity.Mathematics.noise;
using static CultMath.math;
using cfloat3 = CultMath.float3;
using cfloat4 = CultMath.float4;
public abstract class Behavior
{
@ -23,7 +23,7 @@ public abstract class Behavior
protected Entity Entity => Item?.Entity ?? Consumable.Entity;
public float Temperature => Item?.Temperature ?? Consumable.Entity.MaxTemp;
public float3 Direction
public cfloat3 Direction
{
get
{
@ -36,12 +36,13 @@ public abstract class Behavior
}
else
{
var itemDirection = Entity.Direction.Rotate(Item.EquippableItem.Rotation);
return float3(itemDirection.x, 0, itemDirection.y);
var itemDirection = AetheriaMath.Rotate(Entity.CultDirection, Item.EquippableItem.Rotation);
return new cfloat3(itemDirection.x, 0, itemDirection.y);
}
}
return float3(Entity.Direction.x, 0, Entity.Direction.y);
var entityDirection = Entity.CultDirection;
return new cfloat3(entityDirection.x, 0, entityDirection.y);
}
}
@ -65,6 +66,18 @@ public abstract class Behavior
public float Evaluate(PerformanceStat stat) => Item?.Evaluate(stat) ?? Consumable.Evaluate(stat);
public float Evaluate(PerformanceStat stat, StatConditionMask condition, float value) =>
Item?.Evaluate(stat, condition, value) ?? Consumable.Evaluate(stat, condition, value);
public float Evaluate(
PerformanceStat stat,
StatConditionMask firstCondition,
float firstValue,
StatConditionMask secondCondition,
float secondValue) =>
Item?.Evaluate(stat, firstCondition, firstValue, secondCondition, secondValue) ??
Consumable.Evaluate(stat, firstCondition, firstValue, secondCondition, secondValue);
public bool TryGetPerformanceStat(string statName, out PerformanceStat stat)
{
if (!string.IsNullOrWhiteSpace(statName) &&
@ -82,6 +95,7 @@ public abstract class Behavior
if (!string.IsNullOrWhiteSpace(statName) && stat != null)
{
_performanceStats[statName] = stat;
Item?.RegisterPerformanceStat(Kind, statName, stat);
}
}
@ -208,7 +222,7 @@ public sealed class RuntimeBehaviorDefinition
return _fields.TryGetValue(key, out var value) ? checked((uint)value.NumberValue) : fallback;
}
public float3 Float3(int key, float3 fallback = default)
public cfloat3 Float3(int key, cfloat3 fallback = default)
{
return _fields.TryGetValue(key, out var value) ? ToFloat3(value) : fallback;
}
@ -228,7 +242,9 @@ public sealed class RuntimeBehaviorDefinition
public PerformanceStat PerformanceStat(int key, PerformanceStat fallback)
{
return _fields.TryGetValue(key, out var value) ? ToPerformanceStat(value) : fallback;
return _fields.TryGetValue(key, out var value)
? AetheriaRuntimeBehaviorValueReader.ReadPerformanceStat(value)
: fallback;
}
public BezierCurve BezierCurve(int key, BezierCurve fallback)
@ -238,15 +254,12 @@ public sealed class RuntimeBehaviorDefinition
return fallback;
}
return new BezierCurve
{
Keys = value.Children.Count > 0
? value.Children[0].Children.Select(ToFloat4).ToArray()
: Array.Empty<float4>()
};
return global::BezierCurve.FromKeys(value.Children.Count > 0
? value.Children[0].Children.Select(ToBezierKey)
: Enumerable.Empty<(float, float, float, float)>());
}
public float4[] Float4Array(int key, float4[] fallback)
public cfloat4[] Float4Array(int key, cfloat4[] fallback)
{
return _fields.TryGetValue(key, out var value)
? value.Children.Select(ToFloat4).ToArray()
@ -260,49 +273,29 @@ public sealed class RuntimeBehaviorDefinition
return fallback;
}
return new StatReference
{
Target = ChildString(value, 1),
Stat = ChildString(value, 2)
};
return AetheriaRuntimeBehaviorValueReader.ReadStatReference(value);
}
public T Enum<T>(int key, T fallback) where T : struct
{
if (!_fields.TryGetValue(key, out var value))
{
return fallback;
}
if (!string.IsNullOrWhiteSpace(value.StringValue) &&
System.Enum.TryParse(value.StringValue, true, out T parsed))
{
return parsed;
}
return (T)System.Enum.ToObject(typeof(T), checked((int)value.NumberValue));
return _fields.TryGetValue(key, out var value)
? AetheriaRuntimeBehaviorValueReader.ReadEnum(value, fallback)
: fallback;
}
private static PerformanceStat ToPerformanceStat(AetheriaRuntimeBehaviorValue value)
private static cfloat4 ToFloat4(AetheriaRuntimeBehaviorValue value)
{
return new PerformanceStat
{
Min = ChildFloat(value, 0),
Max = ChildFloat(value, 1),
HeatExponentMultiplier = ChildFloat(value, 2),
DurabilityExponentMultiplier = ChildFloat(value, 3),
QualityExponent = ChildFloat(value, 4)
};
return new cfloat4(ChildFloat(value, 0), ChildFloat(value, 1), ChildFloat(value, 2), ChildFloat(value, 3));
}
private static float4 ToFloat4(AetheriaRuntimeBehaviorValue value)
private static (float, float, float, float) ToBezierKey(AetheriaRuntimeBehaviorValue value)
{
return new float4(ChildFloat(value, 0), ChildFloat(value, 1), ChildFloat(value, 2), ChildFloat(value, 3));
return (ChildFloat(value, 0), ChildFloat(value, 1), ChildFloat(value, 2), ChildFloat(value, 3));
}
private static float3 ToFloat3(AetheriaRuntimeBehaviorValue value)
private static cfloat3 ToFloat3(AetheriaRuntimeBehaviorValue value)
{
return new float3(ChildFloat(value, 0), ChildFloat(value, 1), ChildFloat(value, 2));
return new cfloat3(ChildFloat(value, 0), ChildFloat(value, 1), ChildFloat(value, 2));
}
private static float ChildFloat(AetheriaRuntimeBehaviorValue value, int index)
@ -314,4 +307,9 @@ public sealed class RuntimeBehaviorDefinition
{
return value.Children.Count > index ? value.Children[index].StringValue ?? "" : "";
}
private static AetheriaRuntimeBehaviorValue ChildValue(AetheriaRuntimeBehaviorValue value, int index)
{
return value != null && value.Children.Count > index ? value.Children[index] : null;
}
}

View file

@ -2,9 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
using System.Collections.Generic;
using Unity.Mathematics;
using static Unity.Mathematics.math;
using static CultMath.math;
public class Capacitor : Behavior
{

View file

@ -1,8 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Mathematics;
using static Unity.Mathematics.math;
using static CultMath.math;
public class ChargedWeapon : InstantWeapon
{
@ -30,9 +29,9 @@ public class ChargedWeapon : InstantWeapon
public override float DamagePerSecond => Damage * _chargeFiringDamageMultiplier / (Cooldown + ChargeTime);
public override float RangeDamagePerSecond(float range)
{
return Damage *
_chargeFiringDamageMultiplier *
DamageCurve.Evaluate(saturate(unlerp(MinRange, Range, range))) /
return EvaluateRangeDamage(range, StatConditionMask.Charge, saturate(_charge)) *
lerp(1, _chargeFiringDamageMultiplier, saturate(_charge)) *
_chargeFiringDamageMultiplier /
(Cooldown + ChargeTime);
}
@ -106,7 +105,8 @@ public class ChargedWeapon : InstantWeapon
ChargeTime = Evaluate(_chargeTime);
ChargeEnergy = Evaluate(_chargeEnergy);
ChargeHeat = Evaluate(_chargeHeat);
Damage *= lerp(1, _chargeFiringDamageMultiplier, saturate(_charge));
Damage = EvaluateDamage(StatConditionMask.Charge, saturate(_charge)) *
lerp(1, _chargeFiringDamageMultiplier, saturate(_charge));
Heat *= lerp(1, _chargeFiringHeatMultiplier, saturate(_charge));
Spread *= lerp(1, _chargeFiringSpreadMultiplier, saturate(_charge));
BurstCount *= lerp(1, _chargeFiringBurstCountMultiplier, saturate(_charge));

View file

@ -2,10 +2,6 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
using System.Collections.Generic;
using Unity.Mathematics;
using static Unity.Mathematics.math;
public class Cockpit : Behavior
{
public Cockpit(RuntimeBehaviorDefinition definition, EquippedItem item) : base(definition, item)

View file

@ -3,8 +3,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
using System;
using Unity.Mathematics;
using static Unity.Mathematics.math;
using static CultMath.math;
public class ConstantWeapon : Weapon, IProgressBehavior, IEventBehavior
{
@ -30,7 +29,7 @@ public class ConstantWeapon : Weapon, IProgressBehavior, IEventBehavior
public override float DamagePerSecond => Damage;
public override float RangeDamagePerSecond(float range)
{
return Damage * DamageCurve.Evaluate(saturate(unlerp(MinRange, Range, range)));
return EvaluateRangeDamage(range);
}
public event Action OnReloadBegin;

View file

@ -2,10 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
using System;
using System.Linq;
using Unity.Mathematics;
using static Unity.Mathematics.math;
using static CultMath.math;
public class Cooldown : Behavior, IAlwaysUpdatedBehavior, IProgressBehavior
{

View file

@ -2,10 +2,6 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
using System.Collections.Generic;
using Unity.Mathematics;
using static Unity.Mathematics.math;
public class HeatStorage : Behavior
{
public HeatStorage(RuntimeBehaviorDefinition definition, EquippedItem item) : base(definition, item)

View file

@ -1,8 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Mathematics;
using static Unity.Mathematics.math;
using static CultMath.math;
public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior
{
@ -29,7 +28,7 @@ public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior
public override float DamagePerSecond => Damage / Cooldown;
public override float RangeDamagePerSecond(float range)
{
return Damage * DamageCurve.Evaluate(saturate(unlerp(MinRange, Range, range))) / Cooldown;
return EvaluateRangeDamage(range) / Cooldown;
}
public override int Ammo

View file

@ -1,8 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Mathematics;
using static Unity.Mathematics.math;
using static CultMath.math;
public class LockWeapon : InstantWeapon
{
@ -82,10 +79,11 @@ public class LockWeapon : InstantWeapon
DirectionImpact = Evaluate(_directionImpact);
Decay = Evaluate(_decay);
var degrees = acos(dot(normalize(Entity.Target.Value.Position - Entity.Position), normalize(Entity.LookDirection))) * 57.2958f;
if (degrees < LockAngle)
var targetDirection = Entity.Target.Value.CultPosition - Entity.CultPosition;
var angleDegrees = AetheriaMath.AngleDegrees(targetDirection, Entity.CultLookDirection);
if (angleDegrees < LockAngle)
{
var lerp = 1 - unlerp(0, 90, degrees);
var lerp = 1 - unlerp(0, 90, angleDegrees);
_lock = saturate(_lock + pow(lerp, DirectionImpact) * dt * LockSpeed * pow(Entity.EntityInfoGathered[Entity.Target.Value], SensorImpact));
}
else _lock = saturate(_lock - dt * Decay);

View file

@ -2,10 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
using System;
using System.Linq;
using Unity.Mathematics;
using static Unity.Mathematics.math;
using static CultMath.math;
public class MiningTool : Behavior
{
@ -45,10 +42,11 @@ public class MiningTool : Behavior
public override bool Execute(float dt)
{
Range = Evaluate(_range);
if (Entity.Zone.TryGetAsteroidBelt(AsteroidBeltBodyKey, out var belt) &&
!string.IsNullOrWhiteSpace(AsteroidBeltBodyKey) &&
if (!string.IsNullOrWhiteSpace(AsteroidBeltBodyKey) &&
Entity.Zone.TryGetAsteroidBelt(AsteroidBeltBodyKey, out var belt) &&
Entity.Zone.AsteroidExists(AsteroidBeltBodyKey, Asteroid) &&
length(Entity.Position.xz - belt.Transforms[Asteroid].xy) - belt.Transforms[Asteroid].w < Range)
Entity.Zone.TryGetCultAsteroidTransform(AsteroidBeltBodyKey, Asteroid, out var asteroidPosition, out var asteroidRadius) &&
length(Entity.CultPositionXZ - asteroidPosition) - asteroidRadius < Range)
{
Entity.Zone.MineAsteroid(
Entity,

View file

@ -2,8 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
using Unity.Mathematics;
using static Unity.Mathematics.math;
using static CultMath.math;
public class Radiator : Behavior, IAlwaysUpdatedBehavior, IInitializableBehavior
{

View file

@ -6,8 +6,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using UniRx;
using Unity.Mathematics;
using static Unity.Mathematics.math;
using static CultMath.math;
public class Reactor : Behavior, IOrderedBehavior, IDisposable
{

Some files were not shown because too many files have changed in this diff Show more