385 lines
15 KiB
C#
385 lines
15 KiB
C#
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;
|
|
using MessagePack;
|
|
|
|
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":
|
|
await node.PutPlayerSettingsAsync(
|
|
ToPlayerSettings(command.PlayerSettings ?? throw MissingPayload(command), 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 = MessagePackSerializer.Deserialize<AetheriaRuntimeStateCommitDocument>(File.ReadAllBytes(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 = ReferenceKey("aetheria.corporation", entity.CorporationLegacyId ?? ""),
|
|
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 = ReferenceKey("aetheria.item_definition", item.ItemDefinitionLegacyId ?? ""),
|
|
Quality = item.Quality,
|
|
Durability = item.Durability,
|
|
Quantity = item.Quantity
|
|
};
|
|
}
|
|
|
|
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 ToItemSlots(slots)
|
|
.Select(slot => new AetheriaEntityItemSlot
|
|
{
|
|
Position = slot.Position,
|
|
ItemKey = slot.Item.ItemKey
|
|
})
|
|
.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,
|
|
CurrentZoneEntityIndex = checkpoint.CurrentZoneEntityIndex,
|
|
DiscoveredZoneIndices = ToIntArray(checkpoint.DiscoveredZoneIndices),
|
|
ActionBarBindings = ToActionBarBindings(checkpoint.ActionBarBindings),
|
|
FactionRelationships = ToFactionRelationships(checkpoint.FactionRelationships),
|
|
UpdatedAtUtc = updatedAtUtc
|
|
};
|
|
|
|
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()
|
|
})
|
|
.ConfigureAwait(false);
|
|
zoneKeys.Add(zoneKey.ToString());
|
|
}
|
|
|
|
if (zoneKeys.Count > 0)
|
|
run.ZoneKeys = zoneKeys.ToArray();
|
|
await node.PutRunStateAsync(RunKey(run.RunId), run).ConfigureAwait(false);
|
|
}
|
|
|
|
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
|
|
},
|
|
FactionKey = ReferenceKey("aetheria.corporation", entity.CorporationLegacyId ?? ""),
|
|
HullItemKey = ReferenceKey("aetheria.item_definition", entity.HullItemDefinitionLegacyId ?? ""),
|
|
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()
|
|
};
|
|
}
|
|
|
|
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 = ReferenceKey("aetheria.item_definition", binding.ItemDefinitionLegacyId ?? ""),
|
|
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 = ReferenceKey("aetheria.corporation", relationship.CorporationLegacyId ?? ""),
|
|
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 ReferenceKey(string documentName, string legacyId)
|
|
{
|
|
return string.IsNullOrWhiteSpace(legacyId) ? "" : $"{documentName}:legacy:{legacyId}";
|
|
}
|
|
|
|
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.");
|
|
}
|
|
}
|