Neuer Nachrichtentyp SyncStateChangedMessage zur Übermittlung des Sync-Status über das MVVM-Messaging-System eingeführt. GameStateService sendet nach relevanten Aktionen den aktuellen Sync-Status. Methoden zur Synchronisierung sind jetzt thread-sicher und melden Statusänderungen laufend. Fehler beim Senden von Queue-Aktionen werden einzeln behandelt und führen zu Fehlermeldungen im Messenger. ViewModels aktualisieren neue Properties (IsSyncing, SyncStatusText, PendingSyncActions) bei Statusänderungen. XAML-Seiten zeigen Verbindungs- und Sync-Status sowie Offline-Aktionen an. SignalRService meldet Verbindungsstatus konsistenter, auch bei Fehlern und Disconnects.
185 lines
5.9 KiB
C#
185 lines
5.9 KiB
C#
using System.Text.Json;
|
|
using CommunityToolkit.Mvvm.Messaging;
|
|
using SlipItIn.Messages;
|
|
using SlipItIn.Models;
|
|
using SlipItIn.Services.Interfaces;
|
|
using SlipItIn.Shared.DTOs;
|
|
|
|
namespace SlipItIn.Services;
|
|
|
|
public class GameStateService : IGameStateService
|
|
{
|
|
private readonly ISignalRService _signalR;
|
|
private readonly ILocalStorageService _localStorage;
|
|
private readonly IMessenger _messenger;
|
|
private readonly SemaphoreSlim _syncLock = new(1, 1);
|
|
|
|
private readonly List<QueuedGameAction> _queuedActions = [];
|
|
|
|
public GameStateDto? CurrentGameState { get; private set; }
|
|
public PlayerHandDto? CurrentPlayerHand { get; private set; }
|
|
public SlipChallengeDto? PendingChallenge { get; private set; }
|
|
|
|
public GameStateService(ISignalRService signalR, ILocalStorageService localStorage)
|
|
{
|
|
_signalR = signalR;
|
|
_localStorage = localStorage;
|
|
_messenger = WeakReferenceMessenger.Default;
|
|
|
|
_signalR.LobbyCreated += (_, value) => _messenger.Send(new LobbyCreatedMessage(value));
|
|
|
|
_signalR.GameStateUpdated += async (_, state) =>
|
|
{
|
|
CurrentGameState = state;
|
|
await _localStorage.SaveGameStateAsync(state);
|
|
_messenger.Send(new GameStateChangedMessage(state));
|
|
};
|
|
|
|
_signalR.GameStarted += (_, gameId) => _messenger.Send(new GameStartedMessage(gameId));
|
|
|
|
_signalR.PlayerHandUpdated += async (_, hand) =>
|
|
{
|
|
CurrentPlayerHand = hand;
|
|
await _localStorage.SavePlayerHandAsync(hand);
|
|
_messenger.Send(new PlayerHandChangedMessage(hand));
|
|
};
|
|
|
|
_signalR.ChallengeReceived += (_, challenge) =>
|
|
{
|
|
PendingChallenge = challenge;
|
|
_messenger.Send(new ChallengeReceivedMessage(challenge));
|
|
};
|
|
|
|
_signalR.ErrorReceived += (_, error) => _messenger.Send(new ErrorOccurredMessage(error));
|
|
_signalR.ConnectionStateChanged += async (_, connected) =>
|
|
{
|
|
_messenger.Send(new ConnectionStateChangedMessage(connected));
|
|
if (!connected)
|
|
return;
|
|
|
|
try
|
|
{
|
|
await ResyncAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_messenger.Send(new ErrorOccurredMessage($"Resync fehlgeschlagen: {ex.Message}"));
|
|
}
|
|
};
|
|
}
|
|
|
|
public async Task InitializeAsync()
|
|
{
|
|
CurrentGameState = await _localStorage.GetGameStateAsync();
|
|
CurrentPlayerHand = await _localStorage.GetPlayerHandAsync();
|
|
|
|
var queued = await _localStorage.GetQueuedActionsAsync();
|
|
_queuedActions.Clear();
|
|
_queuedActions.AddRange(queued.OrderBy(a => a.CreatedAtUtc));
|
|
|
|
if (CurrentGameState is not null)
|
|
_messenger.Send(new GameStateChangedMessage(CurrentGameState));
|
|
|
|
if (CurrentPlayerHand is not null)
|
|
_messenger.Send(new PlayerHandChangedMessage(CurrentPlayerHand));
|
|
|
|
PublishSyncState(false, "Bereit", _queuedActions.Count);
|
|
}
|
|
|
|
public async Task ResyncAsync()
|
|
{
|
|
await _syncLock.WaitAsync();
|
|
try
|
|
{
|
|
PublishSyncState(true, "Syncing...", _queuedActions.Count);
|
|
|
|
if (CurrentGameState?.GameId > 0 && _signalR.IsConnected)
|
|
await _signalR.RequestGameStateAsync(CurrentGameState.GameId);
|
|
|
|
await FlushQueuedActionsCoreAsync();
|
|
PublishSyncState(false, "Synchronisiert", _queuedActions.Count);
|
|
}
|
|
catch
|
|
{
|
|
PublishSyncState(false, "Sync fehlgeschlagen", _queuedActions.Count);
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
_syncLock.Release();
|
|
}
|
|
}
|
|
|
|
public async Task EnqueueActionAsync(string method, params object[] arguments)
|
|
{
|
|
var action = new QueuedGameAction
|
|
{
|
|
Method = method,
|
|
ArgumentsJson = JsonSerializer.Serialize(arguments),
|
|
CreatedAtUtc = DateTime.UtcNow
|
|
};
|
|
|
|
_queuedActions.Add(action);
|
|
await _localStorage.SaveQueuedActionsAsync(_queuedActions);
|
|
PublishSyncState(false, "Offline-Aktion gespeichert", _queuedActions.Count);
|
|
}
|
|
|
|
public async Task FlushQueuedActionsAsync()
|
|
{
|
|
await _syncLock.WaitAsync();
|
|
try
|
|
{
|
|
PublishSyncState(true, "Warteschlange wird gesendet...", _queuedActions.Count);
|
|
await FlushQueuedActionsCoreAsync();
|
|
PublishSyncState(false, "Warteschlange synchronisiert", _queuedActions.Count);
|
|
}
|
|
finally
|
|
{
|
|
_syncLock.Release();
|
|
}
|
|
}
|
|
|
|
public async Task ClearLocalGameDataAsync()
|
|
{
|
|
CurrentGameState = null;
|
|
CurrentPlayerHand = null;
|
|
PendingChallenge = null;
|
|
_queuedActions.Clear();
|
|
|
|
await _localStorage.ClearGameStateAsync();
|
|
await _localStorage.ClearPlayerHandAsync();
|
|
await _localStorage.ClearQueuedActionsAsync();
|
|
|
|
PublishSyncState(false, "Bereit", 0);
|
|
}
|
|
|
|
private async Task FlushQueuedActionsCoreAsync()
|
|
{
|
|
if (!_signalR.IsConnected || _queuedActions.Count == 0)
|
|
return;
|
|
|
|
var snapshot = _queuedActions.ToList();
|
|
foreach (var action in snapshot)
|
|
{
|
|
try
|
|
{
|
|
await _signalR.SendQueuedActionAsync(action);
|
|
_queuedActions.Remove(action);
|
|
PublishSyncState(true, "Warteschlange wird gesendet...", _queuedActions.Count);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_messenger.Send(new ErrorOccurredMessage($"Queue-Aktion fehlgeschlagen ({action.Method}): {ex.Message}"));
|
|
break;
|
|
}
|
|
}
|
|
|
|
await _localStorage.SaveQueuedActionsAsync(_queuedActions);
|
|
}
|
|
|
|
private void PublishSyncState(bool isSyncing, string statusText, int pendingActions)
|
|
{
|
|
_messenger.Send(new SyncStateChangedMessage(new SyncStateInfo(isSyncing, statusText, pendingActions)));
|
|
}
|
|
}
|