using Fluxor; using KoogleApp.Games; using KoogleApp.Games.Training; using KoogleApp.Hub; using KoogleApp.Model; using KoogleApp.Model.EventMessages; using KoogleApp.Model.Framework; using KoogleApp.Services; using KoogleApp.Store.Game.Participants; using KoogleApp.Store.Game.Setup; using KoogleApp.Store.Game.ThrowTimer; using KoogleApp.Store.Game.UndoRedo; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.Win32.SafeHandles; using System; using System.Diagnostics; using System.Text.Json; namespace KoogleApp.Store.Game.ThrowPanel { public class ThrowPanelEffects( IMyEventAggregator eventAggregator, ILogger logger, HubConnectionService sharedHubService, IState throwPanelState, IState setupState, AuthenticationStateProvider authenticationStateProvider, IGameStatusDataService dataService, IGameServiceFactory gameServiceFactory, SessionStorage sessionStorage) : IDisposable { private readonly string _dataFilePath = "ThrowPanelState.json"; private readonly Lock _lock = new Lock(); public void Dispose() { //_sharedHubService?.Dispose(); } [EffectMethod] public async Task HandleStartAction(StartStopAction startStopAction, IDispatcher dispatcher) { ParticipantsState? participantsState = null; if (startStopAction.StartParams != null) // start action { dispatcher.Dispatch(new SetParticipatingPlayersAction(startStopAction.StartParams.Participants)); // here we just want to pass the participants with ThrowPanelStateChangedAction - therefore we create a copy of ParticipantsState - I think this is valid just for the startup of a new game participantsState = new ParticipantsState(startStopAction.StartParams.Participants, []); var gameService = gameServiceFactory.GetService(startStopAction.StartParams.KnownGameType); var gameModel = gameService.InitGameModel(startStopAction.StartParams, dispatcher); dispatcher.Dispatch(new GameModelChangedAction(gameModel.ConvertToState())); var username = await GetUsername(); dataService.UpdateData(new GameStatus() { ThrowPanelState = throwPanelState.Value, ParticipantsState = participantsState, SetupModel = startStopAction.StartParams, GameState = gameModel.ConvertToState(), }, username); } else // stop action { var gameModel = GameService.FinalizeGameModel(); _gameService = null; var username = await GetUsername(); await dataService.SaveToDatabaseAndReset(new GameStatus { ThrowPanelState = throwPanelState.Value with { ThrowPanelStateStatus = ThrowPanelStateStatus.GameEnd }, GameState = gameModel?.ConvertToState() }, username); dispatcher.Dispatch(new UpdateUndoRedoStateAction()); participantsState = new ParticipantsState(); dispatcher.Dispatch(new ThrowPanelStateChangedAction(throwPanelState.Value, true, ParticipantsState: participantsState, GameState: gameModel?.ConvertToState())); dispatcher.Dispatch(new ResetSetupStateAction()); } // reset settings and all relevant states for the next game dispatcher.Dispatch(new ParticipantsStateChangedAction(participantsState)); //dispatcher.Dispatch(new ResetSetupStateAction()); // TODO: not reduced } [EffectMethod] public Task HandleToggleAllPinsAction(ToggleAllPinsAction stopAction, IDispatcher dispatcher) { dispatcher.Dispatch(new ThrowPanelStateChangedAction(throwPanelState.Value, false, null, null)); return Task.CompletedTask; } [EffectMethod] public Task HandleTogglePinValueAction(TogglePinValueAction stopAction, IDispatcher dispatcher) { dispatcher.Dispatch(new ThrowPanelStateChangedAction(throwPanelState.Value, false, null, null)); return Task.CompletedTask; } [EffectMethod] public Task HandleUpdatePinStateByNumberAction(UpdatePinStateByNumberAction stopAction, IDispatcher dispatcher) { dispatcher.Dispatch(new ThrowPanelStateChangedAction(throwPanelState.Value, false, null, null)); return Task.CompletedTask; } [EffectMethod] public Task HandleToggleBellAction(ToggleBellAction stopAction, IDispatcher dispatcher) { dispatcher.Dispatch(new ThrowPanelStateChangedAction(throwPanelState.Value, false, null, null)); return Task.CompletedTask; } //ThrowBeforeUpdateAction //ThrowUpdateAction //ThrowAfterUpdate [EffectMethod] public async Task HandleEnsureBeforeThrowStatusAction(EnsureBeforeThrowStatusAction throwAction, IDispatcher dispatcher) { dispatcher.Dispatch(new ThrowPanelStateChangedAction(throwPanelState.Value, true, throwAction.ParticipantsState, throwAction.GameState)); } private IGameService? _gameService = null; private IGameService? GameService { get { if (_gameService == null) { _gameService = gameServiceFactory.GetService(dataService.StartParams.KnownGameType); } return _gameService; } } [EffectMethod] public async Task HandleThrowAction(ThrowAction throwAction, IDispatcher dispatcher) { var beforeStates = dataService.GetCurrentData(); // change ThrowStatus and save - here we need to save the old state, before it is changed by ThrowUpdateAction, so that undo/redo works correctly dispatcher.Dispatch(new EnsureBeforeThrowStatusAction(throwAction.ParticipantsState, beforeStates.Status.GameState)); // perform throw - we will get the new state after resetting all pins for the next throw. // For the calculation of the game-progress in HandleThrow nextState if normally not useful var nextState = beforeStates.Status.ThrowPanelState.CalculateNextPanelState(); //dispatcher.Dispatch(new ThrowUpdateAction(nextState)); var generalGameProgress = new GameProgress( BeforeThrowState: beforeStates.Status?.ThrowPanelState, BeforeParticipantsState: throwAction.ParticipantsState, AfterThrowState: throwPanelState.Value, AfterParticipantsState: throwAction.ParticipantsState, StartParams: dataService.StartParams, GameModel: beforeStates.Status.GameState.ConvertToModel(), NextThrowState: nextState); dispatcher.Dispatch(generalGameProgress); // currently unused //var participants = throwAction.ParticipantsState; //var gameSpecificProgress = generalGameProgress; var gameSpecificProgress = GameService.HandleThrow(generalGameProgress, dispatcher); //foreach (var action in actions) //{ // if (action is SelectedNextPlayerIdsAction) // { // participants = ((SelectedNextPlayerIdsAction)action).ParticipantsState; // } // dispatcher.Dispatch(action); //} var gameModelState = gameSpecificProgress.GameModel.ConvertToState(); dispatcher.Dispatch(new GameModelChangedAction(gameModelState)); await ShowBoardForSeconds(dispatcher); // save the new state after the throw dispatcher.Dispatch(new TriggerNewThrowPanelStateChangedAction(true, gameSpecificProgress.AfterParticipantsState, gameModelState, gameSpecificProgress.NextThrowState)); // this will trigger ThrowPanelStateChangedAction with the new state (not current _throwPanelState.Value) } [EffectMethod] public async Task HandleGameButtonAction(GameButtonAction action, IDispatcher dispatcher) { var beforeStates = dataService.GetCurrentData(); var beforeProgress = new GameProgress( BeforeThrowState: beforeStates.Status?.ThrowPanelState, BeforeParticipantsState: beforeStates.Status.ParticipantsState, AfterThrowState: throwPanelState.Value, AfterParticipantsState: beforeStates.Status.ParticipantsState, StartParams: dataService.StartParams, GameModel: beforeStates.Status.GameState.ConvertToModel(), NextThrowState: null); var afterProgress = GameService.HandleGameButton(action.CallbackName, beforeProgress, dispatcher); dispatcher.Dispatch(new TriggerNewThrowPanelStateChangedAction(true, afterProgress.AfterParticipantsState, afterProgress.GameModel.ConvertToState(), afterProgress.NextThrowState)); // this will trigger ThrowPanelStateChangedAction with the new state (not current _throwPanelState.Value) } private async Task ShowBoardForSeconds(IDispatcher dispatcher) { dispatcher.Dispatch(new StartTimerAction(5)); await eventAggregator.PublishAsync(new GameViewChangedMessage(GameView.Board) { Scope = EventScope.Circuit }); } [EffectMethod] public Task HandleTriggerNewThrowPanelStateChangedAction(TriggerNewThrowPanelStateChangedAction action, IDispatcher dispatcher) { if (action.NextModifiedThrowPanelState != null) { // Standardverhalten der Bahn wurde überschrieben, z.B. Bild gestellt oder sonstiges aus der game-spezifischen Logik dispatcher.Dispatch(new OverwriteThrowPanelStateAction(action.NextModifiedThrowPanelState)); dispatcher.Dispatch(new ThrowPanelStateChangedAction(action.NextModifiedThrowPanelState, action.SaveToDb, action.ParticipantsState, action.GameState)); } else { dispatcher.Dispatch(new ThrowPanelStateChangedAction(throwPanelState.Value, action.SaveToDb, action.ParticipantsState, action.GameState)); } return Task.CompletedTask; } [EffectMethod] public async Task HandleConnectToHubAction(ConnectToHubAction action, IDispatcher dispatcher) { try { await sharedHubService.ConnectToHub(dispatcher); } catch (Exception ex) { logger.LogError(ex, "Failed to connect to hub"); //dispatcher.Dispatch(new HubConnectionFailedAction(ex.Message)); } } [EffectMethod] public async Task HandelThrowPanelStateChangedAction(ThrowPanelStateChangedAction action, IDispatcher dispatcher) { dispatcher.Dispatch(new SaveThrowPanelStateAction(action.State)); dispatcher.Dispatch(new BroadcastThrowPanelStateAction(action.State)); if (action.SaveToDb) { var username = await GetUsername(); dataService.UpdateData(new GameStatus { ThrowPanelState = action.State, ParticipantsState = action.ParticipantsState, GameState = action.GameState }, username); // here we need to invalidate _dataFilePath and _sessionStorage, because with a new throw, state data must be provided by dataservice DeleteAllSessionsFile(dispatcher); dispatcher.Dispatch(new UpdateUndoRedoStateAction()); } } private static void DeleteAllSessionsFile(IDispatcher dispatcher) { dispatcher.Dispatch(new DeleteThrowPanelSessionAction()); dispatcher.Dispatch(new DeleteParticipantsSessionAction()); } [EffectMethod] public async Task HandleDeleteThrowPanelSessionAction(DeleteThrowPanelSessionAction allStatesFromSessionAction, IDispatcher dispatcher) { await sessionStorage.SetThrowPanelStateAsync(null); if (File.Exists(_dataFilePath)) { await Task.Delay(100); try { File.Delete(_dataFilePath); } catch (Exception) { } } } [EffectMethod] public async Task HandelLoadAllStatesFromSessionAction(LoadAllStatesFromSessionAction allStatesFromSessionAction, IDispatcher dispatcher) { dispatcher.Dispatch(new LoadThrowPanelStatesFromSessionAction()); dispatcher.Dispatch(new LoadParticipantsStatesFromSessionAction()); dispatcher.Dispatch(new LoadSetupStatesFromSessionAction()); dispatcher.Dispatch(new LoadGameStatesFromDataServiceAction()); } [EffectMethod] public async Task HandelLoadThrowPanelStatesFromSessionAction(LoadThrowPanelStatesFromSessionAction allStatesFromSessionAction, IDispatcher dispatcher) { var state = await sessionStorage.GetThrowPanelStateAsync(); if (state == null) { try { if (File.Exists(_dataFilePath)) { var json = await File.ReadAllTextAsync(_dataFilePath); state = JsonSerializer.Deserialize(json); } } catch (Exception e) { // TODO error handling } } if (state == null) { var data = dataService.GetCurrentData(); if (data.Status != null) { state = data.Status.ThrowPanelState; } dispatcher.Dispatch(new UpdateUndoRedoStateAction()); } if (state != null) { dispatcher.Dispatch(new ThrowPanelStateLoadedAction(state)); } } [EffectMethod] public async Task HandelSaveThrowPanelStateAction(SaveThrowPanelStateAction action, IDispatcher dispatcher) { await sessionStorage.SetThrowPanelStateAsync(action.State); try { var json = JsonSerializer.Serialize(action.State, new JsonSerializerOptions { WriteIndented = true }); await File.WriteAllTextAsync(_dataFilePath, json); } catch (Exception ex) { // TODO error handling } } [EffectMethod] public async Task HandelBroadcastThrowPanelStateAction(BroadcastThrowPanelStateAction action, IDispatcher dispatcher) { await sharedHubService.BroadcastThrowPanelState(action.State, dispatcher); } private async Task GetUsername() { var authState = await authenticationStateProvider.GetAuthenticationStateAsync(); var user = authState.User; var isAuthenticated = user.Identity?.IsAuthenticated ?? false; return isAuthenticated ? user.Identity?.Name : "Gast"; } } }