Sync daemon authority migration
This commit is contained in:
parent
030b4b5d74
commit
39a0dc8737
295 changed files with 25121 additions and 7360 deletions
13
Aetheria.State.Daemon/Aetheria.State.Daemon.csproj
Normal file
13
Aetheria.State.Daemon/Aetheria.State.Daemon.csproj
Normal 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>
|
||||
402
Aetheria.State.Daemon/Program.cs
Normal file
402
Aetheria.State.Daemon/Program.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,5 @@
|
|||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Aetheria.State\Aetheria.State.csproj" />
|
||||
<ProjectReference Include="..\Aetheria.State.Unity\Aetheria.State.Unity.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
55
Aetheria.State/AetheriaRuntimeEveSurfaceStateProjector.cs
Normal file
55
Aetheria.State/AetheriaRuntimeEveSurfaceStateProjector.cs
Normal 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()
|
||||
};
|
||||
}
|
||||
}
|
||||
113
Aetheria.State/AetheriaRuntimeStateMapper.cs
Normal file
113
Aetheria.State/AetheriaRuntimeStateMapper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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; } = [];
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
52
Aetheria.State/docs/verse-daemon-shape.md
Normal file
52
Aetheria.State/docs/verse-daemon-shape.md
Normal 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
85
Aetheria.sln
Normal 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
BIN
Assets/DefaultVolumeProfile.asset
(Stored with Git LFS)
Normal file
Binary file not shown.
8
Assets/DefaultVolumeProfile.asset.meta
Normal file
8
Assets/DefaultVolumeProfile.asset.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7830976a04fc12843bb7f63ba390f142
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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"": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
184
Assets/Scripts/Gameplay/AetheriaDaemonIndirectRenderer.cs
Normal file
184
Assets/Scripts/Gameplay/AetheriaDaemonIndirectRenderer.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 66b38a2f45120a041941eae4412f7da1
|
||||
158
Assets/Scripts/Gameplay/AetheriaDaemonObserver.cs
Normal file
158
Assets/Scripts/Gameplay/AetheriaDaemonObserver.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Gameplay/AetheriaDaemonObserver.cs.meta
Normal file
2
Assets/Scripts/Gameplay/AetheriaDaemonObserver.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 4f5008a2876ee014fb9e442f1f6103e0
|
||||
81
Assets/Scripts/Gameplay/AetheriaDaemonRenderAssetCatalog.cs
Normal file
81
Assets/Scripts/Gameplay/AetheriaDaemonRenderAssetCatalog.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8fa370b1ea2904d40ae295007ac76b48
|
||||
294
Assets/Scripts/Gameplay/AetheriaDaemonRenderBuffer.cs
Normal file
294
Assets/Scripts/Gameplay/AetheriaDaemonRenderBuffer.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9cba277ef259bee438796ab4c0f3df64
|
||||
177
Assets/Scripts/Gameplay/AetheriaDaemonRenderGroupBatch.cs
Normal file
177
Assets/Scripts/Gameplay/AetheriaDaemonRenderGroupBatch.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 0038a222765ff82468c22a516cfcdbc1
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 146aa8d6d00cc1a44b44a5f83d69edea
|
||||
49
Assets/Scripts/Gameplay/AetheriaDaemonRenderMatrixJob.cs
Normal file
49
Assets/Scripts/Gameplay/AetheriaDaemonRenderMatrixJob.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f6b3b3dc2e2a29245a24b772e2cb87b4
|
||||
248
Assets/Scripts/Gameplay/AetheriaDaemonRenderNativeView.cs
Normal file
248
Assets/Scripts/Gameplay/AetheriaDaemonRenderNativeView.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 48740dbd37e79954ca00a3f6bd7ccef1
|
||||
220
Assets/Scripts/Gameplay/AetheriaDaemonSoaMemoryMap.cs
Normal file
220
Assets/Scripts/Gameplay/AetheriaDaemonSoaMemoryMap.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: d8988c7d796df504dab64fd2c64eb868
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
8
Assets/Scripts/Gameplay/Physics.meta
Normal file
8
Assets/Scripts/Gameplay/Physics.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: baa9b1a7f453a7a4aafe9c44c9fba7c3
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
865
Assets/Scripts/Gameplay/Physics/AetheriaYmirPhysicsBridge.cs
Normal file
865
Assets/Scripts/Gameplay/Physics/AetheriaYmirPhysicsBridge.cs
Normal 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; }
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8d2521ff2dd1835498a2c00656d4bff6
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5d5539a23b814254b038a16f32f1d8a9
|
||||
timeCreated: 1618453298
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"name": "Aetheria.Shared.Unity",
|
||||
"rootNamespace": "",
|
||||
"references": [
|
||||
"CultMath",
|
||||
"Unity.Mathematics",
|
||||
"UniRx",
|
||||
"Ink",
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue