Handle disconnects, add restart flow and UI fixes
Server: Make DisconnectedPlayer async, await hub notifications, add logging, and delay scheduled deletion (2s). Harden ScheduleGameDeletion with try/catch and only destroy/send GameDestroyed when game is not running or has no players. Client: Add restart-game flow — expose restartGame on OnlineGame, propagate event from GameEndedMenu (adds local restarted state and disables button), and hook restart handling + player disconnect on unmount in OnlineMode. Also conditionally show bot switch in GameCreationMenu and include replayGameCode in GameEnded interface. Remove automatic reconnect on SignalR connection. Overall: Improves robustness around player disconnects and adds a UI/logic path for restarting games.
This commit is contained in:
@@ -131,27 +131,28 @@ public class GameManager(IGameRepository gameRepository, IHubContext<GameHubSock
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task DisconnectedPlayer(string playerConnectionId)
|
public async Task DisconnectedPlayer(string playerConnectionId)
|
||||||
{
|
{
|
||||||
var game = gameRepository.GetOneByConnectionId(playerConnectionId);
|
var game = gameRepository.GetOneByConnectionId(playerConnectionId);
|
||||||
if (game == null || game.State == GameState.Ended)
|
if (game == null || game.State == GameState.Ended)
|
||||||
return Task.CompletedTask;
|
return;
|
||||||
|
|
||||||
var player = game.GetPlayerByConnectionId(playerConnectionId);
|
var player = game.GetPlayerByConnectionId(playerConnectionId);
|
||||||
if (player == null)
|
if (player == null)
|
||||||
return Task.CompletedTask;
|
return;
|
||||||
|
|
||||||
game.RemovePlayer(playerConnectionId);
|
game.RemovePlayer(playerConnectionId);
|
||||||
game.EndGame();
|
game.EndGame();
|
||||||
hubContext.Clients.Group(game.Id).SendAsync("GameEnded", new
|
|
||||||
|
Console.WriteLine("Play has Disconnected");
|
||||||
|
|
||||||
|
await hubContext.Clients.Group(game.Id).SendAsync("GameEnded", new
|
||||||
{
|
{
|
||||||
Method = "PlayerDisconnected",
|
Method = "PlayerDisconnected",
|
||||||
Player = player
|
Player = player
|
||||||
});
|
});
|
||||||
|
|
||||||
ScheduleGameDeletion(game.Id, TimeSpan.FromSeconds(0));
|
ScheduleGameDeletion(game.Id, TimeSpan.FromSeconds(2));
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int[][] ConvertField(int[,]? field)
|
private static int[][] ConvertField(int[,]? field)
|
||||||
@@ -179,16 +180,27 @@ public class GameManager(IGameRepository gameRepository, IHubContext<GameHubSock
|
|||||||
private void ScheduleGameDeletion(string gameId, TimeSpan delay)
|
private void ScheduleGameDeletion(string gameId, TimeSpan delay)
|
||||||
{
|
{
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
await Task.Delay(delay);
|
await Task.Delay(delay);
|
||||||
|
|
||||||
var g = gameRepository.GetOne(gameId);
|
var g = gameRepository.GetOne(gameId);
|
||||||
if (g != null && g.State != GameState.Running)
|
if (g == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (g.State != GameState.Running || g.Players.Count == 0)
|
||||||
{
|
{
|
||||||
gameRepository.Destroy(gameId);
|
gameRepository.Destroy(gameId);
|
||||||
|
|
||||||
await hubContext.Clients.Group(gameId).SendAsync("GameDestroyed");
|
await hubContext.Clients.Group(gameId).SendAsync("GameDestroyed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"Scheduled deletion of game {gameId} executed. {gameRepository.GetAll().Count}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error deleting game {gameId}: {ex}");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ function gameFieldPreset(x: number, y: number) {
|
|||||||
required
|
required
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
<v-switch
|
<v-switch
|
||||||
|
v-if="settingsProp.botPlayer2 != null"
|
||||||
v-model="settingsProp.botPlayer2"
|
v-model="settingsProp.botPlayer2"
|
||||||
:label="`${settingsProp.playerName2} ist ein Bot`"
|
:label="`${settingsProp.playerName2} ist ein Bot`"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import type { GameEnded } from '@/scripts/logic/signalR/GameConnection';
|
import type { GameEnded } from '@/scripts/logic/signalR/GameConnection';
|
||||||
|
|
||||||
defineEmits(['restartGame']);
|
const emit = defineEmits(['restartGame']);
|
||||||
|
|
||||||
const props = defineProps<{ gameEndedInformation: GameEnded | null }>();
|
const props = defineProps<{ gameEndedInformation: GameEnded | null }>();
|
||||||
|
const restarted = ref<boolean>(false);
|
||||||
|
|
||||||
const message = computed(() => {
|
const message = computed(() => {
|
||||||
|
if(restarted.value) {
|
||||||
|
return 'Das Spiel wird neugestartet...';
|
||||||
|
}
|
||||||
switch (props.gameEndedInformation?.method) {
|
switch (props.gameEndedInformation?.method) {
|
||||||
case 'PlayerDisconnected':
|
case 'PlayerDisconnected':
|
||||||
return `Bei dem Spieler ${props.gameEndedInformation.player?.name} ist die Verbindung abgebrochen :(`;
|
return `Bei dem Spieler ${props.gameEndedInformation.player?.name} ist die Verbindung abgebrochen :(`;
|
||||||
@@ -18,6 +22,12 @@ const message = computed(() => {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function restartClicked() {
|
||||||
|
emit('restartGame');
|
||||||
|
|
||||||
|
restarted.value = true;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -28,7 +38,7 @@ const message = computed(() => {
|
|||||||
<v-divider class="mb-5 mt-5"></v-divider>
|
<v-divider class="mb-5 mt-5"></v-divider>
|
||||||
<div class="d-flex align-center justify-space-evenly ma-4 w-100">
|
<div class="d-flex align-center justify-space-evenly ma-4 w-100">
|
||||||
<v-btn color="red" @click="$router.push('/')" rounded="xl"> Abbrechen </v-btn>
|
<v-btn color="red" @click="$router.push('/')" rounded="xl"> Abbrechen </v-btn>
|
||||||
<v-btn color="primary" @click="$emit('restartGame')" rounded="xl"> Spiel Neustarten </v-btn>
|
<v-btn color="primary" @click="restartClicked()" rounded="xl" :disabled="restarted"> Spiel Neustarten </v-btn>
|
||||||
</div>
|
</div>
|
||||||
</v-sheet>
|
</v-sheet>
|
||||||
</v-container>
|
</v-container>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import type { GameSettings } from '@/scripts/interfaces/GameSettings';
|
|||||||
import type JoinGameObject from '@/scripts/interfaces/JoinGameObject';
|
import type JoinGameObject from '@/scripts/interfaces/JoinGameObject';
|
||||||
import OnlineGame from '@/scripts/logic/onlineMode/OnlineGame';
|
import OnlineGame from '@/scripts/logic/onlineMode/OnlineGame';
|
||||||
import type { GameEnded } from '@/scripts/logic/signalR/GameConnection';
|
import type { GameEnded } from '@/scripts/logic/signalR/GameConnection';
|
||||||
import { ref } from 'vue';
|
import { onUnmounted, ref } from 'vue';
|
||||||
|
|
||||||
enum CurrentState {
|
enum CurrentState {
|
||||||
CreateOrJoinSelection,
|
CreateOrJoinSelection,
|
||||||
@@ -63,6 +63,15 @@ async function createGame() {
|
|||||||
async function tryToJoin() {
|
async function tryToJoin() {
|
||||||
await game.value.joinGame(joiningModel.value);
|
await game.value.joinGame(joiningModel.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function restartGame() {
|
||||||
|
if(!gameEndedInformation.value?.replayGameCode) return;
|
||||||
|
await game.value.restartGame(gameEndedInformation.value.replayGameCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(async () => {
|
||||||
|
await game.value.player.disconnect();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -89,6 +98,7 @@ async function tryToJoin() {
|
|||||||
<GameEndedMenu
|
<GameEndedMenu
|
||||||
:game-ended-information="gameEndedInformation"
|
:game-ended-information="gameEndedInformation"
|
||||||
v-else-if="currentState === CurrentState.EndScreen"
|
v-else-if="currentState === CurrentState.EndScreen"
|
||||||
|
@restart-game="restartGame()"
|
||||||
></GameEndedMenu>
|
></GameEndedMenu>
|
||||||
|
|
||||||
<div id="game" v-else>
|
<div id="game" v-else>
|
||||||
|
|||||||
@@ -92,6 +92,16 @@ class OnlineGame {
|
|||||||
this.gameState?.currentTurn == 1 ? this.gameState.players[0]?.name : this.gameState?.players[1]?.name;
|
this.gameState?.currentTurn == 1 ? this.gameState.players[0]?.name : this.gameState?.players[1]?.name;
|
||||||
this.currentDescription = getRandomMovePhrase(playerName ?? "Spieler");
|
this.currentDescription = getRandomMovePhrase(playerName ?? "Spieler");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async restartGame(replayGameCode: any) {
|
||||||
|
const code = Number(replayGameCode.value); // Proxy → primitive number
|
||||||
|
await this.player.disconnect();
|
||||||
|
await this.joinGame({
|
||||||
|
gameCode: code.toString(),
|
||||||
|
playerName: this.player.playerName ?? 'Spieler',
|
||||||
|
failed: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default OnlineGame;
|
export default OnlineGame;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface Player {
|
|||||||
export interface GameEnded {
|
export interface GameEnded {
|
||||||
method: string;
|
method: string;
|
||||||
player?: Player | null;
|
player?: Player | null;
|
||||||
|
replayGameCode?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GameIdentifier {
|
export interface GameIdentifier {
|
||||||
@@ -41,7 +42,6 @@ class GameConnection {
|
|||||||
constructor() {
|
constructor() {
|
||||||
this.connection = new signalR.HubConnectionBuilder()
|
this.connection = new signalR.HubConnectionBuilder()
|
||||||
.withUrl('/api/gamehub')
|
.withUrl('/api/gamehub')
|
||||||
.withAutomaticReconnect()
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
this.connection.on('GameCreated', (payload: GameIdentifier) => {
|
this.connection.on('GameCreated', (payload: GameIdentifier) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user