Aetheria/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStateCommitLog.cs
2026-06-13 21:30:42 +01:00

461 lines
20 KiB
C#

using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using MessagePack;
#nullable enable
namespace GameCult.Aetheria.State.Unity
{
public enum AetheriaRuntimeCommitKind
{
PlayerSettings,
LoadoutTemplate,
RunCheckpoint
}
public sealed class AetheriaRuntimeCommitEnvelope
{
public AetheriaRuntimeCommitEnvelope(
string schema,
AetheriaRuntimeCommitKind kind,
string commandId,
string createdAtUtc,
string path)
{
Schema = schema;
Kind = kind;
CommandId = commandId;
CreatedAtUtc = createdAtUtc;
Path = path;
}
public string Schema { get; }
public AetheriaRuntimeCommitKind Kind { get; }
public string CommandId { get; }
public string CreatedAtUtc { get; }
public string Path { get; }
}
public sealed class AetheriaRuntimePlayerSettingsCommit
{
public string PlayerName { get; set; } = "";
public bool TutorialPassed { get; set; }
public IReadOnlyList<AetheriaRuntimeStoryFileHashCommit> StoryFileHashes { get; set; } = Array.Empty<AetheriaRuntimeStoryFileHashCommit>();
public string TemperatureUnit { get; set; } = "Celsius";
public int SignificantDigits { get; set; } = 3;
public string NebulaQuality { get; set; } = "Normal";
public bool ShowAsteroidsInMinimap { get; set; }
public IReadOnlyList<AetheriaRuntimeInputBindingCommit> BindingOverrides { get; set; } = Array.Empty<AetheriaRuntimeInputBindingCommit>();
public IReadOnlyList<string> ActionBarInputs { get; set; } = Array.Empty<string>();
}
public sealed class AetheriaRuntimeStoryFileHashCommit
{
public string StoryPath { get; set; } = "";
public string Hash { get; set; } = "";
}
public sealed class AetheriaRuntimeInputBindingCommit
{
public string ActionName { get; set; } = "";
public int BindingIndex { get; set; }
public string BindingPath { get; set; } = "";
}
public sealed class AetheriaRuntimeLoadoutTemplateCommit
{
public string Name { get; set; } = "";
public string OwnerPlayerKey { get; set; } = "";
public AetheriaRuntimeEntityLoadoutCommit RootEntity { get; set; } = new AetheriaRuntimeEntityLoadoutCommit();
}
public sealed class AetheriaRuntimeEntityLoadoutCommit
{
public string Name { get; set; } = "";
public string Kind { get; set; } = "";
public string CorporationLegacyId { get; set; } = "";
public AetheriaRuntimeLoadoutItemCommit Hull { get; set; } = new AetheriaRuntimeLoadoutItemCommit();
public IReadOnlyList<AetheriaRuntimeLoadoutItemSlotCommit> Equipment { get; set; } = Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>();
public IReadOnlyList<AetheriaRuntimeLoadoutItemSlotCommit> CargoBays { get; set; } = Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>();
public IReadOnlyList<AetheriaRuntimeLoadoutItemSlotCommit> DockingBays { get; set; } = Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>();
public IReadOnlyList<AetheriaRuntimeCargoBayLoadoutCommit> CargoContents { get; set; } = Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>();
public IReadOnlyList<AetheriaRuntimeCargoBayLoadoutCommit> DockingBayContents { get; set; } = Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>();
public IReadOnlyList<int> DockingBayAssignments { get; set; } = Array.Empty<int>();
public IReadOnlyList<IReadOnlyList<int>> WeaponGroups { get; set; } = Array.Empty<IReadOnlyList<int>>();
public IReadOnlyList<AetheriaRuntimeEntityLoadoutCommit> Children { get; set; } = Array.Empty<AetheriaRuntimeEntityLoadoutCommit>();
}
public sealed class AetheriaRuntimeLoadoutItemCommit
{
public string ItemDefinitionLegacyId { get; set; } = "";
public double Quality { get; set; } = 1.0;
public double Durability { get; set; } = 1.0;
public int Quantity { get; set; } = 1;
}
public sealed class AetheriaRuntimeLoadoutItemSlotCommit
{
public int X { get; set; }
public int Y { get; set; }
public AetheriaRuntimeLoadoutItemCommit Item { get; set; } = new AetheriaRuntimeLoadoutItemCommit();
}
public sealed class AetheriaRuntimeCargoBayLoadoutCommit
{
public IReadOnlyList<AetheriaRuntimeLoadoutItemSlotCommit> Items { get; set; } = Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>();
}
public sealed class AetheriaRuntimeRunCheckpointCommit
{
public string RunId { get; set; } = "local";
public bool IsTutorial { get; set; }
public int EntranceZoneIndex { get; set; } = -1;
public int ExitZoneIndex { get; set; } = -1;
public int CurrentZoneIndex { get; set; } = -1;
public int CurrentZoneEntityIndex { get; set; } = -1;
public IReadOnlyList<int> DiscoveredZoneIndices { get; set; } = Array.Empty<int>();
public IReadOnlyList<AetheriaRuntimeZoneSnapshotCommit> Zones { get; set; } = Array.Empty<AetheriaRuntimeZoneSnapshotCommit>();
}
public sealed class AetheriaRuntimeZoneSnapshotCommit
{
public int ZoneIndex { get; set; } = -1;
public string Name { get; set; } = "";
public double PositionX { get; set; }
public double PositionY { get; set; }
public IReadOnlyList<int> AdjacentZoneIndices { get; set; } = Array.Empty<int>();
public IReadOnlyList<int> FactionIndices { get; set; } = Array.Empty<int>();
public int OwnerFactionIndex { get; set; } = -1;
public IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> Entities { get; set; } = Array.Empty<AetheriaRuntimeEntitySnapshotCommit>();
}
public sealed class AetheriaRuntimeEntitySnapshotCommit
{
public int EntityIndex { get; set; } = -1;
public string Name { get; set; } = "";
public string Kind { get; set; } = "";
public double PositionX { get; set; }
public double PositionY { get; set; }
public double PositionZ { get; set; }
public double DirectionX { get; set; }
public double DirectionY { get; set; }
public string CorporationLegacyId { get; set; } = "";
public string HullItemDefinitionLegacyId { get; set; } = "";
public IReadOnlyList<AetheriaRuntimeLoadoutItemSlotCommit> Equipment { get; set; } = Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>();
public IReadOnlyList<AetheriaRuntimeLoadoutItemSlotCommit> CargoBays { get; set; } = Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>();
public IReadOnlyList<AetheriaRuntimeLoadoutItemSlotCommit> DockingBays { get; set; } = Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>();
public IReadOnlyList<int> ChildEntityIndices { get; set; } = Array.Empty<int>();
public IReadOnlyList<IReadOnlyList<int>> WeaponGroups { get; set; } = Array.Empty<IReadOnlyList<int>>();
}
public static class AetheriaRuntimeStateCommitLog
{
public const string CommitSchema = "gamecult.aetheria.runtime_commit.v1";
private delegate void WritePayload(ref MessagePackWriter writer);
public static string GetPendingDirectory(string stateFilePath)
{
if (string.IsNullOrWhiteSpace(stateFilePath))
throw new ArgumentException("State file path must be non-empty.", nameof(stateFilePath));
return stateFilePath + ".pending";
}
public static AetheriaRuntimeCommitEnvelope QueuePlayerSettings(
string stateFilePath,
AetheriaRuntimePlayerSettingsCommit settings)
{
if (settings == null) throw new ArgumentNullException(nameof(settings));
return WriteCommit(stateFilePath, AetheriaRuntimeCommitKind.PlayerSettings, (ref MessagePackWriter writer) => WritePlayerSettings(ref writer, settings));
}
public static AetheriaRuntimeCommitEnvelope QueueLoadoutTemplate(
string stateFilePath,
AetheriaRuntimeLoadoutTemplateCommit loadout)
{
if (loadout == null) throw new ArgumentNullException(nameof(loadout));
return WriteCommit(stateFilePath, AetheriaRuntimeCommitKind.LoadoutTemplate, (ref MessagePackWriter writer) => WriteLoadoutTemplate(ref writer, loadout));
}
public static AetheriaRuntimeCommitEnvelope QueueRunCheckpoint(
string stateFilePath,
AetheriaRuntimeRunCheckpointCommit run)
{
if (run == null) throw new ArgumentNullException(nameof(run));
return WriteCommit(stateFilePath, AetheriaRuntimeCommitKind.RunCheckpoint, (ref MessagePackWriter writer) => WriteRunCheckpoint(ref writer, run));
}
public static IReadOnlyList<AetheriaRuntimeCommitEnvelope> ReadPending(string stateFilePath)
{
var pendingDirectory = GetPendingDirectory(stateFilePath);
if (!Directory.Exists(pendingDirectory))
return Array.Empty<AetheriaRuntimeCommitEnvelope>();
return Directory
.EnumerateFiles(pendingDirectory, "*.cc")
.OrderBy(path => path, StringComparer.Ordinal)
.Select(ReadEnvelope)
.ToArray();
}
private static AetheriaRuntimeCommitEnvelope WriteCommit(
string stateFilePath,
AetheriaRuntimeCommitKind kind,
WritePayload writePayload)
{
var commandId = Guid.NewGuid().ToString("N");
var createdAtUtc = DateTime.UtcNow.ToString("O");
var pendingDirectory = GetPendingDirectory(stateFilePath);
Directory.CreateDirectory(pendingDirectory);
var finalPath = Path.Combine(
pendingDirectory,
$"{createdAtUtc.Replace(':', '-')}.{KindToken(kind)}.{commandId}.cc");
var tempPath = finalPath + ".tmp";
var buffer = new ArrayBufferWriter<byte>();
var writer = new MessagePackWriter(buffer);
writer.WriteArrayHeader(5);
writer.Write(CommitSchema);
writer.Write(KindToken(kind));
writer.Write(commandId);
writer.Write(createdAtUtc);
writePayload(ref writer);
writer.Flush();
File.WriteAllBytes(tempPath, buffer.WrittenSpan.ToArray());
if (File.Exists(finalPath))
File.Delete(finalPath);
File.Move(tempPath, finalPath);
return new AetheriaRuntimeCommitEnvelope(CommitSchema, kind, commandId, createdAtUtc, finalPath);
}
private static AetheriaRuntimeCommitEnvelope ReadEnvelope(string path)
{
var reader = new MessagePackReader(File.ReadAllBytes(path));
var fields = reader.ReadArrayHeader();
var schema = fields > 0 ? ReadString(ref reader) : "";
var kind = fields > 1 ? ParseKind(ReadString(ref reader)) : AetheriaRuntimeCommitKind.RunCheckpoint;
var commandId = fields > 2 ? ReadString(ref reader) : "";
var createdAtUtc = fields > 3 ? ReadString(ref reader) : "";
for (var field = 4; field < fields; field++)
reader.Skip();
return new AetheriaRuntimeCommitEnvelope(schema, kind, commandId, createdAtUtc, path);
}
private static void WritePlayerSettings(ref MessagePackWriter writer, AetheriaRuntimePlayerSettingsCommit settings)
{
writer.WriteArrayHeader(9);
writer.Write(settings.PlayerName ?? "");
writer.Write(settings.TutorialPassed);
WriteStoryHashes(ref writer, settings.StoryFileHashes);
writer.Write(settings.TemperatureUnit ?? "");
writer.Write(settings.SignificantDigits);
writer.Write(settings.NebulaQuality ?? "");
writer.Write(settings.ShowAsteroidsInMinimap);
WriteBindings(ref writer, settings.BindingOverrides);
WriteStrings(ref writer, settings.ActionBarInputs);
}
private static void WriteStoryHashes(ref MessagePackWriter writer, IReadOnlyList<AetheriaRuntimeStoryFileHashCommit>? hashes)
{
hashes ??= Array.Empty<AetheriaRuntimeStoryFileHashCommit>();
writer.WriteArrayHeader(hashes.Count);
foreach (var hash in hashes)
{
writer.WriteArrayHeader(2);
writer.Write(hash.StoryPath ?? "");
writer.Write(hash.Hash ?? "");
}
}
private static void WriteBindings(ref MessagePackWriter writer, IReadOnlyList<AetheriaRuntimeInputBindingCommit>? bindings)
{
bindings ??= Array.Empty<AetheriaRuntimeInputBindingCommit>();
writer.WriteArrayHeader(bindings.Count);
foreach (var binding in bindings)
{
writer.WriteArrayHeader(3);
writer.Write(binding.ActionName ?? "");
writer.Write(binding.BindingIndex);
writer.Write(binding.BindingPath ?? "");
}
}
private static void WriteLoadoutTemplate(ref MessagePackWriter writer, AetheriaRuntimeLoadoutTemplateCommit loadout)
{
writer.WriteArrayHeader(3);
writer.Write(loadout.Name ?? "");
writer.Write(loadout.OwnerPlayerKey ?? "");
WriteEntityLoadout(ref writer, loadout.RootEntity);
}
private static void WriteEntityLoadout(ref MessagePackWriter writer, AetheriaRuntimeEntityLoadoutCommit entity)
{
writer.WriteArrayHeader(12);
writer.Write(entity.Name ?? "");
writer.Write(entity.Kind ?? "");
writer.Write(entity.CorporationLegacyId ?? "");
WriteLoadoutItem(ref writer, entity.Hull);
WriteItemSlots(ref writer, entity.Equipment);
WriteItemSlots(ref writer, entity.CargoBays);
WriteItemSlots(ref writer, entity.DockingBays);
WriteCargoBays(ref writer, entity.CargoContents);
WriteCargoBays(ref writer, entity.DockingBayContents);
WriteInts(ref writer, entity.DockingBayAssignments);
WriteIntLists(ref writer, entity.WeaponGroups);
WriteChildren(ref writer, entity.Children);
}
private static void WriteLoadoutItem(ref MessagePackWriter writer, AetheriaRuntimeLoadoutItemCommit item)
{
writer.WriteArrayHeader(4);
writer.Write(item.ItemDefinitionLegacyId ?? "");
writer.Write(item.Quality);
writer.Write(item.Durability);
writer.Write(item.Quantity);
}
private static void WriteItemSlots(ref MessagePackWriter writer, IReadOnlyList<AetheriaRuntimeLoadoutItemSlotCommit>? slots)
{
slots ??= Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>();
writer.WriteArrayHeader(slots.Count);
foreach (var slot in slots)
{
writer.WriteArrayHeader(3);
writer.Write(slot.X);
writer.Write(slot.Y);
WriteLoadoutItem(ref writer, slot.Item);
}
}
private static void WriteCargoBays(ref MessagePackWriter writer, IReadOnlyList<AetheriaRuntimeCargoBayLoadoutCommit>? cargoBays)
{
cargoBays ??= Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>();
writer.WriteArrayHeader(cargoBays.Count);
foreach (var bay in cargoBays)
WriteItemSlots(ref writer, bay.Items);
}
private static void WriteChildren(ref MessagePackWriter writer, IReadOnlyList<AetheriaRuntimeEntityLoadoutCommit>? children)
{
children ??= Array.Empty<AetheriaRuntimeEntityLoadoutCommit>();
writer.WriteArrayHeader(children.Count);
foreach (var child in children)
WriteEntityLoadout(ref writer, child);
}
private static void WriteRunCheckpoint(ref MessagePackWriter writer, AetheriaRuntimeRunCheckpointCommit run)
{
writer.WriteArrayHeader(8);
writer.Write(run.RunId ?? "");
writer.Write(run.IsTutorial);
writer.Write(run.EntranceZoneIndex);
writer.Write(run.ExitZoneIndex);
writer.Write(run.CurrentZoneIndex);
writer.Write(run.CurrentZoneEntityIndex);
WriteInts(ref writer, run.DiscoveredZoneIndices);
WriteZoneSnapshots(ref writer, run.Zones);
}
private static void WriteZoneSnapshots(ref MessagePackWriter writer, IReadOnlyList<AetheriaRuntimeZoneSnapshotCommit>? zones)
{
zones ??= Array.Empty<AetheriaRuntimeZoneSnapshotCommit>();
writer.WriteArrayHeader(zones.Count);
foreach (var zone in zones)
{
writer.WriteArrayHeader(8);
writer.Write(zone.ZoneIndex);
writer.Write(zone.Name ?? "");
writer.Write(zone.PositionX);
writer.Write(zone.PositionY);
WriteInts(ref writer, zone.AdjacentZoneIndices);
WriteInts(ref writer, zone.FactionIndices);
writer.Write(zone.OwnerFactionIndex);
WriteEntitySnapshots(ref writer, zone.Entities);
}
}
private static void WriteEntitySnapshots(ref MessagePackWriter writer, IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit>? entities)
{
entities ??= Array.Empty<AetheriaRuntimeEntitySnapshotCommit>();
writer.WriteArrayHeader(entities.Count);
foreach (var entity in entities)
{
writer.WriteArrayHeader(15);
writer.Write(entity.EntityIndex);
writer.Write(entity.Name ?? "");
writer.Write(entity.Kind ?? "");
writer.Write(entity.PositionX);
writer.Write(entity.PositionY);
writer.Write(entity.PositionZ);
writer.Write(entity.DirectionX);
writer.Write(entity.DirectionY);
writer.Write(entity.CorporationLegacyId ?? "");
writer.Write(entity.HullItemDefinitionLegacyId ?? "");
WriteItemSlots(ref writer, entity.Equipment);
WriteItemSlots(ref writer, entity.CargoBays);
WriteItemSlots(ref writer, entity.DockingBays);
WriteInts(ref writer, entity.ChildEntityIndices);
WriteIntLists(ref writer, entity.WeaponGroups);
}
}
private static void WriteStrings(ref MessagePackWriter writer, IReadOnlyList<string>? values)
{
values ??= Array.Empty<string>();
writer.WriteArrayHeader(values.Count);
foreach (var value in values)
writer.Write(value ?? "");
}
private static void WriteInts(ref MessagePackWriter writer, IReadOnlyList<int>? values)
{
values ??= Array.Empty<int>();
writer.WriteArrayHeader(values.Count);
foreach (var value in values)
writer.Write(value);
}
private static void WriteIntLists(ref MessagePackWriter writer, IReadOnlyList<IReadOnlyList<int>>? values)
{
values ??= Array.Empty<IReadOnlyList<int>>();
writer.WriteArrayHeader(values.Count);
foreach (var value in values)
WriteInts(ref writer, value);
}
private static string KindToken(AetheriaRuntimeCommitKind kind)
{
return kind switch
{
AetheriaRuntimeCommitKind.PlayerSettings => "player_settings",
AetheriaRuntimeCommitKind.LoadoutTemplate => "loadout_template",
AetheriaRuntimeCommitKind.RunCheckpoint => "run_checkpoint",
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null)
};
}
private static AetheriaRuntimeCommitKind ParseKind(string token)
{
return token switch
{
"player_settings" => AetheriaRuntimeCommitKind.PlayerSettings,
"loadout_template" => AetheriaRuntimeCommitKind.LoadoutTemplate,
"run_checkpoint" => AetheriaRuntimeCommitKind.RunCheckpoint,
_ => throw new InvalidDataException($"Unknown Aetheria runtime commit kind '{token}'.")
};
}
private static string ReadString(ref MessagePackReader reader)
{
return reader.ReadString() ?? "";
}
}
}