/ GUNRPG.Application / Distributed / LocalGameAuthority.cs
LocalGameAuthority.cs
 1  using System.Security.Cryptography;
 2  using System.Text;
 3  using System.Text.Json;
 4  
 5  namespace GUNRPG.Application.Distributed;
 6  
 7  /// <summary>
 8  /// Single-node game authority that applies actions locally using the shared
 9  /// <see cref="IDeterministicGameEngine"/>. Maintains the same action log,
10  /// state hashing, and interface as <see cref="DistributedAuthority"/> but
11  /// without any networking or peer replication.
12  /// </summary>
13  public sealed class LocalGameAuthority : IGameAuthority
14  {
15      private readonly IDeterministicGameEngine _engine;
16      private readonly List<DistributedActionEntry> _actionLog = new();
17      private readonly object _lock = new();
18  
19      private GameStateDto _currentState;
20      private long _nextSequenceNumber;
21      private string _currentStateHash;
22  
23      private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
24      {
25          PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
26          WriteIndented = false
27      };
28  
29      public LocalGameAuthority(Guid nodeId, IDeterministicGameEngine engine)
30      {
31          NodeId = nodeId;
32          _engine = engine;
33          _currentState = new GameStateDto { ActionCount = 0, Operators = new List<GameStateDto.OperatorSnapshot>() };
34          _currentStateHash = ComputeHash(_currentState);
35      }
36  
37      public Guid NodeId { get; }
38      public bool IsDesynced => false;
39  
40      public Task SubmitActionAsync(PlayerActionDto action, CancellationToken ct = default)
41      {
42          lock (_lock)
43          {
44              _currentState = _engine.Step(_currentState, action);
45              var hash = ComputeHash(_currentState);
46              _currentStateHash = hash;
47  
48              _actionLog.Add(new DistributedActionEntry
49              {
50                  SequenceNumber = _nextSequenceNumber++,
51                  NodeId = NodeId,
52                  Action = action,
53                  StateHashAfterApply = hash
54              });
55          }
56  
57          return Task.CompletedTask;
58      }
59  
60      public GameStateDto GetCurrentState()
61      {
62          lock (_lock)
63          {
64              return _currentState;
65          }
66      }
67  
68      public string GetCurrentStateHash()
69      {
70          lock (_lock)
71          {
72              return _currentStateHash;
73          }
74      }
75  
76      public IReadOnlyList<DistributedActionEntry> GetActionLog()
77      {
78          lock (_lock)
79          {
80              return _actionLog.ToList().AsReadOnly();
81          }
82      }
83  
84      private static string ComputeHash(GameStateDto state)
85      {
86          var json = JsonSerializer.Serialize(state, SerializerOptions);
87          var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(json));
88          return Convert.ToHexString(bytes);
89      }
90  }