Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ public async UniTask ShutdownAsync()
_queueProcessingSubscription = null;

// チャットクライアント切断
_chatClient?.Dispose();
if (_chatClient != null) { await _chatClient.DisconnectAsync(); }

// セーブデータ保存(変更がある場合のみ)
await _saveService.SaveIfDirtyAsync();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Game.Library.Shared.Dto;
Expand All @@ -18,6 +19,7 @@ namespace Game.Shared.Chat.Client
public class ChatClient : IChatClient
{
private readonly IApiClient _apiClient;
private readonly Dictionary<string, string> _joinedRooms = new();
private string _hubUrl;
private Func<Task<string>> _accessTokenProvider;
private HubConnection _hubConnection;
Expand Down Expand Up @@ -142,6 +144,7 @@ public async Task ConnectAsync()
.Build();

RegisterCallbacks();
RegisterReconnectHandler();

await _hubConnection.StartAsync();
Debug.Log("[ChatClient] Connected to SignalR chat hub");
Expand All @@ -151,13 +154,15 @@ public async Task JoinAsync(string roomId, string playerName)
{
EnsureConnected();
await _hubConnection.InvokeAsync("JoinAsync", roomId, playerName);
_joinedRooms[roomId] = playerName;
Debug.Log($"[ChatClient] Joined chat room: {roomId}");
}

public async Task LeaveAsync(string roomId)
{
EnsureConnected();
await _hubConnection.InvokeAsync("LeaveAsync", roomId);
_joinedRooms.Remove(roomId);
Debug.Log($"[ChatClient] Left chat room: {roomId}");
}

Expand All @@ -174,15 +179,40 @@ public async Task<ChatMessage[]> GetRecentMessagesAsync(string roomId, int count
"GetRecentMessagesAsync", roomId, count);
}

public async Task DisconnectAsync()
{
if (!_disposed)
{
_disposed = true;
_joinedRooms.Clear();
if (_hubConnection != null)
{
try
{
await _hubConnection.StopAsync();
await _hubConnection.DisposeAsync();
}
catch (Exception ex)
{
Debug.LogWarning($"[ChatClient] Disconnect error: {ex.Message}");
}
_hubConnection = null;
}
}
}

public void Dispose()
{
if (!_disposed)
{
_disposed = true;
_joinedRooms.Clear();
if (_hubConnection != null)
{
_hubConnection.StopAsync().GetAwaiter().GetResult();
_hubConnection.DisposeAsync().GetAwaiter().GetResult();
try { _hubConnection.StopAsync().GetAwaiter().GetResult(); }
catch (Exception ex) { Debug.LogWarning($"[ChatClient] Dispose Stop error: {ex.Message}"); }
try { _hubConnection.DisposeAsync().GetAwaiter().GetResult(); }
catch (Exception ex) { Debug.LogWarning($"[ChatClient] Dispose error: {ex.Message}"); }
_hubConnection = null;
}
}
Expand Down Expand Up @@ -222,6 +252,26 @@ private void RegisterCallbacks()
});
}

private void RegisterReconnectHandler()
{
_hubConnection.Reconnected += async _ =>
{
Debug.Log($"[ChatClient] Reconnected to SignalR hub, re-joining {_joinedRooms.Count} rooms");
foreach (var (roomId, playerName) in _joinedRooms)
{
try
{
await _hubConnection.InvokeAsync("JoinAsync", roomId, playerName);
Debug.Log($"[ChatClient] Re-joined room: {roomId}");
}
catch (Exception ex)
{
Debug.LogWarning($"[ChatClient] Failed to re-join room {roomId}: {ex.Message}");
}
}
};
}

private void EnsureConnected()
{
if (_hubConnection == null || _hubConnection.State != HubConnectionState.Connected)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ public interface IChatClient : IDisposable
/// </summary>
Task ConnectAsync();

/// <summary>
/// SignalR 接続を切断し、リソースを解放する(非同期)
/// </summary>
Task DisconnectAsync();

/// <summary>
/// チャットルームに参加
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Game.Library.Shared.Dto;
using Game.Library.Shared.Realtime.Hubs;
Expand All @@ -18,6 +19,7 @@ public class LobbyClient : ILobbyClient, ILobbyHubReceiver
private readonly AuthClientFilter _authFilter;
private readonly IClientFilter[] _filters;
private ILobbyHub _hub;
private CancellationTokenSource _monitorCts;
private string _currentLobbyId;
private bool _disposed;

Expand Down Expand Up @@ -94,8 +96,9 @@ public async Task ConnectToLobbyAsync(string lobbyId, string playerName)
_currentLobbyId = lobbyId;
Debug.Log($"[LobbyClient] Connected to lobby hub: {lobbyId}");

// 切断監視(fire-and-forget)
_ = MonitorDisconnectionAsync();
// 切断監視
_monitorCts = new CancellationTokenSource();
_ = MonitorDisconnectionAsync(_monitorCts.Token);
}
catch (RpcException ex)
{
Expand All @@ -108,6 +111,10 @@ public async Task LeaveLobbyAsync()
{
try
{
_monitorCts?.Cancel();
_monitorCts?.Dispose();
_monitorCts = null;

if (_hub != null)
{
await _hub.LeaveAsync();
Expand Down Expand Up @@ -234,12 +241,13 @@ void ILobbyHubReceiver.OnGameStarting(string matchId, string serverAddress, int
OnGameStarting?.Invoke(matchId, serverAddress, serverPort);
}

private async Task MonitorDisconnectionAsync()
private async Task MonitorDisconnectionAsync(CancellationToken cancellationToken)
{
try
{
if (_hub == null) return;
var reason = await _hub.WaitForDisconnectAsync();
if (cancellationToken.IsCancellationRequested) return;
if (reason.Type != DisconnectionType.CompletedNormally)
{
Debug.LogWarning($"[LobbyClient] Unexpected disconnect: {reason.Type}");
Expand All @@ -248,7 +256,10 @@ private async Task MonitorDisconnectionAsync()
}
catch (Exception ex)
{
Debug.LogWarning($"[LobbyClient] Disconnect monitor error: {ex.Message}");
if (!cancellationToken.IsCancellationRequested)
{
Debug.LogWarning($"[LobbyClient] Disconnect monitor error: {ex.Message}");
}
}
}

Expand All @@ -257,6 +268,9 @@ public void Dispose()
if (!_disposed)
{
_disposed = true;
_monitorCts?.Cancel();
_monitorCts?.Dispose();
_monitorCts = null;
if (_hub != null)
{
try { _hub.DisposeAsync().GetAwaiter().GetResult(); }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Game.Library.Shared.Dto;
using Game.Library.Shared.Realtime.Hubs;
Expand All @@ -18,6 +19,7 @@ public class MatchmakingClient : IMatchmakingClient, IMatchmakingHubReceiver
private readonly AuthClientFilter _authFilter;
private readonly IClientFilter[] _filters;
private IMatchmakingHub _hub;
private CancellationTokenSource _monitorCts;
private string _currentGameMode;
private bool _disposed;

Expand Down Expand Up @@ -72,8 +74,9 @@ public async Task<MatchmakingResponse> StartMatchmakingAsync(string gameMode)
_currentGameMode = gameMode;
IsSearching = true;

// 切断監視(fire-and-forget)
_ = MonitorDisconnectionAsync();
// 切断監視
_monitorCts = new CancellationTokenSource();
_ = MonitorDisconnectionAsync(_monitorCts.Token);

return response;
}
Expand All @@ -97,6 +100,10 @@ public async Task CancelMatchmakingAsync()
await CreateService().DequeueAsync(
new MatchmakingRequest { GameMode = _currentGameMode });

_monitorCts?.Cancel();
_monitorCts?.Dispose();
_monitorCts = null;

if (_hub != null)
{
await _hub.UnsubscribeAsync();
Expand Down Expand Up @@ -152,12 +159,13 @@ void IMatchmakingHubReceiver.OnQueueStatusUpdated(int playersInQueue)
OnQueueStatusUpdated?.Invoke(playersInQueue);
}

private async Task MonitorDisconnectionAsync()
private async Task MonitorDisconnectionAsync(CancellationToken cancellationToken)
{
try
{
if (_hub == null) return;
var reason = await _hub.WaitForDisconnectAsync();
if (cancellationToken.IsCancellationRequested) return;
if (reason.Type != DisconnectionType.CompletedNormally)
{
Debug.LogWarning($"[MatchmakingClient] Unexpected disconnect: {reason.Type}");
Expand All @@ -167,7 +175,10 @@ private async Task MonitorDisconnectionAsync()
}
catch (Exception ex)
{
Debug.LogWarning($"[MatchmakingClient] Disconnect monitor error: {ex.Message}");
if (!cancellationToken.IsCancellationRequested)
{
Debug.LogWarning($"[MatchmakingClient] Disconnect monitor error: {ex.Message}");
}
}
}

Expand All @@ -177,6 +188,9 @@ public void Dispose()
{
_disposed = true;
IsSearching = false;
_monitorCts?.Cancel();
_monitorCts?.Dispose();
_monitorCts = null;
if (_hub != null)
{
try { _hub.DisposeAsync().GetAwaiter().GetResult(); }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using Cysharp.Net.Http;
using Grpc.Net.Client;
using MagicOnion.Client;
Expand All @@ -20,7 +21,14 @@ public static void Initialize()
GrpcChannelProviderHost.Initialize(new DefaultGrpcChannelProvider(
() => new GrpcChannelOptions
{
HttpHandler = new YetAnotherHttpHandler { Http2Only = true },
HttpHandler = new YetAnotherHttpHandler
{
Http2Only = true,
ConnectTimeout = TimeSpan.FromSeconds(10),
Http2KeepAliveInterval = TimeSpan.FromSeconds(30),
Http2KeepAliveTimeout = TimeSpan.FromSeconds(10),
Http2KeepAliveWhileIdle = true,
},
DisposeHttpClient = true,
}));

Expand Down
10 changes: 8 additions & 2 deletions src/Game.Realtime/Hubs/LobbyHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,15 +127,21 @@ public async ValueTask SetReadyAsync(bool isReady)

private async ValueTask StartGameAsync()
{
var matchId = Guid.NewGuid().ToString("N");
var players = await _lobbyDataService.GetPlayersAsync(_lobbyId);
if (players.Length == 0 || _currentGroup == null)
{
_logger.LogWarning("StartGameAsync aborted: lobby {LobbyId} has no players or group is null", _lobbyId);
return;
}

var matchId = Guid.NewGuid().ToString("N");

foreach (var player in players)
{
await _tokenService.IssueTokenAsync(player.UserId, matchId);
}

_currentGroup!.All.OnGameStarting(matchId, _gameServerConfig.ServerAddress, _gameServerConfig.ServerPort);
_currentGroup.All.OnGameStarting(matchId, _gameServerConfig.ServerAddress, _gameServerConfig.ServerPort);

_logger.LogInformation(
"Game starting from lobby {LobbyId}: match {MatchId} with {PlayerCount} players",
Expand Down
24 changes: 8 additions & 16 deletions src/Game.Realtime/Hubs/MatchmakingHub.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Text.Json;
using Game.Library.Shared.Realtime.Hubs;
using Game.Realtime.Validation;
using Game.Server.Shared.Extensions;
Expand Down Expand Up @@ -48,24 +47,17 @@ public async ValueTask SubscribeAsync(string gameMode)
var channel = RedisChannel.Literal($"matchmaking:notify:{_userId}");
await _subscriber.SubscribeAsync(channel, (_, message) =>
{
try
var result = JsonHelper.TryDeserialize<MatchResult>(message.ToString(), _logger, $"match result for user {_userId}");
if (result != null)
{
var result = JsonSerializer.Deserialize<MatchResult>(message.ToString());
if (result != null)
try
{
try
{
Client.OnMatchFound(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send match notification to user {UserId}", _userId);
}
Client.OnMatchFound(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send match notification to user {UserId}", _userId);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to deserialize match result for user {UserId}", _userId);
}
});

Expand Down
Loading
Loading