Aetheria/Aetheria.State/AetheriaRuntimeCommitLogApplier.cs

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.");
}
}