Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -416,20 +416,23 @@ public async Task GetRoomInfoAsync_ReturnsNull_WhenApiFails()
public async Task GetRoomMembersAsync_ReturnsMembers_WhenSuccess()
{
// Arrange
var members = new[]
var membersResponse = new ChatRoomMembersResponse
{
new ChatRoomMemberInfoResponse { userId = "user-1", playerName = "Player1" },
new ChatRoomMemberInfoResponse { userId = "user-2", playerName = "Player2" },
members = new System.Collections.Generic.List<ChatRoomMemberInfoResponse>
{
new ChatRoomMemberInfoResponse { userId = "user-1", playerName = "Player1" },
new ChatRoomMemberInfoResponse { userId = "user-2", playerName = "Player2" },
}
};
_mockApiClient
.GetAsync<ChatRoomMemberInfoResponse[]>(
.GetAsync<ChatRoomMembersResponse>(
Arg.Is("/api/chat/rooms/room-1/members"),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>())
.Returns(UniTask.FromResult(new ApiResponse<ChatRoomMemberInfoResponse[]>
.Returns(UniTask.FromResult(new ApiResponse<ChatRoomMembersResponse>
{
IsSuccess = true,
Data = members,
Data = membersResponse,
}));

// Act
Expand All @@ -446,11 +449,11 @@ public async Task GetRoomMembersAsync_ReturnsEmpty_WhenApiFails()
{
// Arrange
_mockApiClient
.GetAsync<ChatRoomMemberInfoResponse[]>(
.GetAsync<ChatRoomMembersResponse>(
Arg.Any<string>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>())
.Returns(UniTask.FromResult(new ApiResponse<ChatRoomMemberInfoResponse[]>
.Returns(UniTask.FromResult(new ApiResponse<ChatRoomMembersResponse>
{
IsSuccess = false,
}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,15 @@ public void Events_CanBeSubscribedAndUnsubscribed()
var messageReceivedCalled = false;
var readyChangedCalled = false;
var gameStartingCalled = false;
var lobbyClosedCalled = false;
var disconnectedCalled = false;

void OnPlayerJoined(string a, string b) => playerJoinedCalled = true;
void OnPlayerLeft(string a, string b) => playerLeftCalled = true;
void OnMessageReceived(string a, string b, string c) => messageReceivedCalled = true;
void OnReadyChanged(string a, bool b) => readyChangedCalled = true;
void OnGameStarting(string a, string b, int c) => gameStartingCalled = true;
void OnLobbyClosed(string _) => lobbyClosedCalled = true;
void OnDisconnected(string _) => disconnectedCalled = true;

Assert.DoesNotThrow(() =>
Expand All @@ -115,13 +117,15 @@ public void Events_CanBeSubscribedAndUnsubscribed()
_client.OnMessageReceived += OnMessageReceived;
_client.OnPlayerReadyChanged += OnReadyChanged;
_client.OnGameStarting += OnGameStarting;
_client.OnLobbyClosed += OnLobbyClosed;
_client.OnDisconnected += OnDisconnected;

_client.OnPlayerJoined -= OnPlayerJoined;
_client.OnPlayerLeft -= OnPlayerLeft;
_client.OnMessageReceived -= OnMessageReceived;
_client.OnPlayerReadyChanged -= OnReadyChanged;
_client.OnGameStarting -= OnGameStarting;
_client.OnLobbyClosed -= OnLobbyClosed;
_client.OnDisconnected -= OnDisconnected;
});

Expand All @@ -130,6 +134,7 @@ public void Events_CanBeSubscribedAndUnsubscribed()
Assert.That(messageReceivedCalled, Is.False);
Assert.That(readyChangedCalled, Is.False);
Assert.That(gameStartingCalled, Is.False);
Assert.That(lobbyClosedCalled, Is.False);
Assert.That(disconnectedCalled, Is.False);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
using Game.MVP.Survivor.Root;
using Game.MVP.Survivor.SaveData;
using Game.MVP.Survivor.Scenes;
using Game.Shared;
using Game.Shared.SaveData;
using Game.Shared.Chat.Client;
using Game.Shared.Services;
using Game.Shared.Services.Network;
using Game.Shared.Services.Network.Queue;
Expand Down Expand Up @@ -38,6 +40,7 @@ public class SurvivorGameRunner : ISurvivorGameRunner
private readonly IAuthApiService _authApiService;
private readonly IRequestQueue _requestQueue;
private readonly INetworkService _networkService;
private readonly IChatClient _chatClient;

private GameObject _gameRootInstance;
private SurvivorGameRootController _gameRootController;
Expand All @@ -57,7 +60,8 @@ public SurvivorGameRunner(
IApiClient apiClient,
IAuthApiService authApiService,
IRequestQueue requestQueue,
INetworkService networkService)
INetworkService networkService,
IChatClient chatClient)
{
_container = container;
_sceneService = sceneService;
Expand All @@ -73,6 +77,7 @@ public SurvivorGameRunner(
_authApiService = authApiService;
_requestQueue = requestQueue;
_networkService = networkService;
_chatClient = chatClient;
}

public async UniTask StartupAsync()
Expand Down Expand Up @@ -103,13 +108,22 @@ public async UniTask StartupAsync()
await TryValidateTokenAsync();
}

// 5. 共通オブジェクト読み込み(カメラ、UIルートなど)
// 5. ChatClient SignalR 接続設定
var envConfig = GameEnvironmentHelper.CurrentConfig;
if (!string.IsNullOrEmpty(envConfig?.WebSocketUrl))
{
_chatClient.Configure(
envConfig.WebSocketUrl,
() => System.Threading.Tasks.Task.FromResult(_authSessionService.AuthToken ?? ""));
}

// 6. 共通オブジェクト読み込み(カメラ、UIルートなど)
await LoadGameRootControllerAsync();

// 6. リクエストキューの自動処理を設定
// 7. リクエストキューの自動処理を設定
SetupQueueProcessing();

// 7. 初期シーンへ遷移
// 8. 初期シーンへ遷移
await _sceneService.TransitionAsync<SurvivorTitleScene>();

Debug.Log("[SurvivorGameRunner] Game started");
Expand Down Expand Up @@ -190,6 +204,9 @@ public async UniTask ShutdownAsync()
_queueProcessingSubscription?.Dispose();
_queueProcessingSubscription = null;

// チャットクライアント切断
_chatClient?.Dispose();

// セーブデータ保存(変更がある場合のみ)
await _saveService.SaveIfDirtyAsync();
await _audioSaveService.SaveIfDirtyAsync();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Game.Library.Shared.Chat.Dto;
using Game.Shared.Dto.Chat;
Expand Down Expand Up @@ -109,9 +110,11 @@ public async Task<ChatRoomInfoResponse> GetRoomInfoAsync(string roomId)

public async Task<ChatRoomMemberInfoResponse[]> GetRoomMembersAsync(string roomId)
{
var response = await _apiClient.GetAsync<ChatRoomMemberInfoResponse[]>(
var response = await _apiClient.GetAsync<ChatRoomMembersResponse>(
$"/api/chat/rooms/{roomId}/members");
return response.IsSuccess ? response.Data : Array.Empty<ChatRoomMemberInfoResponse>();
return response.IsSuccess && response.Data?.members != null
? response.Data.members.ToArray()
: Array.Empty<ChatRoomMemberInfoResponse>();
}

// SignalR 操作
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ namespace Game.Shared.Chat.Client
/// </summary>
public interface IChatClient : IDisposable
{
/// <summary>
/// SignalR 接続設定(ConnectAsync の前に呼び出すこと)
/// </summary>
void Configure(string hubUrl, Func<Task<string>> accessTokenProvider);

// REST 操作

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;

namespace Game.Shared.Dto.Chat
{
Expand Down Expand Up @@ -67,4 +68,10 @@ public class ChatOperationResponse
{
public bool success;
}

[Serializable]
public class ChatRoomMembersResponse
{
public List<ChatRoomMemberInfoResponse> members;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ public interface ILobbyClient : IDisposable
/// </summary>
Task<LobbyInfo[]> SearchLobbiesAsync(string gameMode, int maxResults);

/// <summary>
/// ロビー情報取得(Unary のみ)
/// </summary>
Task<LobbyInfo> GetLobbyInfoAsync(string lobbyId);

/// <summary>
/// ロビーのプレイヤー一覧取得(Unary のみ)
/// </summary>
Task<LobbyPlayerInfo[]> GetLobbyPlayersAsync(string lobbyId);

/// <summary>
/// メッセージ送信(Hub)
/// </summary>
Expand Down Expand Up @@ -74,6 +84,11 @@ public interface ILobbyClient : IDisposable
/// </summary>
event Action<string, string, int> OnGameStarting;

/// <summary>
/// ロビー閉鎖イベント (reason)
/// </summary>
event Action<string> OnLobbyClosed;

/// <summary>
/// 予期しない切断イベント (reason)
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public class LobbyClient : ILobbyClient, ILobbyHubReceiver
public event Action<string, string, string> OnMessageReceived;
public event Action<string, bool> OnPlayerReadyChanged;
public event Action<string, string, int> OnGameStarting;
public event Action<string> OnLobbyClosed;
public event Action<string> OnDisconnected;

public LobbyClient(
Expand Down Expand Up @@ -141,6 +142,32 @@ public async Task<LobbyInfo[]> SearchLobbiesAsync(string gameMode, int maxResult
}
}

public async Task<LobbyInfo> GetLobbyInfoAsync(string lobbyId)
{
try
{
return await CreateService().GetLobbyInfoAsync(lobbyId);
}
catch (RpcException ex)
{
Debug.LogError($"[LobbyClient] RPC error in GetLobbyInfo: {ex.StatusCode}");
throw;
}
}

public async Task<LobbyPlayerInfo[]> GetLobbyPlayersAsync(string lobbyId)
{
try
{
return await CreateService().GetLobbyPlayersAsync(lobbyId);
}
catch (RpcException ex)
{
Debug.LogError($"[LobbyClient] RPC error in GetLobbyPlayers: {ex.StatusCode}");
return Array.Empty<LobbyPlayerInfo>();
}
}

public async Task SendMessageAsync(string message)
{
try
Expand Down Expand Up @@ -192,6 +219,7 @@ void ILobbyHubReceiver.OnMessageReceived(string userId, string playerName, strin
void ILobbyHubReceiver.OnLobbyClosed(string reason)
{
Debug.Log($"[LobbyClient] Lobby closed: {reason}");
OnLobbyClosed?.Invoke(reason);
}

void ILobbyHubReceiver.OnPlayerReadyChanged(string userId, bool isReady)
Expand Down
4 changes: 4 additions & 0 deletions src/Game.Realtime/Extensions/RealtimeServiceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ public static IServiceCollection AddRealtimeServices(
services.Configure<MatchmakingConfiguration>(
configuration.GetSection("Matchmaking"));

// Game Server Configuration
services.Configure<GameServerConfiguration>(
configuration.GetSection("GameServer"));

// Matchmaking Background Processor
services.AddHostedService<MatchmakingProcessor>();

Expand Down
50 changes: 49 additions & 1 deletion src/Game.Realtime/Hubs/LobbyHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Grpc.Core;
using MagicOnion.Server.Hubs;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;

namespace Game.Realtime.Hubs;

Expand All @@ -14,16 +15,24 @@ public class LobbyHub : StreamingHubBase<ILobbyHub, ILobbyHubReceiver>, ILobbyHu
{
private readonly ILogger<LobbyHub> _logger;
private readonly ILobbyDataService _lobbyDataService;
private readonly IMatchSessionTokenService _tokenService;
private readonly GameServerConfiguration _gameServerConfig;

private IGroup<ILobbyHubReceiver>? _currentGroup;
private string _userId = string.Empty;
private string _playerName = string.Empty;
private string _lobbyId = string.Empty;

public LobbyHub(ILogger<LobbyHub> logger, ILobbyDataService lobbyDataService)
public LobbyHub(
ILogger<LobbyHub> logger,
ILobbyDataService lobbyDataService,
IMatchSessionTokenService tokenService,
IOptions<GameServerConfiguration> gameServerConfig)
{
_logger = logger;
_lobbyDataService = lobbyDataService;
_tokenService = tokenService;
_gameServerConfig = gameServerConfig.Value;
}

public async ValueTask ConnectAsync(string lobbyId, string playerName)
Expand Down Expand Up @@ -51,6 +60,14 @@ public async ValueTask LeaveAsync()
_playerName, _userId, _lobbyId);

_currentGroup.All.OnPlayerLeft(_userId, _playerName);

// ホスト退出時はロビーを閉じる
var lobby = await _lobbyDataService.GetLobbyAsync(_lobbyId);
if (lobby != null && lobby.HostUserId == _userId)
{
_currentGroup.All.OnLobbyClosed("Host left");
}

await _currentGroup.RemoveAsync(Context);
_currentGroup = null;

Expand Down Expand Up @@ -83,16 +100,47 @@ public async ValueTask SetReadyAsync(bool isReady)
_currentGroup.All.OnPlayerReadyChanged(_userId, isReady);
}

// 全員 Ready チェック → ゲーム開始
if (isReady && _currentGroup != null && await _lobbyDataService.AreAllReadyAsync(_lobbyId))
{
await StartGameAsync();
}

_logger.LogDebug(
"Player {UserId} set ready={IsReady} in lobby {LobbyId}",
_userId, isReady, _lobbyId);
}

private async ValueTask StartGameAsync()
{
var matchId = Guid.NewGuid().ToString("N");
var players = await _lobbyDataService.GetPlayersAsync(_lobbyId);

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

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

_logger.LogInformation(
"Game starting from lobby {LobbyId}: match {MatchId} with {PlayerCount} players",
_lobbyId, matchId, players.Length);
}

protected override async ValueTask OnDisconnected()
{
if (_currentGroup != null)
{
_currentGroup.All.OnPlayerLeft(_userId, _playerName);

// ホスト退出時はロビーを閉じる
var lobby = await _lobbyDataService.GetLobbyAsync(_lobbyId);
if (lobby != null && lobby.HostUserId == _userId)
{
_currentGroup.All.OnLobbyClosed("Host disconnected");
}

await _currentGroup.RemoveAsync(Context);
_currentGroup = null;
}
Expand Down
Loading
Loading