diff --git a/Assets/Infrastructure/Network/Configs.meta b/Assets/Infrastructure/Network/Configs.meta new file mode 100644 index 0000000..b8432f4 --- /dev/null +++ b/Assets/Infrastructure/Network/Configs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: fd48a88d389f79249986de098143c22f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Infrastructure/Network/Configs/NetworkConfig.cs b/Assets/Infrastructure/Network/Configs/NetworkConfig.cs new file mode 100644 index 0000000..e69bdde --- /dev/null +++ b/Assets/Infrastructure/Network/Configs/NetworkConfig.cs @@ -0,0 +1,404 @@ +using UnityEngine; + +namespace ProjectVG.Infrastructure.Network.Configs +{ + /// + /// Unity 표준 방식의 네트워크 설정 ScriptableObject + /// Editor에서 설정 가능하고, 런타임에서는 정적 접근자로 사용 + /// + [CreateAssetMenu(fileName = "NetworkConfig", menuName = "ProjectVG/Network/NetworkConfig")] + public class NetworkConfig : ScriptableObject + { + [Header("Environment Settings")] + [SerializeField] private EnvironmentType environment = EnvironmentType.Development; + + [Header("Server Addresses")] + [SerializeField] private string developmentServer = "localhost:7900"; + [SerializeField] private string testServer = "localhost:7900"; + [SerializeField] private string productionServer = "122.153.130.223:7900"; + + [Header("HTTP API Settings")] + [SerializeField] private string apiVersion = "v1"; + [SerializeField] private string apiPath = "api"; + [SerializeField] private float httpTimeout = 30f; + [SerializeField] private int maxRetryCount = 3; + [SerializeField] private float retryDelay = 1f; + + [Header("WebSocket Settings")] + [SerializeField] private string wsPath = "ws"; + [SerializeField] private float wsTimeout = 30f; + [SerializeField] private float reconnectDelay = 5f; + [SerializeField] private int maxReconnectAttempts = 3; + [SerializeField] private bool autoReconnect = true; + [SerializeField] private float heartbeatInterval = 30f; + [SerializeField] private bool enableHeartbeat = true; + [SerializeField] private int maxMessageSize = 65536; // 64KB + [SerializeField] private float messageTimeout = 10f; + [SerializeField] private bool enableMessageLogging = true; + [SerializeField] private string wsMessageType = "json"; // "json" 또는 "binary" + + [Header("Common Settings")] + [SerializeField] private string userAgent = "ProjectVG-Client/1.0"; + [SerializeField] private string contentType = "application/json"; + + // Environment enum + public enum EnvironmentType + { + Development, + Test, + Production + } + + // Singleton instance + private static NetworkConfig _instance; + public static NetworkConfig Instance + { + get + { + if (_instance == null) + { + _instance = Resources.Load("NetworkConfig"); + if (_instance == null) + { + Debug.LogError("NetworkConfig를 찾을 수 없습니다. Resources 폴더에 NetworkConfig.asset 파일을 생성하세요."); + _instance = CreateDefaultInstance(); + } + } + return _instance; + } + } + + // Properties + public EnvironmentType Environment => environment; + public string ApiPath => apiPath; + public string WsPath => wsPath; + + + + #region Environment Configuration + + + + #endregion + + #region Static Accessors (편의 메서드들) + + /// + /// 현재 환경 + /// + public static EnvironmentType CurrentEnvironment => Instance.Environment; + + /// + /// HTTP 서버 주소 + /// + public static string HttpServerAddress + { + get + { + string server; + switch (Instance.environment) + { + case EnvironmentType.Development: + server = Instance.developmentServer; + break; + case EnvironmentType.Test: + server = Instance.testServer; + break; + case EnvironmentType.Production: + server = Instance.productionServer; + break; + default: + server = Instance.developmentServer; + break; + } + return $"http://{server}"; + } + } + + /// + /// WebSocket 서버 주소 + /// + public static string WebSocketServerAddress + { + get + { + string server; + switch (Instance.environment) + { + case EnvironmentType.Development: + server = Instance.developmentServer; + break; + case EnvironmentType.Test: + server = Instance.testServer; + break; + case EnvironmentType.Production: + server = Instance.productionServer; + break; + default: + server = Instance.developmentServer; + break; + } + return $"ws://{server}"; + } + } + + /// + /// API 버전 + /// + public static string ApiVersion => Instance.apiVersion; + + /// + /// HTTP 타임아웃 + /// + public static float HttpTimeout => Instance.httpTimeout; + + /// + /// 최대 재시도 횟수 + /// + public static int MaxRetryCount => Instance.maxRetryCount; + + /// + /// 재시도 지연 시간 + /// + public static float RetryDelay => Instance.retryDelay; + + /// + /// WebSocket 타임아웃 + /// + public static float WebSocketTimeout => Instance.wsTimeout; + + /// + /// 재연결 지연 시간 + /// + public static float ReconnectDelay => Instance.reconnectDelay; + + /// + /// 최대 재연결 시도 횟수 + /// + public static int MaxReconnectAttempts => Instance.maxReconnectAttempts; + + /// + /// 자동 재연결 + /// + public static bool AutoReconnect => Instance.autoReconnect; + + /// + /// 하트비트 간격 + /// + public static float HeartbeatInterval => Instance.heartbeatInterval; + + /// + /// 하트비트 활성화 + /// + public static bool EnableHeartbeat => Instance.enableHeartbeat; + + /// + /// 최대 메시지 크기 + /// + public static int MaxMessageSize => Instance.maxMessageSize; + + /// + /// 메시지 타임아웃 + /// + public static float MessageTimeout => Instance.messageTimeout; + + /// + /// 메시지 로깅 활성화 + /// + public static bool EnableMessageLogging => Instance.enableMessageLogging; + + /// + /// WebSocket 메시지 타입 + /// + public static string WebSocketMessageType => Instance.wsMessageType; + + /// + /// JSON 메시지 타입인지 확인 + /// + public static bool IsJsonMessageType => Instance.wsMessageType?.ToLower() == "json"; + + /// + /// 바이너리 메시지 타입인지 확인 + /// + public static bool IsBinaryMessageType => Instance.wsMessageType?.ToLower() == "binary"; + + /// + /// 사용자 에이전트 + /// + public static string UserAgent => Instance.userAgent; + + /// + /// 콘텐츠 타입 + /// + public static string ContentType => Instance.contentType; + + /// + /// 전체 API URL 생성 + /// + public static string GetFullApiUrl(string endpoint) + { + var baseUrl = HttpServerAddress; + return $"{baseUrl.TrimEnd('/')}/{Instance.apiPath.TrimStart('/').TrimEnd('/')}/{Instance.apiVersion.TrimStart('/').TrimEnd('/')}/{endpoint.TrimStart('/')}"; + } + + /// + /// 사용자 API URL + /// + public static string GetUserApiUrl(string path = "") + { + return GetFullApiUrl($"users/{path.TrimStart('/')}"); + } + + /// + /// 캐릭터 API URL + /// + public static string GetCharacterApiUrl(string path = "") + { + return GetFullApiUrl($"characters/{path.TrimStart('/')}"); + } + + /// + /// 대화 API URL + /// + public static string GetConversationApiUrl(string path = "") + { + return GetFullApiUrl($"conversations/{path.TrimStart('/')}"); + } + + /// + /// 인증 API URL + /// + public static string GetAuthApiUrl(string path = "") + { + return GetFullApiUrl($"auth/{path.TrimStart('/')}"); + } + + /// + /// WebSocket URL + /// + public static string GetWebSocketUrl() + { + var baseUrl = WebSocketServerAddress; + return $"{baseUrl.TrimEnd('/')}/{Instance.wsPath.TrimStart('/').TrimEnd('/')}"; + } + + /// + /// 버전이 포함된 WebSocket URL + /// + public static string GetWebSocketUrlWithVersion() + { + var baseUrl = WebSocketServerAddress; + return $"{baseUrl.TrimEnd('/')}/api/{Instance.apiVersion.TrimStart('/').TrimEnd('/')}/{Instance.wsPath.TrimStart('/').TrimEnd('/')}"; + } + + /// + /// 세션이 포함된 WebSocket URL + /// + public static string GetWebSocketUrlWithSession(string sessionId) + { + var baseWsUrl = GetWebSocketUrlWithVersion(); + return $"{baseWsUrl}?sessionId={sessionId}"; + } + + + /// + /// 개발 환경 설정 + /// + public static void SetDevelopmentEnvironment() + { + if (Application.isPlaying) + { + Debug.LogWarning("런타임 중에는 환경 설정을 변경할 수 없습니다."); + return; + } + + Instance.environment = EnvironmentType.Development; + } + + /// + /// 테스트 환경 설정 + /// + public static void SetTestEnvironment() + { + if (Application.isPlaying) + { + Debug.LogWarning("런타임 중에는 환경 설정을 변경할 수 없습니다."); + return; + } + + Instance.environment = EnvironmentType.Test; + } + + /// + /// 프로덕션 환경 설정 + /// + public static void SetProductionEnvironment() + { + if (Application.isPlaying) + { + Debug.LogWarning("런타임 중에는 환경 설정을 변경할 수 없습니다."); + return; + } + + Instance.environment = EnvironmentType.Production; + } + + /// + /// 현재 설정 로그 출력 + /// + public static void LogCurrentSettings() + { + Debug.Log($"=== NetworkConfig 현재 설정 ==="); + Debug.Log($"환경: {CurrentEnvironment}"); + Debug.Log($"HTTP 서버: {HttpServerAddress}"); + Debug.Log($"WebSocket 서버: {WebSocketServerAddress}"); + Debug.Log($"API 버전: {ApiVersion}"); + Debug.Log($"HTTP 타임아웃: {HttpTimeout}s"); + Debug.Log($"WebSocket 타임아웃: {WebSocketTimeout}s"); + Debug.Log($"자동 재연결: {AutoReconnect}"); + Debug.Log($"하트비트: {EnableHeartbeat} ({HeartbeatInterval}s)"); + Debug.Log($"================================"); + } + + #endregion + + #region Private Methods + + /// + /// 기본 인스턴스 생성 (Resources 폴더에 파일이 없을 때) + /// + private static NetworkConfig CreateDefaultInstance() + { + var instance = CreateInstance(); + + // 기본 설정 + instance.environment = EnvironmentType.Development; + instance.developmentServer = "localhost:7900"; + instance.testServer = "localhost:7900"; + instance.productionServer = "122.153.130.223:7900"; + instance.apiVersion = "v1"; + instance.apiPath = "api"; + instance.httpTimeout = 30f; + instance.maxRetryCount = 3; + instance.retryDelay = 1f; + instance.wsPath = "ws"; + instance.wsTimeout = 30f; + instance.reconnectDelay = 5f; + instance.maxReconnectAttempts = 3; + instance.autoReconnect = true; + instance.heartbeatInterval = 30f; + instance.enableHeartbeat = true; + instance.maxMessageSize = 65536; + instance.messageTimeout = 10f; + instance.enableMessageLogging = true; + instance.userAgent = "ProjectVG-Client/1.0"; + instance.contentType = "application/json"; + + Debug.LogWarning("기본 NetworkConfig를 생성했습니다. Resources 폴더에 NetworkConfig.asset 파일을 생성하는 것을 권장합니다."); + + return instance; + } + + #endregion + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Configs/NetworkConfig.cs.meta b/Assets/Infrastructure/Network/Configs/NetworkConfig.cs.meta new file mode 100644 index 0000000..233db0c --- /dev/null +++ b/Assets/Infrastructure/Network/Configs/NetworkConfig.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 86f34d98107a627428323909bc15f04d \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Configs/ServerConfig.cs b/Assets/Infrastructure/Network/Configs/ServerConfig.cs new file mode 100644 index 0000000..8ea77ca --- /dev/null +++ b/Assets/Infrastructure/Network/Configs/ServerConfig.cs @@ -0,0 +1,59 @@ +namespace ProjectVG.Infrastructure.Network.Configs +{ + /// + /// 서버 설정 정보 (현재 미사용) + /// 서버의 메시지 형식과 지원 기능을 정의합니다. + /// WebSocket뿐만 아니라 다른 서버 설정도 포함할 수 있습니다. + /// + [System.Serializable] + public class ServerConfig + { + /// + /// 메시지 타입 ("json" 또는 "binary") + /// + public string messageType; + + /// + /// 서버 버전 + /// + public string version; + + /// + /// 오디오 지원 여부 + /// + public bool supportsAudio; + + /// + /// 바이너리 메시지 지원 여부 + /// + public bool supportsBinary; + + /// + /// 오디오 형식 (예: "wav", "mp3") + /// + public string audioFormat; + + /// + /// 최대 메시지 크기 (바이트) + /// + public int maxMessageSize; + + /// + /// JSON 형식인지 확인 + /// + public bool IsJsonFormat => messageType?.ToLower() == "json"; + + /// + /// 바이너리 형식인지 확인 + /// + public bool IsBinaryFormat => messageType?.ToLower() == "binary"; + + /// + /// 설정 정보를 문자열로 반환 + /// + public override string ToString() + { + return $"ServerConfig[Type: {messageType}, Version: {version}, Audio: {supportsAudio}, Binary: {supportsBinary}, Format: {audioFormat}, MaxSize: {maxMessageSize} bytes]"; + } + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Configs/ServerConfig.cs.meta b/Assets/Infrastructure/Network/Configs/ServerConfig.cs.meta new file mode 100644 index 0000000..d2afddc --- /dev/null +++ b/Assets/Infrastructure/Network/Configs/ServerConfig.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: aed6fafe9b2afc74c800a2a8ba99570a \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Configs/ServerConfigLoader.cs b/Assets/Infrastructure/Network/Configs/ServerConfigLoader.cs new file mode 100644 index 0000000..d5a07db --- /dev/null +++ b/Assets/Infrastructure/Network/Configs/ServerConfigLoader.cs @@ -0,0 +1,111 @@ +using System; +using UnityEngine; +using UnityEngine.Networking; +using Cysharp.Threading.Tasks; +using ProjectVG.Infrastructure.Network.Configs; + +namespace ProjectVG.Infrastructure.Network.Configs +{ + /// + /// 서버 설정 로더 (현재 미사용) + /// 서버에서 메시지 타입 등의 설정을 동적으로 로드합니다. + /// WebSocket뿐만 아니라 다른 서버 설정도 로드할 수 있습니다. + /// + public static class ServerConfigLoader + { + /// + /// 서버 설정 로드 + /// + /// 설정 엔드포인트 (기본값: "config") + /// 서버 설정 또는 null + public static async UniTask LoadServerConfigAsync(string configEndpoint = "config") + { + try + { + Debug.Log($"서버 설정 로드 중... (엔드포인트: {configEndpoint})"); + + var configUrl = NetworkConfig.GetFullApiUrl(configEndpoint); + using (var request = UnityWebRequest.Get(configUrl)) + { + request.timeout = 10; + await request.SendWebRequest(); + + if (request.result == UnityWebRequest.Result.Success) + { + var jsonResponse = request.downloadHandler.text; + var serverConfig = JsonUtility.FromJson(jsonResponse); + + Debug.Log($"서버 설정 로드 완료: {serverConfig.messageType}"); + return serverConfig; + } + else + { + Debug.LogWarning($"서버 설정 로드 실패: {request.error}"); + return null; + } + } + } + catch (Exception ex) + { + Debug.LogError($"서버 설정 로드 중 오류: {ex.Message}"); + return null; + } + } + + /// + /// 서버 설정 유효성 검사 + /// + /// 서버 설정 + /// 유효한지 여부 + public static bool ValidateServerConfig(ServerConfig config) + { + if (config == null) + { + Debug.LogWarning("서버 설정이 null입니다."); + return false; + } + + if (string.IsNullOrEmpty(config.messageType)) + { + Debug.LogWarning("서버 설정에 메시지 타입이 없습니다."); + return false; + } + + var messageType = config.messageType.ToLower(); + if (messageType != "json" && messageType != "binary") + { + Debug.LogWarning($"지원하지 않는 메시지 타입: {config.messageType}"); + return false; + } + + Debug.Log($"서버 설정 유효성 검사 통과: {config.messageType}"); + return true; + } + + /// + /// 서버 설정과 NetworkConfig 비교 + /// + /// 서버 설정 + /// 일치하는지 여부 + public static bool CompareWithNetworkConfig(ServerConfig serverConfig) + { + if (serverConfig == null) return false; + + var networkConfigType = NetworkConfig.WebSocketMessageType.ToLower(); + var serverConfigType = serverConfig.messageType.ToLower(); + + var isMatch = networkConfigType == serverConfigType; + + if (!isMatch) + { + Debug.LogWarning($"NetworkConfig({networkConfigType})와 서버 설정({serverConfigType})이 일치하지 않습니다."); + } + else + { + Debug.Log($"NetworkConfig와 서버 설정이 일치합니다: {networkConfigType}"); + } + + return isMatch; + } + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Configs/ServerConfigLoader.cs.meta b/Assets/Infrastructure/Network/Configs/ServerConfigLoader.cs.meta new file mode 100644 index 0000000..c972348 --- /dev/null +++ b/Assets/Infrastructure/Network/Configs/ServerConfigLoader.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 709b436d429928c4c8da5256aad82347 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/DTOs.meta b/Assets/Infrastructure/Network/DTOs.meta new file mode 100644 index 0000000..08d2e6f --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 195135de6dfc91948b7c869169221d53 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Infrastructure/Network/DTOs/BaseApiResponse.cs b/Assets/Infrastructure/Network/DTOs/BaseApiResponse.cs new file mode 100644 index 0000000..cc8bb20 --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/BaseApiResponse.cs @@ -0,0 +1,60 @@ +using System; + +namespace ProjectVG.Infrastructure.Network.DTOs +{ + /// + /// API 응답의 기본 구조 + /// + [Serializable] + public class BaseApiResponse + { + public bool success; + public string message; + public long timestamp; + public string requestId; + } + + /// + /// 데이터를 포함하는 API 응답 + /// + [Serializable] + public class ApiResponse : BaseApiResponse + { + public T data; + } + + /// + /// 페이지네이션 정보 + /// + [Serializable] + public class PaginationInfo + { + public int page; + public int limit; + public int total; + public int totalPages; + public bool hasNext; + public bool hasPrev; + } + + /// + /// 페이지네이션된 API 응답 + /// + [Serializable] + public class PaginatedApiResponse : BaseApiResponse + { + public T[] data; + public PaginationInfo pagination; + } + + /// + /// 에러 응답 + /// + [Serializable] + public class ErrorResponse : BaseApiResponse + { + public string errorCode; + public string errorType; + public string[] details; + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/DTOs/BaseApiResponse.cs.meta b/Assets/Infrastructure/Network/DTOs/BaseApiResponse.cs.meta new file mode 100644 index 0000000..7c3ecc0 --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/BaseApiResponse.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 359796341ec306b42b9219b5fff1feca \ No newline at end of file diff --git a/Assets/Infrastructure/Network/DTOs/Character.meta b/Assets/Infrastructure/Network/DTOs/Character.meta new file mode 100644 index 0000000..1705825 --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/Character.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6c376144b261f82498097408eb9c393f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Infrastructure/Network/DTOs/Character/CharacterInfo.cs b/Assets/Infrastructure/Network/DTOs/Character/CharacterInfo.cs new file mode 100644 index 0000000..3db5e85 --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/Character/CharacterInfo.cs @@ -0,0 +1,18 @@ +using System; +using UnityEngine; + +namespace ProjectVG.Infrastructure.Network.DTOs.Character +{ + /// + /// 캐릭터 정보 DTO + /// + [Serializable] + public class CharacterData + { + [SerializeField] public string id; + [SerializeField] public string name; + [SerializeField] public string description; + [SerializeField] public string role; + [SerializeField] public bool isActive; + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/DTOs/Character/CharacterInfo.cs.meta b/Assets/Infrastructure/Network/DTOs/Character/CharacterInfo.cs.meta new file mode 100644 index 0000000..28feae1 --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/Character/CharacterInfo.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4442e4d5ae93e4f4aa37f85e1a2eef48 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/DTOs/Character/CreateCharacterRequest.cs b/Assets/Infrastructure/Network/DTOs/Character/CreateCharacterRequest.cs new file mode 100644 index 0000000..45375d8 --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/Character/CreateCharacterRequest.cs @@ -0,0 +1,16 @@ +using System; + +namespace ProjectVG.Infrastructure.Network.DTOs.Character +{ + /// + /// 캐릭터 생성 요청 DTO + /// + [Serializable] + public class CreateCharacterRequest + { + public string name; + public string description; + public string role; + public bool isActive = true; + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/DTOs/Character/CreateCharacterRequest.cs.meta b/Assets/Infrastructure/Network/DTOs/Character/CreateCharacterRequest.cs.meta new file mode 100644 index 0000000..2cc318b --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/Character/CreateCharacterRequest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8934f22c6576c584c9a76cd0ca57a5fe \ No newline at end of file diff --git a/Assets/Infrastructure/Network/DTOs/Character/UpdateCharacterRequest.cs b/Assets/Infrastructure/Network/DTOs/Character/UpdateCharacterRequest.cs new file mode 100644 index 0000000..2cf7044 --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/Character/UpdateCharacterRequest.cs @@ -0,0 +1,16 @@ +using System; + +namespace ProjectVG.Infrastructure.Network.DTOs.Character +{ + /// + /// 캐릭터 수정 요청 DTO + /// + [Serializable] + public class UpdateCharacterRequest + { + public string name; + public string description; + public string role; + public bool isActive; + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/DTOs/Character/UpdateCharacterRequest.cs.meta b/Assets/Infrastructure/Network/DTOs/Character/UpdateCharacterRequest.cs.meta new file mode 100644 index 0000000..51be348 --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/Character/UpdateCharacterRequest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 88a11fb1cb2ce8f46a6fd6df981f1854 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/DTOs/Chat.meta b/Assets/Infrastructure/Network/DTOs/Chat.meta new file mode 100644 index 0000000..733db01 --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/Chat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 45b3af16b93a1e44399f346aed069e31 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Infrastructure/Network/DTOs/Chat/ChatRequest.cs b/Assets/Infrastructure/Network/DTOs/Chat/ChatRequest.cs new file mode 100644 index 0000000..247f357 --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/Chat/ChatRequest.cs @@ -0,0 +1,37 @@ +using System; +using UnityEngine; +using Newtonsoft.Json; + +namespace ProjectVG.Infrastructure.Network.DTOs.Chat +{ + /// + /// 채팅 요청 DTO (Newtonsoft.Json을 사용하여 snake_case 지원) + /// + [Serializable] + public class ChatRequest + { + [JsonProperty("session_id")] + [SerializeField] public string sessionId; + + [JsonProperty("message")] + [SerializeField] public string message; + + [JsonProperty("character_id")] + [SerializeField] public string characterId; + + [JsonProperty("user_id")] + [SerializeField] public string userId; + + [JsonProperty("action")] + [SerializeField] public string action = "chat"; + + [JsonProperty("actor")] + [SerializeField] public string actor; + + [JsonProperty("instruction")] + [SerializeField] public string instruction; + + [JsonProperty("requested_at")] + [SerializeField] public string requestedAt; + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/DTOs/Chat/ChatRequest.cs.meta b/Assets/Infrastructure/Network/DTOs/Chat/ChatRequest.cs.meta new file mode 100644 index 0000000..48b9b22 --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/Chat/ChatRequest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 508dce3896fd1a24dbb39ec613bac2df \ No newline at end of file diff --git a/Assets/Infrastructure/Network/DTOs/Chat/ChatResponse.cs b/Assets/Infrastructure/Network/DTOs/Chat/ChatResponse.cs new file mode 100644 index 0000000..609dafb --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/Chat/ChatResponse.cs @@ -0,0 +1,15 @@ +using System; + +namespace ProjectVG.Infrastructure.Network.DTOs.Chat +{ + /// + /// 채팅 응답 DTO + /// + [Serializable] + public class ChatResponse + { + public bool success; + public string message; + public string sessionId; + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/DTOs/Chat/ChatResponse.cs.meta b/Assets/Infrastructure/Network/DTOs/Chat/ChatResponse.cs.meta new file mode 100644 index 0000000..956fddd --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/Chat/ChatResponse.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f55f8bd1f455ad54caaea20c4fc09253 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/DTOs/WebSocket.meta b/Assets/Infrastructure/Network/DTOs/WebSocket.meta new file mode 100644 index 0000000..461d67b --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/WebSocket.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 96cf5f40d2d8aab478c179145464c91b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Infrastructure/Network/Http.meta b/Assets/Infrastructure/Network/Http.meta new file mode 100644 index 0000000..e5bdd72 --- /dev/null +++ b/Assets/Infrastructure/Network/Http.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a27aa8428fdf0974990bab2179bf081f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Infrastructure/Network/Http/HttpApiClient.cs b/Assets/Infrastructure/Network/Http/HttpApiClient.cs new file mode 100644 index 0000000..9983bfa --- /dev/null +++ b/Assets/Infrastructure/Network/Http/HttpApiClient.cs @@ -0,0 +1,386 @@ +using System; +using System.Collections.Generic; +using System.Text; +using UnityEngine; +using UnityEngine.Networking; +using Cysharp.Threading.Tasks; +using System.Threading; +using ProjectVG.Infrastructure.Network.Configs; +using Newtonsoft.Json; + +namespace ProjectVG.Infrastructure.Network.Http +{ + /// + /// HTTP API 클라이언트 + /// UnityWebRequest를 사용하여 서버와 통신하며, UniTask 기반 비동기 처리를 지원합니다. + /// + public class HttpApiClient : MonoBehaviour + { + [Header("API Configuration")] + // NetworkConfig를 사용하여 설정을 관리합니다. + + private const string ACCEPT_HEADER = "application/json"; + private const string AUTHORIZATION_HEADER = "Authorization"; + private const string BEARER_PREFIX = "Bearer "; + + private readonly Dictionary defaultHeaders = new Dictionary(); + private CancellationTokenSource cancellationTokenSource; + + public static HttpApiClient Instance { get; private set; } + + #region Unity Lifecycle + + private void Awake() + { + InitializeSingleton(); + } + + private void OnDestroy() + { + Cleanup(); + } + + #endregion + + #region Public API + + + + public void AddDefaultHeader(string key, string value) + { + defaultHeaders[key] = value; + } + + public void SetAuthToken(string token) + { + AddDefaultHeader(AUTHORIZATION_HEADER, $"{BEARER_PREFIX}{token}"); + } + + public async UniTask GetAsync(string endpoint, Dictionary headers = null, CancellationToken cancellationToken = default) + { + var url = GetFullUrl(endpoint); + return await SendRequestAsync(url, UnityWebRequest.kHttpVerbGET, null, headers, cancellationToken); + } + + public async UniTask PostAsync(string endpoint, object data = null, Dictionary headers = null, CancellationToken cancellationToken = default) + { + var url = GetFullUrl(endpoint); + var jsonData = SerializeData(data); + LogRequestDetails("POST", url, jsonData); + return await SendRequestAsync(url, UnityWebRequest.kHttpVerbPOST, jsonData, headers, cancellationToken); + } + + public async UniTask PutAsync(string endpoint, object data = null, Dictionary headers = null, CancellationToken cancellationToken = default) + { + var url = GetFullUrl(endpoint); + var jsonData = SerializeData(data); + return await SendRequestAsync(url, UnityWebRequest.kHttpVerbPUT, jsonData, headers, cancellationToken); + } + + public async UniTask DeleteAsync(string endpoint, Dictionary headers = null, CancellationToken cancellationToken = default) + { + var url = GetFullUrl(endpoint); + return await SendRequestAsync(url, UnityWebRequest.kHttpVerbDELETE, null, headers, cancellationToken); + } + + public async UniTask UploadFileAsync(string endpoint, byte[] fileData, string fileName, string fieldName = "file", Dictionary headers = null, CancellationToken cancellationToken = default) + { + var url = GetFullUrl(endpoint); + return await SendFileRequestAsync(url, fileData, fileName, fieldName, headers, cancellationToken); + } + + public void Shutdown() + { + cancellationTokenSource?.Cancel(); + } + + #endregion + + #region Private Methods + + private void InitializeSingleton() + { + if (Instance == null) + { + Instance = this; + DontDestroyOnLoad(gameObject); + InitializeClient(); + } + else + { + Destroy(gameObject); + } + } + + private void InitializeClient() + { + cancellationTokenSource = new CancellationTokenSource(); + ApplyNetworkConfig(); + SetupDefaultHeaders(); + } + + private void ApplyNetworkConfig() + { + Debug.Log($"NetworkConfig 적용: {NetworkConfig.CurrentEnvironment} 환경"); + } + + private void SetupDefaultHeaders() + { + defaultHeaders.Clear(); + defaultHeaders["Content-Type"] = NetworkConfig.ContentType; + defaultHeaders["User-Agent"] = NetworkConfig.UserAgent; + defaultHeaders["Accept"] = ACCEPT_HEADER; + } + + private string GetFullUrl(string endpoint) + { + return NetworkConfig.GetFullApiUrl(endpoint); + } + + private string SerializeData(object data) + { + return data != null ? JsonConvert.SerializeObject(data) : null; + } + + private void LogRequestDetails(string method, string url, string jsonData) + { + Debug.Log($"HTTP {method} 요청 URL: {url}"); + if (!string.IsNullOrEmpty(jsonData)) + { + Debug.Log($"HTTP 요청 데이터: {jsonData}"); + } + } + + private async UniTask SendRequestAsync(string url, string method, string jsonData, Dictionary headers, CancellationToken cancellationToken) + { + var combinedCancellationToken = CreateCombinedCancellationToken(cancellationToken); + + Debug.Log($"HTTP 요청 시작: {method} {url}"); + if (!string.IsNullOrEmpty(jsonData)) + { + Debug.Log($"HTTP 요청 데이터: {jsonData}"); + } + + for (int attempt = 0; attempt <= NetworkConfig.MaxRetryCount; attempt++) + { + try + { + using var request = CreateRequest(url, method, jsonData, headers); + + Debug.Log($"HTTP 요청 전송 중... (시도 {attempt + 1}/{NetworkConfig.MaxRetryCount + 1})"); + var operation = request.SendWebRequest(); + await operation.WithCancellation(combinedCancellationToken); + + if (request.result == UnityWebRequest.Result.Success) + { + Debug.Log($"HTTP 요청 성공: {request.responseCode}"); + return ParseResponse(request); + } + else + { + await HandleRequestFailure(request, attempt, combinedCancellationToken); + } + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) when (ex is not ApiException) + { + await HandleRequestException(ex, attempt, combinedCancellationToken); + } + } + + throw new ApiException($"{NetworkConfig.MaxRetryCount + 1}번 시도 후 요청 실패", 0, "최대 재시도 횟수 초과"); + } + + private async UniTask SendFileRequestAsync(string url, byte[] fileData, string fileName, string fieldName, Dictionary headers, CancellationToken cancellationToken) + { + var combinedCancellationToken = CreateCombinedCancellationToken(cancellationToken); + + for (int attempt = 0; attempt <= NetworkConfig.MaxRetryCount; attempt++) + { + try + { + var form = new WWWForm(); + form.AddBinaryData(fieldName, fileData, fileName); + + using var request = UnityWebRequest.Post(url, form); + SetupRequest(request, headers); + request.timeout = (int)NetworkConfig.HttpTimeout; + + var operation = request.SendWebRequest(); + await operation.WithCancellation(combinedCancellationToken); + + if (request.result == UnityWebRequest.Result.Success) + { + return ParseResponse(request); + } + else + { + await HandleFileUploadFailure(request, attempt, combinedCancellationToken); + } + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) when (ex is not ApiException) + { + await HandleFileUploadException(ex, attempt, combinedCancellationToken); + } + } + + throw new ApiException($"{NetworkConfig.MaxRetryCount + 1}번 시도 후 파일 업로드 실패", 0, "최대 재시도 횟수 초과"); + } + + private CancellationToken CreateCombinedCancellationToken(CancellationToken cancellationToken) + { + return CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, cancellationTokenSource.Token).Token; + } + + private async UniTask HandleRequestFailure(UnityWebRequest request, int attempt, CancellationToken cancellationToken) + { + var error = new ApiException(request.error, request.responseCode, request.downloadHandler?.text); + Debug.LogError($"HTTP 요청 실패: {request.result}, 상태코드: {request.responseCode}, 오류: {request.error}"); + + if (ShouldRetry(request.responseCode) && attempt < NetworkConfig.MaxRetryCount) + { + Debug.LogWarning($"API 요청 실패 (시도 {attempt + 1}/{NetworkConfig.MaxRetryCount + 1}): {error.Message}"); + await UniTask.Delay(TimeSpan.FromSeconds(NetworkConfig.RetryDelay * (attempt + 1)), cancellationToken: cancellationToken); + return; + } + + throw error; + } + + private async UniTask HandleRequestException(Exception ex, int attempt, CancellationToken cancellationToken) + { + if (attempt < NetworkConfig.MaxRetryCount) + { + Debug.LogWarning($"API 요청 예외 발생 (시도 {attempt + 1}/{NetworkConfig.MaxRetryCount + 1}): {ex.Message}"); + await UniTask.Delay(TimeSpan.FromSeconds(NetworkConfig.RetryDelay * (attempt + 1)), cancellationToken: cancellationToken); + return; + } + throw new ApiException($"{NetworkConfig.MaxRetryCount + 1}번 시도 후 요청 실패", 0, ex.Message); + } + + private async UniTask HandleFileUploadFailure(UnityWebRequest request, int attempt, CancellationToken cancellationToken) + { + var error = new ApiException(request.error, request.responseCode, request.downloadHandler?.text); + + if (ShouldRetry(request.responseCode) && attempt < NetworkConfig.MaxRetryCount) + { + Debug.LogWarning($"파일 업로드 실패 (시도 {attempt + 1}/{NetworkConfig.MaxRetryCount + 1}): {error.Message}"); + await UniTask.Delay(TimeSpan.FromSeconds(NetworkConfig.RetryDelay * (attempt + 1)), cancellationToken: cancellationToken); + return; + } + + throw error; + } + + private async UniTask HandleFileUploadException(Exception ex, int attempt, CancellationToken cancellationToken) + { + if (attempt < NetworkConfig.MaxRetryCount) + { + Debug.LogWarning($"파일 업로드 예외 발생 (시도 {attempt + 1}/{NetworkConfig.MaxRetryCount + 1}): {ex.Message}"); + await UniTask.Delay(TimeSpan.FromSeconds(NetworkConfig.RetryDelay * (attempt + 1)), cancellationToken: cancellationToken); + return; + } + throw new ApiException($"{NetworkConfig.MaxRetryCount + 1}번 시도 후 파일 업로드 실패", 0, ex.Message); + } + + private UnityWebRequest CreateRequest(string url, string method, string jsonData, Dictionary headers) + { + var request = new UnityWebRequest(url, method); + + if (!string.IsNullOrEmpty(jsonData)) + { + var bodyRaw = Encoding.UTF8.GetBytes(jsonData); + request.uploadHandler = new UploadHandlerRaw(bodyRaw); + } + + request.downloadHandler = new DownloadHandlerBuffer(); + SetupRequest(request, headers); + request.timeout = (int)NetworkConfig.HttpTimeout; + + return request; + } + + private void SetupRequest(UnityWebRequest request, Dictionary headers) + { + foreach (var header in defaultHeaders) + { + request.SetRequestHeader(header.Key, header.Value); + } + + if (headers != null) + { + foreach (var header in headers) + { + request.SetRequestHeader(header.Key, header.Value); + } + } + } + + private T ParseResponse(UnityWebRequest request) + { + var responseText = request.downloadHandler?.text; + + if (string.IsNullOrEmpty(responseText)) + { + return default(T); + } + + try + { + return JsonConvert.DeserializeObject(responseText); + } + catch (Exception ex) + { + return TryFallbackParse(responseText, request.responseCode, ex); + } + } + + private T TryFallbackParse(string responseText, long responseCode, Exception originalException) + { + try + { + return JsonUtility.FromJson(responseText); + } + catch (Exception fallbackEx) + { + throw new ApiException($"응답 파싱 실패: {originalException.Message} (폴백도 실패: {fallbackEx.Message})", responseCode, responseText); + } + } + + private bool ShouldRetry(long responseCode) + { + return responseCode >= 500 || responseCode == 429; + } + + private void Cleanup() + { + cancellationTokenSource?.Cancel(); + cancellationTokenSource?.Dispose(); + } + + #endregion + } + + /// + /// API 예외 클래스 + /// + public class ApiException : Exception + { + public long StatusCode { get; } + public string ResponseBody { get; } + + public ApiException(string message, long statusCode, string responseBody) + : base(message) + { + StatusCode = statusCode; + ResponseBody = responseBody; + } + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Http/HttpApiClient.cs.meta b/Assets/Infrastructure/Network/Http/HttpApiClient.cs.meta new file mode 100644 index 0000000..4ad71af --- /dev/null +++ b/Assets/Infrastructure/Network/Http/HttpApiClient.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 00fbb8efd091db646bc0653b6295222f \ No newline at end of file diff --git a/Assets/Infrastructure/Network/README.md b/Assets/Infrastructure/Network/README.md new file mode 100644 index 0000000..b546cf2 --- /dev/null +++ b/Assets/Infrastructure/Network/README.md @@ -0,0 +1,269 @@ +# ProjectVG Network Module + +Unity 클라이언트와 서버 간의 통신을 위한 네트워크 모듈입니다. +강제된 JSON 형식 `{type: "xxx", data: {...}}`을 사용합니다. + +## 📦 설치 + +### 1. UniTask 설치 +```json +// Packages/manifest.json +"com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask" +``` + +### 2. 플랫폼별 WebSocket 구현 +플랫폼에 따라 최적화된 WebSocket 구현을 사용합니다. + +**플랫폼별 구현:** +- 🟩 Desktop: System.Net.WebSockets.ClientWebSocket +- 🟩 WebGL: UnityWebRequest.WebSocket +- 🟩 Mobile: 네이티브 WebSocket 라이브러리 + +## 🏗️ 구조 + +``` +Assets/Infrastructure/Network/ +├── Configs/ # 설정 파일들 +│ └── NetworkConfig.cs # Unity 표준 ScriptableObject 기반 설정 +├── DTOs/ # 데이터 전송 객체들 +│ ├── BaseApiResponse.cs # 기본 API 응답 +│ ├── Chat/ # 채팅 관련 DTO +│ └── Character/ # 캐릭터 관련 DTO +├── Http/ # HTTP 클라이언트 +│ └── HttpApiClient.cs # HTTP API 클라이언트 +├── Services/ # API 서비스들 +│ ├── ApiServiceManager.cs # API 서비스 매니저 +│ ├── ChatApiService.cs # 채팅 API 서비스 +│ └── CharacterApiService.cs # 캐릭터 API 서비스 +└── WebSocket/ # WebSocket 관련 + ├── WebSocketManager.cs # WebSocket 매니저 (단순화됨) + ├── WebSocketFactory.cs # 플랫폼별 WebSocket 팩토리 + ├── INativeWebSocket.cs # 플랫폼별 WebSocket 인터페이스 + └── Platforms/ # 플랫폼별 WebSocket 구현 + ├── DesktopWebSocket.cs # 데스크톱용 (.NET ClientWebSocket) + ├── WebGLWebSocket.cs # WebGL용 (UnityWebRequest) + └── MobileWebSocket.cs # 모바일용 (네이티브 라이브러리) +``` + +## 🚀 사용법 + +### 1. Unity 표준 ScriptableObject 기반 설정 관리 + +#### 설정 파일 생성 +```csharp +// Unity Editor에서 ScriptableObject 생성 +// Assets/Infrastructure/Network/Configs/ 폴더에서 우클릭 +// Create > ProjectVG > Network > NetworkConfig +// Resources 폴더에 NetworkConfig.asset 파일 생성 +``` + +#### 앱 시작 시 환경 설정 +```csharp +// 앱 시작 시 (Editor에서만 가능) +NetworkConfig.SetDevelopmentEnvironment(); // localhost:7900 +NetworkConfig.SetTestEnvironment(); // localhost:7900 +NetworkConfig.SetProductionEnvironment(); // 122.153.130.223:7900 +``` + +#### 런타임 중 설정 사용 +```csharp +// 어디서든 동일한 설정 접근 (강제로 NetworkConfig 사용) +var currentEnv = NetworkConfig.CurrentEnvironment; +var apiUrl = NetworkConfig.GetFullApiUrl("chat"); +var wsUrl = NetworkConfig.GetWebSocketUrl(); + +// API URL 생성 +var userUrl = NetworkConfig.GetUserApiUrl(); +var characterUrl = NetworkConfig.GetCharacterApiUrl(); +var conversationUrl = NetworkConfig.GetConversationApiUrl(); +var authUrl = NetworkConfig.GetAuthApiUrl(); + +// WebSocket URL 생성 +var wsUrl = NetworkConfig.GetWebSocketUrl(); +var wsUrlWithVersion = NetworkConfig.GetWebSocketUrlWithVersion(); +var wsUrlWithSession = NetworkConfig.GetWebSocketUrlWithSession("session-123"); +``` + +### 2. WebSocket 사용 (단순화됨) + +#### 기본 사용법 +```csharp +// WebSocket 매니저 사용 +var wsManager = WebSocketManager.Instance; + +// 이벤트 구독 +wsManager.OnConnected += () => Debug.Log("연결됨"); +wsManager.OnDisconnected += () => Debug.Log("연결 해제됨"); +wsManager.OnError += (error) => Debug.LogError($"오류: {error}"); +wsManager.OnSessionIdReceived += (sessionId) => Debug.Log($"세션 ID: {sessionId}"); +wsManager.OnChatMessageReceived += (message) => Debug.Log($"채팅: {message}"); + +// 연결 +await wsManager.ConnectAsync(); + +// 메시지 전송 +await wsManager.SendChatMessageAsync("안녕하세요!"); + +// 연결 해제 +await wsManager.DisconnectAsync(); +``` + +#### 강제된 JSON 형식 사용 +```csharp +// 메시지 전송 (강제된 형식) +await wsManager.SendMessageAsync("chat", new ChatData +{ + message = "안녕하세요!", + sessionId = "session-123", + timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() +}); + +// 서버에서 받는 메시지 형식 +// { +// "type": "session_id", +// "data": { +// "session_id": "session_123456789" +// } +// } +// +// { +// "type": "chat", +// "data": { +// "message": "안녕하세요!", +// "sessionId": "session-123", +// "timestamp": 1703123456789 +// } +// } +``` + +### 3. HTTP API 사용 + +#### API 서비스 매니저 사용 +```csharp +// API 서비스 매니저 사용 +var apiManager = ApiServiceManager.Instance; + +// 채팅 API 사용 +var chatResponse = await apiManager.Chat.SendChatAsync( + new ChatRequest + { + message = "안녕하세요!", + characterId = "char-456", + userId = "user-789", + sessionId = "session-123" + } +); + +// 캐릭터 API 사용 +var character = await apiManager.Character.GetCharacterAsync("char-456"); +``` + +### 4. 전체 흐름 테스트 (권장) +```csharp +// NetworkTestManager 사용 +var testManager = FindObjectOfType(); + +// 1. WebSocket 연결 +await testManager.ConnectWebSocket(); + +// 2. HTTP 요청 전송 +await testManager.SendChatRequest(); + +// 3. WebSocket으로 결과 수신 (자동) +// 서버가 비동기 작업 완료 후 WebSocket으로 결과 전송 +``` + +## ⚙️ 설정 + +### Unity 표준 ScriptableObject 설정 관리 + +#### 1. 설정 파일 생성 +1. **NetworkConfig.asset** 생성: + - `Assets/Resources/` 폴더에 `NetworkConfig.asset` 파일 생성 + - Unity Editor에서 우클릭 > Create > ProjectVG > Network > NetworkConfig + +#### 2. 환경별 서버 주소 +- **개발 환경**: `localhost:7900` +- **테스트 환경**: `localhost:7900` +- **프로덕션 환경**: `122.153.130.223:7900` + +#### 3. 설정값들 (Editor에서 설정 가능) +- **HTTP 타임아웃**: 30초 +- **최대 재시도**: 3회 +- **WebSocket 타임아웃**: 30초 +- **자동 재연결**: 활성화 +- **하트비트**: 활성화 (30초 간격) + +#### 4. 런타임 보안 +- ✅ 앱 시작 시 한 번 설정 +- ✅ 런타임 중 설정 변경 불가 +- ✅ 어디서든 동일한 설정 접근 +- ✅ ScriptableObject로 일관성 보장 +- ✅ Editor에서 설정 가능 +- ✅ 팀 협업 용이 +- ✅ NetworkConfig 강제 사용으로 일관성 보장 + +## 🔧 플랫폼별 WebSocket 구현 + +### 1. DesktopWebSocket (데스크톱) +- System.Net.WebSockets.ClientWebSocket 사용 +- Windows/Mac/Linux 지원 +- 최고 성능 +- JSON 메시지만 처리 + +### 2. WebGLWebSocket (브라우저) +- UnityWebRequest.WebSocket 사용 +- WebGL 플랫폼 지원 +- 브라우저 제약사항 대응 +- JSON 메시지만 처리 + +### 3. MobileWebSocket (모바일) +- 네이티브 WebSocket 라이브러리 사용 +- iOS/Android 지원 +- 네이티브 성능 +- JSON 메시지만 처리 + +### 4. WebSocketFactory +- 플랫폼별 WebSocket 구현 생성 +- 컴파일 타임에 적절한 구현체 선택 + +## 🐛 문제 해결 + +### 플랫폼별 WebSocket +``` +데스크톱 플랫폼용 WebSocket 생성 +WebSocket 연결 시도: ws://localhost:7900/ws +WebSocket 연결 성공 +``` +**설명:** 플랫폼에 따라 적절한 WebSocket 구현체가 자동으로 선택됩니다. + +### 플랫폼별 특징 +- **Desktop**: .NET ClientWebSocket으로 최고 성능 +- **WebGL**: 브라우저 WebSocket API 사용 +- **Mobile**: 네이티브 라이브러리로 최적화 + +### 테스트 실행 방법 +1. **NetworkTestManager** 컴포넌트를 씬에 추가 +2. **Context Menu**에서 테스트 실행: + - `1. WebSocket 연결` + - `2. HTTP 채팅 요청` + - `3. HTTP 캐릭터 정보 요청` + - `4. WebSocket 메시지 전송` + - `5. WebSocket 연결 해제` + - `전체 테스트 실행` + +## 📝 로그 + +모든 로그는 한국어로 출력됩니다: +- `Debug.Log("WebSocket 연결 성공")` +- `Debug.LogError("연결 실패")` +- `Debug.LogWarning("재연결 시도")` + +## 🔄 변경 사항 + +### 주요 변경사항 (v2.0) +1. **바이너리 방식 완전 제거**: JSON 메시지만 처리 +2. **강제된 JSON 형식**: `{type: "xxx", data: {...}}` 형식 사용 +3. **MessageRouter 제거**: WebSocketManager에서 직접 처리 +4. **단순화된 구조**: 불필요한 복잡성 제거 +5. **확장 가능한 설계**: 추후 기능 추가 용이 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/README.md.meta b/Assets/Infrastructure/Network/README.md.meta new file mode 100644 index 0000000..6d02e29 --- /dev/null +++ b/Assets/Infrastructure/Network/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 4ceeee07406643741bb8b3a1e83f927f +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Infrastructure/Network/Services.meta b/Assets/Infrastructure/Network/Services.meta new file mode 100644 index 0000000..c4ba0b4 --- /dev/null +++ b/Assets/Infrastructure/Network/Services.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f9bc83dcef956db4784a7336ee77e17b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Infrastructure/Network/Services/ApiServiceManager.cs b/Assets/Infrastructure/Network/Services/ApiServiceManager.cs new file mode 100644 index 0000000..08f14da --- /dev/null +++ b/Assets/Infrastructure/Network/Services/ApiServiceManager.cs @@ -0,0 +1,72 @@ +using UnityEngine; + +namespace ProjectVG.Infrastructure.Network.Services +{ + /// + /// API 서비스 매니저 + /// 모든 API 서비스의 중앙 관리자 + /// + public class ApiServiceManager : MonoBehaviour + { + private static ApiServiceManager _instance; + public static ApiServiceManager Instance + { + get + { + if (_instance == null) + { + _instance = FindFirstObjectByType(); + if (_instance == null) + { + GameObject go = new GameObject("ApiServiceManager"); + _instance = go.AddComponent(); + DontDestroyOnLoad(go); + } + } + return _instance; + } + } + + // API 서비스 인스턴스들 + private ChatApiService _chatService; + private CharacterApiService _characterService; + + // 프로퍼티로 서비스 접근 + public ChatApiService Chat => _chatService ??= new ChatApiService(); + public CharacterApiService Character => _characterService ??= new CharacterApiService(); + + private void Awake() + { + if (_instance == null) + { + _instance = this; + DontDestroyOnLoad(gameObject); + InitializeServices(); + } + else if (_instance != this) + { + Destroy(gameObject); + } + } + + private void InitializeServices() + { + // 서비스 초기화 + _chatService = new ChatApiService(); + _characterService = new CharacterApiService(); + + Debug.Log("API 서비스 매니저 초기화 완료"); + } + + /// + /// 모든 서비스 재초기화 + /// + public void ReinitializeServices() + { + _chatService = new ChatApiService(); + _characterService = new CharacterApiService(); + + Debug.Log("API 서비스 재초기화 완료"); + } + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Services/ApiServiceManager.cs.meta b/Assets/Infrastructure/Network/Services/ApiServiceManager.cs.meta new file mode 100644 index 0000000..fe19897 --- /dev/null +++ b/Assets/Infrastructure/Network/Services/ApiServiceManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: bc3a1165acfed50488abb14eb8dbc022 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Services/CharacterApiService.cs b/Assets/Infrastructure/Network/Services/CharacterApiService.cs new file mode 100644 index 0000000..2e9205d --- /dev/null +++ b/Assets/Infrastructure/Network/Services/CharacterApiService.cs @@ -0,0 +1,155 @@ +using System.Threading; +using UnityEngine; +using Cysharp.Threading.Tasks; +using ProjectVG.Infrastructure.Network.Http; +using ProjectVG.Infrastructure.Network.DTOs.Character; + +namespace ProjectVG.Infrastructure.Network.Services +{ + /// + /// 캐릭터 API 서비스 + /// + public class CharacterApiService + { + private readonly HttpApiClient _httpClient; + + public CharacterApiService() + { + _httpClient = HttpApiClient.Instance; + if (_httpClient == null) + { + Debug.LogError("HttpApiClient.Instance가 null입니다. HttpApiClient가 생성되지 않았습니다."); + } + } + + /// + /// 모든 캐릭터 목록 조회 + /// + /// 취소 토큰 + /// 캐릭터 목록 + public async UniTask GetAllCharactersAsync(CancellationToken cancellationToken = default) + { + if (_httpClient == null) + { + Debug.LogError("HttpApiClient가 null입니다. 초기화를 확인해주세요."); + return null; + } + + return await _httpClient.GetAsync("character", cancellationToken: cancellationToken); + } + + /// + /// 특정 캐릭터 정보 조회 + /// + /// 캐릭터 ID + /// 취소 토큰 + /// 캐릭터 정보 + public async UniTask GetCharacterAsync(string characterId, CancellationToken cancellationToken = default) + { + if (_httpClient == null) + { + Debug.LogError("HttpApiClient가 null입니다. 초기화를 확인해주세요."); + return null; + } + + return await _httpClient.GetAsync($"character/{characterId}", cancellationToken: cancellationToken); + } + + /// + /// 캐릭터 생성 + /// + /// 캐릭터 생성 요청 + /// 취소 토큰 + /// 생성된 캐릭터 정보 + public async UniTask CreateCharacterAsync(CreateCharacterRequest request, CancellationToken cancellationToken = default) + { + return await _httpClient.PostAsync("character", request, cancellationToken: cancellationToken); + } + + /// + /// 간편한 캐릭터 생성 + /// + /// 캐릭터 이름 + /// 캐릭터 설명 + /// 캐릭터 역할 + /// 활성화 여부 + /// 취소 토큰 + /// 생성된 캐릭터 정보 + public async UniTask CreateCharacterAsync( + string name, + string description, + string role, + bool isActive = true, + CancellationToken cancellationToken = default) + { + var request = new CreateCharacterRequest + { + name = name, + description = description, + role = role, + isActive = isActive + }; + + return await CreateCharacterAsync(request, cancellationToken); + } + + /// + /// 캐릭터 정보 수정 + /// + /// 캐릭터 ID + /// 수정 요청 + /// 취소 토큰 + /// 수정된 캐릭터 정보 + public async UniTask UpdateCharacterAsync(string characterId, UpdateCharacterRequest request, CancellationToken cancellationToken = default) + { + return await _httpClient.PutAsync($"character/{characterId}", request, cancellationToken: cancellationToken); + } + + /// + /// 간편한 캐릭터 수정 + /// + /// 캐릭터 ID + /// 캐릭터 이름 + /// 캐릭터 설명 + /// 캐릭터 역할 + /// 활성화 여부 + /// 취소 토큰 + /// 수정된 캐릭터 정보 + public async UniTask UpdateCharacterAsync( + string characterId, + string name = null, + string description = null, + string role = null, + bool? isActive = null, + CancellationToken cancellationToken = default) + { + var request = new UpdateCharacterRequest(); + + if (name != null) request.name = name; + if (description != null) request.description = description; + if (role != null) request.role = role; + if (isActive.HasValue) request.isActive = isActive.Value; + + return await UpdateCharacterAsync(characterId, request, cancellationToken); + } + + /// + /// 캐릭터 삭제 + /// + /// 캐릭터 ID + /// 취소 토큰 + /// 삭제 성공 여부 + public async UniTask DeleteCharacterAsync(string characterId, CancellationToken cancellationToken = default) + { + try + { + await _httpClient.DeleteAsync($"character/{characterId}", cancellationToken: cancellationToken); + return true; + } + catch + { + return false; + } + } + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Services/CharacterApiService.cs.meta b/Assets/Infrastructure/Network/Services/CharacterApiService.cs.meta new file mode 100644 index 0000000..a985190 --- /dev/null +++ b/Assets/Infrastructure/Network/Services/CharacterApiService.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2e505a265149b82478a22ae11d759ca6 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Services/ChatApiService.cs b/Assets/Infrastructure/Network/Services/ChatApiService.cs new file mode 100644 index 0000000..f6ec055 --- /dev/null +++ b/Assets/Infrastructure/Network/Services/ChatApiService.cs @@ -0,0 +1,135 @@ +using System; +using System.Threading; +using UnityEngine; +using Cysharp.Threading.Tasks; +using ProjectVG.Infrastructure.Network.Http; +using ProjectVG.Infrastructure.Network.DTOs.Chat; +using Newtonsoft.Json; + +namespace ProjectVG.Infrastructure.Network.Services +{ + /// + /// 채팅 API 서비스 + /// + public class ChatApiService + { + private readonly HttpApiClient _httpClient; + private const string CHAT_ENDPOINT = "chat"; + private const string DEFAULT_ACTION = "chat"; + + public ChatApiService() + { + _httpClient = HttpApiClient.Instance; + ValidateHttpClient(); + } + + /// + /// 채팅 요청 전송 + /// + /// 채팅 요청 데이터 + /// 취소 토큰 + /// 채팅 응답 + public async UniTask SendChatAsync(ChatRequest request, CancellationToken cancellationToken = default) + { + ValidateRequest(request); + ValidateHttpClient(); + + var serverRequest = CreateServerRequest(request); + LogRequestDetails(serverRequest); + + return await _httpClient.PostAsync(CHAT_ENDPOINT, serverRequest, cancellationToken: cancellationToken); + } + + /// + /// 간편한 채팅 요청 (기본값 사용) + /// + /// 메시지 + /// 캐릭터 ID + /// 사용자 ID + /// 세션 ID (선택사항) + /// 액터 (선택사항) + /// 취소 토큰 + /// 채팅 응답 + public async UniTask SendChatAsync( + string message, + string characterId, + string userId, + string sessionId = null, + string actor = null, + CancellationToken cancellationToken = default) + { + var request = CreateSimpleRequest(message, characterId, userId, sessionId, actor); + return await SendChatAsync(request, cancellationToken); + } + + #region Private Methods + + private void ValidateHttpClient() + { + if (_httpClient == null) + { + throw new InvalidOperationException("HttpApiClient.Instance가 null입니다. HttpApiClient가 생성되지 않았습니다."); + } + } + + private void ValidateRequest(ChatRequest request) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request), "채팅 요청이 null입니다."); + } + + if (string.IsNullOrEmpty(request.message)) + { + throw new ArgumentException("메시지가 비어있습니다.", nameof(request.message)); + } + + if (string.IsNullOrEmpty(request.characterId)) + { + throw new ArgumentException("캐릭터 ID가 비어있습니다.", nameof(request.characterId)); + } + + if (string.IsNullOrEmpty(request.userId)) + { + throw new ArgumentException("사용자 ID가 비어있습니다.", nameof(request.userId)); + } + } + + private ChatRequest CreateServerRequest(ChatRequest originalRequest) + { + return new ChatRequest + { + sessionId = originalRequest.sessionId, + message = originalRequest.message, + characterId = originalRequest.characterId, + userId = originalRequest.userId, + action = originalRequest.action, + actor = originalRequest.actor, + instruction = originalRequest.instruction, + requestedAt = originalRequest.requestedAt + }; + } + + private ChatRequest CreateSimpleRequest(string message, string characterId, string userId, string sessionId, string actor) + { + return new ChatRequest + { + sessionId = sessionId, + message = message, + characterId = characterId, + userId = userId, + actor = actor, + action = DEFAULT_ACTION, + requestedAt = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ") + }; + } + + private void LogRequestDetails(ChatRequest request) + { + Debug.Log($"채팅 요청 엔드포인트: {CHAT_ENDPOINT}"); + Debug.Log($"서버로 전송할 JSON: {JsonConvert.SerializeObject(request)}"); + } + + #endregion + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Services/ChatApiService.cs.meta b/Assets/Infrastructure/Network/Services/ChatApiService.cs.meta new file mode 100644 index 0000000..5a73fce --- /dev/null +++ b/Assets/Infrastructure/Network/Services/ChatApiService.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0c581a111d7799c40b9d79a5887c988b \ No newline at end of file diff --git a/Assets/Infrastructure/Network/TestScene.meta b/Assets/Infrastructure/Network/TestScene.meta new file mode 100644 index 0000000..8558e6e --- /dev/null +++ b/Assets/Infrastructure/Network/TestScene.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 153323d148c986941975059f33cfa79e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Infrastructure/Network/UI.meta b/Assets/Infrastructure/Network/UI.meta new file mode 100644 index 0000000..194b70f --- /dev/null +++ b/Assets/Infrastructure/Network/UI.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 191d3f4a282ef6545b6fde81cdd0b3d3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Infrastructure/Network/WebSocket.meta b/Assets/Infrastructure/Network/WebSocket.meta new file mode 100644 index 0000000..27133f5 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b7243b76e275de847a176769c0af5e9e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Infrastructure/Network/WebSocket/Handlers.meta b/Assets/Infrastructure/Network/WebSocket/Handlers.meta new file mode 100644 index 0000000..3e9543f --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/Handlers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 45c015a8db87cc9409bf35790fcb2470 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Infrastructure/Network/WebSocket/INativeWebSocket.cs b/Assets/Infrastructure/Network/WebSocket/INativeWebSocket.cs new file mode 100644 index 0000000..12fd0d1 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/INativeWebSocket.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading; +using Cysharp.Threading.Tasks; + +namespace ProjectVG.Infrastructure.Network.WebSocket +{ + /// + /// 플랫폼별 Native WebSocket 구현을 위한 인터페이스 + /// + public interface INativeWebSocket : IDisposable + { + // 연결 상태 + bool IsConnected { get; } + bool IsConnecting { get; } + + // 이벤트 + event Action OnConnected; + event Action OnDisconnected; + event Action OnError; + event Action OnMessageReceived; + + // 연결 관리 + UniTask ConnectAsync(string url, CancellationToken cancellationToken = default); + UniTask DisconnectAsync(); + + // 메시지 전송 + UniTask SendMessageAsync(string message, CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/INativeWebSocket.cs.meta b/Assets/Infrastructure/Network/WebSocket/INativeWebSocket.cs.meta new file mode 100644 index 0000000..dc687fe --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/INativeWebSocket.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 259c46c284a0b4e4e9fb9510200ccf0c \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Platforms.meta b/Assets/Infrastructure/Network/WebSocket/Platforms.meta new file mode 100644 index 0000000..44c78e9 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/Platforms.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4da30c2f4bafd614ca7d49bf3e3b9287 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Infrastructure/Network/WebSocket/Platforms/DesktopWebSocket.cs b/Assets/Infrastructure/Network/WebSocket/Platforms/DesktopWebSocket.cs new file mode 100644 index 0000000..4c8bacf --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/Platforms/DesktopWebSocket.cs @@ -0,0 +1,175 @@ +using System; +using System.Threading; +using UnityEngine; +using Cysharp.Threading.Tasks; +using System.Net.WebSockets; +using System.Text; +using System.Threading.Tasks; + +namespace ProjectVG.Infrastructure.Network.WebSocket.Platforms +{ + /// + /// 데스크톱 플랫폼용 WebSocket 구현체 + /// System.Net.WebSockets.ClientWebSocket을 사용합니다. + /// JSON 메시지만 처리합니다. + /// + public class DesktopWebSocket : INativeWebSocket + { + public bool IsConnected { get; private set; } + public bool IsConnecting { get; private set; } + + public event Action OnConnected; + public event Action OnDisconnected; + public event Action OnError; + public event Action OnMessageReceived; + + private ClientWebSocket _webSocket; + private CancellationTokenSource _cancellationTokenSource; + private bool _isDisposed = false; + + public DesktopWebSocket() + { + _webSocket = new ClientWebSocket(); + _cancellationTokenSource = new CancellationTokenSource(); + } + + public async UniTask ConnectAsync(string url, CancellationToken cancellationToken = default) + { + if (IsConnected || IsConnecting) + { + return IsConnected; + } + + IsConnecting = true; + + try + { + var wsUrl = url.Replace("http://", "wss://").Replace("https://", "wss://"); + Debug.Log($"Desktop WebSocket 연결: {wsUrl}"); + + var combinedCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationTokenSource.Token).Token; + + await _webSocket.ConnectAsync(new Uri(wsUrl), combinedCancellationToken); + + IsConnected = true; + IsConnecting = false; + OnConnected?.Invoke(); + + // 메시지 수신 루프 시작 + _ = ReceiveLoopAsync(); + + return true; + } + catch (Exception ex) + { + IsConnecting = false; + var error = $"Desktop WebSocket 연결 실패: {ex.Message}"; + Debug.LogError(error); + OnError?.Invoke(error); + return false; + } + } + + public async UniTask DisconnectAsync() + { + if (!IsConnected) + { + return; + } + + try + { + IsConnected = false; + IsConnecting = false; + + if (_webSocket.State == WebSocketState.Open) + { + await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Client disconnect", CancellationToken.None); + } + + OnDisconnected?.Invoke(); + } + catch (Exception ex) + { + Debug.LogError($"Desktop WebSocket 연결 해제 중 오류: {ex.Message}"); + } + } + + public async UniTask SendMessageAsync(string message, CancellationToken cancellationToken = default) + { + if (!IsConnected || _webSocket.State != WebSocketState.Open) + { + Debug.LogWarning("Desktop WebSocket이 연결되지 않았습니다."); + return false; + } + + try + { + var buffer = Encoding.UTF8.GetBytes(message); + await _webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, cancellationToken); + return true; + } + catch (Exception ex) + { + Debug.LogError($"Desktop WebSocket 메시지 전송 실패: {ex.Message}"); + return false; + } + } + + private async Task ReceiveLoopAsync() + { + var buffer = new byte[4096]; + + try + { + while (IsConnected && _webSocket.State == WebSocketState.Open) + { + var result = await _webSocket.ReceiveAsync(new ArraySegment(buffer), _cancellationTokenSource.Token); + + if (result.MessageType == WebSocketMessageType.Close) + { + Debug.Log("Desktop WebSocket: 서버에서 연결 종료 요청"); + break; + } + else if (result.MessageType == WebSocketMessageType.Text) + { + var message = Encoding.UTF8.GetString(buffer, 0, result.Count); + OnMessageReceived?.Invoke(message); + } + else if (result.MessageType == WebSocketMessageType.Binary) + { + // 바이너리 메시지는 무시 (JSON만 처리) + Debug.LogWarning("Desktop WebSocket: 바이너리 메시지 수신됨 (무시됨)"); + } + } + } + catch (Exception ex) + { + if (!_isDisposed) + { + Debug.LogError($"Desktop WebSocket 수신 루프 오류: {ex.Message}"); + OnError?.Invoke(ex.Message); + } + } + finally + { + IsConnected = false; + if (!_isDisposed) + { + OnDisconnected?.Invoke(); + } + } + } + + public void Dispose() + { + if (_isDisposed) + return; + + _isDisposed = true; + _cancellationTokenSource?.Cancel(); + _cancellationTokenSource?.Dispose(); + _webSocket?.Dispose(); + } + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Platforms/DesktopWebSocket.cs.meta b/Assets/Infrastructure/Network/WebSocket/Platforms/DesktopWebSocket.cs.meta new file mode 100644 index 0000000..8b9b5bd --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/Platforms/DesktopWebSocket.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ecf4142f21c17ee48a6646dd7e267fec \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Platforms/MobileWebSocket.cs b/Assets/Infrastructure/Network/WebSocket/Platforms/MobileWebSocket.cs new file mode 100644 index 0000000..7d94fad --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/Platforms/MobileWebSocket.cs @@ -0,0 +1,155 @@ +using System; +using System.Threading; +using UnityEngine; +using Cysharp.Threading.Tasks; + +namespace ProjectVG.Infrastructure.Network.WebSocket.Platforms +{ + /// + /// 모바일 플랫폼용 WebSocket 구현체 + /// iOS/Android 네이티브 WebSocket 라이브러리를 사용합니다. + /// + public class MobileWebSocket : INativeWebSocket + { + public bool IsConnected { get; private set; } + public bool IsConnecting { get; private set; } + + public event Action OnConnected; + public event Action OnDisconnected; + public event Action OnError; +#pragma warning disable CS0067 + public event Action OnMessageReceived; +#pragma warning restore CS0067 + + private CancellationTokenSource _cancellationTokenSource; + private bool _isDisposed = false; + + public MobileWebSocket() + { + _cancellationTokenSource = new CancellationTokenSource(); + } + + public async UniTask ConnectAsync(string url, CancellationToken cancellationToken = default) + { + if (IsConnected || IsConnecting) + { + return IsConnected; + } + + IsConnecting = true; + + try + { + var combinedCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationTokenSource.Token).Token; + + // TODO : 모바일에서는 네이티브 WebSocket 라이브러리 사용 + // TODO : 현재는 기본 구현만 제공 (실제 구현 시 네이티브 플러그인 필요) + + // TODO : 임시로 성공 시뮬레이션 + await UniTask.Delay(100, cancellationToken: combinedCancellationToken); + + IsConnected = true; + IsConnecting = false; + OnConnected?.Invoke(); + + // 메시지 수신 루프 시작 + _ = ReceiveLoopAsync(); + + return true; + } + catch (Exception ex) + { + IsConnecting = false; + var error = $"모바일 WebSocket 연결 중 예외 발생: {ex.Message}"; + Debug.LogError(error); + OnError?.Invoke(error); + return false; + } + } + + public async UniTask DisconnectAsync() + { + if (!IsConnected) + { + return; + } + + try + { + IsConnected = false; + IsConnecting = false; + + // TODO : 네이티브 WebSocket 연결 해제 + // TODO : 실제 구현 시 네이티브 플러그인 호출 + await UniTask.CompletedTask; + + OnDisconnected?.Invoke(); + } + catch (Exception ex) + { + Debug.LogError($"모바일 WebSocket 연결 해제 중 오류: {ex.Message}"); + } + } + + public async UniTask SendMessageAsync(string message, CancellationToken cancellationToken = default) + { + if (!IsConnected) + { + Debug.LogWarning("모바일 WebSocket이 연결되지 않았습니다."); + return false; + } + + try + { + // TODO : 네이티브 WebSocket 메시지 전송 + // TODO : 실제 구현 시 네이티브 플러그인 호출 + await UniTask.CompletedTask; + return true; + } + catch (Exception ex) + { + Debug.LogError($"모바일 WebSocket 메시지 전송 실패: {ex.Message}"); + return false; + } + } + + private async UniTask ReceiveLoopAsync() + { + try + { + while (IsConnected && !_isDisposed) + { + // TODO : 네이티브 WebSocket 메시지 수신 + // TODO : 실제 구현 시 네이티브 플러그인에서 메시지 수신 + await UniTask.Delay(100); + } + } + catch (Exception ex) + { + if (!_isDisposed) + { + Debug.LogError($"모바일 WebSocket 수신 루프 오류: {ex.Message}"); + OnError?.Invoke(ex.Message); + } + } + finally + { + IsConnected = false; + if (!_isDisposed) + { + OnDisconnected?.Invoke(); + } + } + } + + public void Dispose() + { + if (_isDisposed) + return; + + _isDisposed = true; + _cancellationTokenSource?.Cancel(); + _cancellationTokenSource?.Dispose(); + } + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Platforms/MobileWebSocket.cs.meta b/Assets/Infrastructure/Network/WebSocket/Platforms/MobileWebSocket.cs.meta new file mode 100644 index 0000000..de72dd6 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/Platforms/MobileWebSocket.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1150790d7959b3147b99271055b8e7ff \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Platforms/WebGLWebSocket.cs b/Assets/Infrastructure/Network/WebSocket/Platforms/WebGLWebSocket.cs new file mode 100644 index 0000000..849911d --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/Platforms/WebGLWebSocket.cs @@ -0,0 +1,170 @@ +using System; +using System.Threading; +using UnityEngine; +using Cysharp.Threading.Tasks; +using UnityEngine.Networking; + +namespace ProjectVG.Infrastructure.Network.WebSocket.Platforms +{ + /// + /// WebGL 플랫폼용 WebSocket 구현체 + /// UnityWebRequest.WebSocket을 사용합니다. + /// + public class WebGLWebSocket : INativeWebSocket + { + public bool IsConnected { get; private set; } + public bool IsConnecting { get; private set; } + + public event Action OnConnected; + public event Action OnDisconnected; + public event Action OnError; +#pragma warning disable CS0067 + public event Action OnMessageReceived; +#pragma warning restore CS0067 + + private UnityWebRequest _webRequest; + private CancellationTokenSource _cancellationTokenSource; + private bool _isDisposed = false; + + public WebGLWebSocket() + { + _cancellationTokenSource = new CancellationTokenSource(); + } + + public async UniTask ConnectAsync(string url, CancellationToken cancellationToken = default) + { + if (IsConnected || IsConnecting) + { + return IsConnected; + } + + IsConnecting = true; + + try + { + var combinedCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationTokenSource.Token).Token; + + // UnityWebRequest.WebSocket 사용 + _webRequest = UnityWebRequest.Get(url); + _webRequest.SetRequestHeader("Upgrade", "websocket"); + _webRequest.SetRequestHeader("Connection", "Upgrade"); + + var operation = _webRequest.SendWebRequest(); + await operation.WithCancellation(combinedCancellationToken); + + if (_webRequest.result == UnityWebRequest.Result.Success) + { + IsConnected = true; + IsConnecting = false; + OnConnected?.Invoke(); + + // 메시지 수신 루프 시작 + _ = ReceiveLoopAsync(); + + return true; + } + else + { + var error = $"WebGL WebSocket 연결 실패: {_webRequest.error}"; + Debug.LogError(error); + OnError?.Invoke(error); + return false; + } + } + catch (Exception ex) + { + IsConnecting = false; + var error = $"WebGL WebSocket 연결 중 예외 발생: {ex.Message}"; + Debug.LogError(error); + OnError?.Invoke(error); + return false; + } + } + + public async UniTask DisconnectAsync() + { + if (!IsConnected) + { + return; + } + + try + { + IsConnected = false; + IsConnecting = false; + + _webRequest?.Abort(); + _webRequest?.Dispose(); + _webRequest = null; + + await UniTask.CompletedTask; // 비동기 작업 시뮬레이션 + + OnDisconnected?.Invoke(); + } + catch (Exception ex) + { + Debug.LogError($"WebGL WebSocket 연결 해제 중 오류: {ex.Message}"); + } + } + + public async UniTask SendMessageAsync(string message, CancellationToken cancellationToken = default) + { + if (!IsConnected) + { + Debug.LogWarning("WebGL WebSocket이 연결되지 않았습니다."); + return false; + } + + try + { + // TODO : WebGL에서는 WebSocket 메시지 전송을 위한 별도 구현 필요 + await UniTask.CompletedTask; + return true; + } + catch (Exception ex) + { + Debug.LogError($"WebGL WebSocket 메시지 전송 실패: {ex.Message}"); + return false; + } + } + + private async UniTask ReceiveLoopAsync() + { + try + { + while (IsConnected && !_isDisposed) + { + // TODO : WebGL에서는 WebSocket 메시지 수신을 위한 별도 구현 필요 + await UniTask.Delay(100); + } + } + catch (Exception ex) + { + if (!_isDisposed) + { + Debug.LogError($"WebGL WebSocket 수신 루프 오류: {ex.Message}"); + OnError?.Invoke(ex.Message); + } + } + finally + { + IsConnected = false; + if (!_isDisposed) + { + OnDisconnected?.Invoke(); + } + } + } + + public void Dispose() + { + if (_isDisposed) + return; + + _isDisposed = true; + _cancellationTokenSource?.Cancel(); + _cancellationTokenSource?.Dispose(); + _webRequest?.Dispose(); + } + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Platforms/WebGLWebSocket.cs.meta b/Assets/Infrastructure/Network/WebSocket/Platforms/WebGLWebSocket.cs.meta new file mode 100644 index 0000000..3705f86 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/Platforms/WebGLWebSocket.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 21c0c4d6a3b08134d975cd3c476a8dd6 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Processors.meta b/Assets/Infrastructure/Network/WebSocket/Processors.meta new file mode 100644 index 0000000..dd8cb27 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/Processors.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a0069f6496b3bf7468c732dd8bce79a1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Infrastructure/Network/WebSocket/WebSocketFactory.cs b/Assets/Infrastructure/Network/WebSocket/WebSocketFactory.cs new file mode 100644 index 0000000..fa186d5 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/WebSocketFactory.cs @@ -0,0 +1,25 @@ +using UnityEngine; +using ProjectVG.Infrastructure.Network.WebSocket.Platforms; + +namespace ProjectVG.Infrastructure.Network.WebSocket +{ + /// + /// 플랫폼별 WebSocket 구현을 생성하는 팩토리 + /// + public static class WebSocketFactory + { + /// + /// 현재 플랫폼에 맞는 WebSocket 구현을 생성합니다. + /// + public static INativeWebSocket Create() + { + #if UNITY_WEBGL && !UNITY_EDITOR + return new WebGLWebSocket(); + #elif UNITY_IOS || UNITY_ANDROID + return new MobileWebSocket(); + #else + return new DesktopWebSocket(); + #endif + } + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/WebSocketFactory.cs.meta b/Assets/Infrastructure/Network/WebSocket/WebSocketFactory.cs.meta new file mode 100644 index 0000000..6b9be90 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/WebSocketFactory.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 60c74fa19d97fbb46930ab87d63ab193 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs b/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs new file mode 100644 index 0000000..6f5b1b7 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs @@ -0,0 +1,482 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using UnityEngine; +using UnityEngine.Networking; +using Cysharp.Threading.Tasks; +using ProjectVG.Infrastructure.Network.Configs; + +namespace ProjectVG.Infrastructure.Network.WebSocket +{ + /// + /// WebSocket 연결 및 메시지 관리자 + /// 강제된 JSON 형식 {type: "xxx", data: {...}}을 사용합니다. + /// + public class WebSocketManager : MonoBehaviour + { + private INativeWebSocket _nativeWebSocket; + private CancellationTokenSource _cancellationTokenSource; + + // 메시지 버퍼링을 위한 필드 + private readonly StringBuilder _messageBuffer = new StringBuilder(); + private readonly object _bufferLock = new object(); + + private bool _isConnected = false; + private bool _isConnecting = false; + private int _reconnectAttempts = 0; + private string _sessionId; + + public static WebSocketManager Instance { get; private set; } + + // 이벤트 + public event Action OnConnected; + public event Action OnDisconnected; + public event Action OnError; + public event Action OnSessionIdReceived; + public event Action OnChatMessageReceived; + + public bool IsConnected => _isConnected; + public bool IsConnecting => _isConnecting; + public string SessionId => _sessionId; + + private void Awake() + { + if (Instance == null) + { + Instance = this; + DontDestroyOnLoad(gameObject); + InitializeManager(); + } + else + { + Destroy(gameObject); + } + } + + private void OnDestroy() + { + DisconnectAsync().Forget(); + _cancellationTokenSource?.Cancel(); + _cancellationTokenSource?.Dispose(); + } + + private void InitializeManager() + { + _cancellationTokenSource = new CancellationTokenSource(); + InitializeNativeWebSocket(); + } + + private void InitializeNativeWebSocket() + { + _nativeWebSocket = WebSocketFactory.Create(); + + _nativeWebSocket.OnConnected += OnNativeConnected; + _nativeWebSocket.OnDisconnected += OnNativeDisconnected; + _nativeWebSocket.OnError += OnNativeError; + _nativeWebSocket.OnMessageReceived += OnNativeMessageReceived; + } + + /// + /// WebSocket 연결 + /// + public async UniTask ConnectAsync(string sessionId = null, CancellationToken cancellationToken = default) + { + if (_isConnected || _isConnecting) + { + Debug.LogWarning("이미 연결 중이거나 연결되어 있습니다."); + return _isConnected; + } + + _isConnecting = true; + _sessionId = sessionId; + + try + { + var combinedCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationTokenSource.Token).Token; + + var wsUrl = GetWebSocketUrl(sessionId); + Debug.Log($"WebSocket 연결 시도: {wsUrl}"); + + var success = await _nativeWebSocket.ConnectAsync(wsUrl, combinedCancellationToken); + + if (success) + { + _isConnected = true; + _isConnecting = false; + _reconnectAttempts = 0; + Debug.Log("WebSocket 연결 성공"); + return true; + } + else + { + _isConnecting = false; + return false; + } + } + catch (OperationCanceledException) + { + Debug.Log("WebSocket 연결이 취소되었습니다."); + return false; + } + catch (Exception ex) + { + var error = $"WebSocket 연결 중 예외 발생: {ex.Message}"; + Debug.LogError(error); + OnError?.Invoke(error); + return false; + } + finally + { + _isConnecting = false; + } + } + + /// + /// WebSocket 연결 해제 + /// + public async UniTask DisconnectAsync() + { + if (!_isConnected) + { + return; + } + + _isConnected = false; + _isConnecting = false; + + if (_nativeWebSocket != null) + { + await _nativeWebSocket.DisconnectAsync(); + } + + Debug.Log("WebSocket 연결 해제"); + OnDisconnected?.Invoke(); + } + + /// + /// 메시지 전송 + /// + public async UniTask SendMessageAsync(string type, object data, CancellationToken cancellationToken = default) + { + if (!_isConnected) + { + Debug.LogWarning("WebSocket이 연결되지 않았습니다."); + return false; + } + + try + { + var message = new WebSocketMessage + { + type = type, + data = data + }; + + var jsonMessage = JsonUtility.ToJson(message); + return await _nativeWebSocket.SendMessageAsync(jsonMessage, cancellationToken); + } + catch (Exception ex) + { + Debug.LogError($"메시지 전송 실패: {ex.Message}"); + return false; + } + } + + /// + /// 채팅 메시지 전송 + /// + public async UniTask SendChatMessageAsync(string message, CancellationToken cancellationToken = default) + { + var chatData = new ChatData + { + message = message, + sessionId = _sessionId, + timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + }; + + return await SendMessageAsync("chat", chatData, cancellationToken); + } + + /// + /// WebSocket URL 생성 + /// + private string GetWebSocketUrl(string sessionId = null) + { + string baseUrl = NetworkConfig.GetWebSocketUrl(); + + if (!string.IsNullOrEmpty(sessionId)) + { + return $"{baseUrl}?sessionId={sessionId}"; + } + + return baseUrl; + } + + /// + /// 자동 재연결 시도 + /// + private async UniTaskVoid TryReconnectAsync() + { + bool autoReconnect = NetworkConfig.AutoReconnect; + int maxReconnectAttempts = NetworkConfig.MaxReconnectAttempts; + float reconnectDelay = NetworkConfig.ReconnectDelay; + + if (!autoReconnect || _reconnectAttempts >= maxReconnectAttempts) + { + return; + } + + _reconnectAttempts++; + Debug.Log($"WebSocket 재연결 시도 {_reconnectAttempts}/{maxReconnectAttempts}"); + + await UniTask.Delay(TimeSpan.FromSeconds(reconnectDelay)); + + if (!_isConnected) + { + ConnectAsync(_sessionId).Forget(); + } + } + + #region Native WebSocket Event Handlers + + private void OnNativeConnected() + { + _isConnected = true; + _isConnecting = false; + _reconnectAttempts = 0; + + Debug.Log("WebSocket 연결 성공"); + OnConnected?.Invoke(); + } + + private void OnNativeDisconnected() + { + _isConnected = false; + _isConnecting = false; + + OnDisconnected?.Invoke(); + TryReconnectAsync().Forget(); + } + + private void OnNativeError(string error) + { + _isConnected = false; + _isConnecting = false; + + OnError?.Invoke(error); + } + + private void OnNativeMessageReceived(string message) + { + try + { + Debug.Log($"메시지 수신: {message?.Length ?? 0} bytes"); + ProcessBufferedMessage(message); + } + catch (Exception ex) + { + Debug.LogError($"메시지 파싱 실패: {ex.Message}"); + Debug.LogError($"원시 메시지: {message}"); + } + } + + /// + /// 메시지 버퍼링 및 완전한 JSON 메시지 처리 + /// + private void ProcessBufferedMessage(string message) + { + lock (_bufferLock) + { + _messageBuffer.Append(message); + string bufferedMessage = _messageBuffer.ToString(); + + Debug.Log($"버퍼링된 메시지 길이: {bufferedMessage.Length}"); + + if (IsCompleteJsonMessage(bufferedMessage)) + { + Debug.Log("완전한 JSON 메시지 감지됨. 처리 시작."); + + if (IsValidJsonMessage(bufferedMessage)) + { + ProcessMessage(bufferedMessage); + } + else + { + Debug.LogWarning("JSON 형식이 아닌 메시지가 수신됨"); + } + + _messageBuffer.Clear(); + Debug.Log("메시지 처리 완료. 버퍼 초기화됨."); + } + else + { + Debug.Log($"불완전한 메시지. 버퍼에 누적 중... (현재 길이: {bufferedMessage.Length})"); + } + } + } + + /// + /// 완전한 JSON 메시지인지 확인 + /// + private bool IsCompleteJsonMessage(string message) + { + if (string.IsNullOrEmpty(message)) + return false; + + int openBraces = 0; + int closeBraces = 0; + bool inString = false; + char escapeChar = '\\'; + + for (int i = 0; i < message.Length; i++) + { + char c = message[i]; + + if (c == '"' && (i == 0 || message[i - 1] != escapeChar)) + { + inString = !inString; + } + else if (!inString) + { + if (c == '{') + openBraces++; + else if (c == '}') + closeBraces++; + } + } + + bool isComplete = openBraces > 0 && openBraces == closeBraces; + Debug.Log($"JSON 완성도 체크: 열린괄호={openBraces}, 닫힌괄호={closeBraces}, 완성={isComplete}"); + + return isComplete; + } + + /// + /// JSON 형식인지 확인 + /// + private bool IsValidJsonMessage(string message) + { + if (string.IsNullOrEmpty(message)) + return false; + + message = message.Trim(); + + if (message.StartsWith("{") && message.EndsWith("}")) + return true; + + if (message.StartsWith("[") && message.EndsWith("]")) + return true; + + return false; + } + + /// + /// 메시지 타입에 따른 처리 + /// + private void ProcessMessage(string message) + { + try + { + var webSocketMessage = JsonUtility.FromJson(message); + if (webSocketMessage == null) + { + Debug.LogError("WebSocket 메시지 파싱 실패"); + return; + } + + Debug.Log($"메시지 타입: {webSocketMessage.type}"); + + switch (webSocketMessage.type) + { + case "session_id": + ProcessSessionIdMessage(webSocketMessage.data); + break; + case "chat": + ProcessChatMessage(webSocketMessage.data); + break; + default: + Debug.LogWarning($"알 수 없는 메시지 타입: {webSocketMessage.type}"); + break; + } + } + catch (Exception ex) + { + Debug.LogError($"메시지 처리 중 오류: {ex.Message}"); + } + } + + /// + /// 세션 ID 메시지 처리 + /// + private void ProcessSessionIdMessage(object data) + { + try + { + var sessionIdData = JsonUtility.FromJson(JsonUtility.ToJson(data)); + if (sessionIdData != null) + { + _sessionId = sessionIdData.session_id; + Debug.Log($"세션 ID 수신: {sessionIdData.session_id}"); + OnSessionIdReceived?.Invoke(sessionIdData.session_id); + } + } + catch (Exception ex) + { + Debug.LogError($"세션 ID 메시지 처리 중 오류: {ex.Message}"); + } + } + + /// + /// 채팅 메시지 처리 + /// + private void ProcessChatMessage(object data) + { + try + { + var chatData = JsonUtility.FromJson(JsonUtility.ToJson(data)); + if (chatData != null) + { + Debug.Log($"채팅 메시지 수신: {chatData.message}"); + OnChatMessageReceived?.Invoke(chatData.message); + } + } + catch (Exception ex) + { + Debug.LogError($"채팅 메시지 처리 중 오류: {ex.Message}"); + } + } + + #endregion + } + + /// + /// WebSocket 메시지 기본 구조 + /// + [Serializable] + public class WebSocketMessage + { + public string type; + public object data; + } + + /// + /// 세션 ID 데이터 + /// + [Serializable] + public class SessionIdData + { + public string session_id; + } + + /// + /// 채팅 데이터 + /// + [Serializable] + public class ChatData + { + public string message; + public string sessionId; + public long timestamp; + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs.meta b/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs.meta new file mode 100644 index 0000000..22dfba5 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8e1977f0484f5f84684a077ad63409b5 \ No newline at end of file diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/Mao.fadeMotionList.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/Mao.fadeMotionList.asset index 09f5f5a..4a530de 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/Mao.fadeMotionList.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/Mao.fadeMotionList.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 403ae2dd693bb1d4b924f6b8d206b053, type: 3} m_Name: Mao.fadeMotionList m_EditorClassIdentifier: - MotionInstanceIds: e0d10000e8d1000042d2000052d20000bed100001cd2000024d20000e4d10000 + MotionInstanceIds: 14d2000020d20000d8d20000e8d20000dad100007ad2000086d2000018d20000 CubismFadeMotionObjects: - {fileID: 11400000, guid: c6a5276089e6b2c4bb2f78cca94c4ded, type: 2} - {fileID: 11400000, guid: a0145e27134640343b894f03870256bc, type: 2} diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_01.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_01.anim index 8083ee5..5e56187 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_01.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_01.anim @@ -11378,5 +11378,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53788 + intParameter: 53882 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_01.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_01.fade.asset index eb31e47..e5459ca 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_01.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_01.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_01.fade m_EditorClassIdentifier: - MotionName: Assets\Live2D\Cubism\Samples\Models\Mao/motions/mtn_01.motion3.json + MotionName: Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_01.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_02.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_02.anim index 5323a7b..38b146e 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_02.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_02.anim @@ -12098,5 +12098,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53694 + intParameter: 53722 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_02.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_02.fade.asset index 0b70576..a911e59 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_02.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_02.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_02.fade m_EditorClassIdentifier: - MotionName: Assets\Live2D\Cubism\Samples\Models\Mao/motions/mtn_02.motion3.json + MotionName: Assets\Plugins\Live2D\Cubism\Samples\Models\Mao/motions/mtn_02.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 0.5 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_03.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_03.anim index 8b6dff7..0445509 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_03.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_03.anim @@ -11774,5 +11774,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53736 + intParameter: 53792 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_03.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_03.fade.asset index 65403f3..7f02379 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_03.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_03.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_03.fade m_EditorClassIdentifier: - MotionName: Assets\Live2D\Cubism\Samples\Models\Mao/motions/mtn_03.motion3.json + MotionName: Assets\Plugins\Live2D\Cubism\Samples\Models\Mao/motions/mtn_03.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 0.5 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_04.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_04.anim index 5f8bca3..1202af8 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_04.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_04.anim @@ -12944,5 +12944,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53796 + intParameter: 53894 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_04.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_04.fade.asset index 76fb1c1..9e8d2a1 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_04.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_04.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_04.fade m_EditorClassIdentifier: - MotionName: Assets/Live2D/Cubism/Samples/Models/Mao/motions/mtn_04.motion3.json + MotionName: Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_04.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 0.5 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/sample_01.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/sample_01.anim index 50626d2..c5d4590 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/sample_01.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/sample_01.anim @@ -11168,5 +11168,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53826 + intParameter: 53976 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/sample_01.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/sample_01.fade.asset index b647ce3..8508f79 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/sample_01.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/sample_01.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: sample_01.fade m_EditorClassIdentifier: - MotionName: Assets\Live2D\Cubism\Samples\Models\Mao/motions/sample_01.motion3.json + MotionName: Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/sample_01.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 0 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_01.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_01.anim index a8ba80f..7f8f15a 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_01.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_01.anim @@ -15266,5 +15266,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53732 + intParameter: 53784 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_01.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_01.fade.asset index 80566a8..9bc7561 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_01.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_01.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: special_01.fade m_EditorClassIdentifier: - MotionName: Assets/Live2D/Cubism/Samples/Models/Mao/motions/special_01.motion3.json + MotionName: Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_01.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 0.25 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_02.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_02.anim index 9a145ff..676e10f 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_02.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_02.anim @@ -17336,5 +17336,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53728 + intParameter: 53780 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_02.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_02.fade.asset index 29d1ffe..121ded3 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_02.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_02.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: special_02.fade m_EditorClassIdentifier: - MotionName: Assets\Live2D\Cubism\Samples\Models\Mao/motions/special_02.motion3.json + MotionName: Assets\Plugins\Live2D\Cubism\Samples\Models\Mao/motions/special_02.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 0.25 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_03.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_03.anim index 3383509..1d7c7e4 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_03.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_03.anim @@ -18134,5 +18134,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53842 + intParameter: 53992 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_03.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_03.fade.asset index 3788240..84be377 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_03.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_03.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: special_03.fade m_EditorClassIdentifier: - MotionName: Assets/Live2D/Cubism/Samples/Models/Mao/motions/special_03.motion3.json + MotionName: Assets\Plugins\Live2D\Cubism\Samples\Models\Mao/motions/special_03.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 0.25 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/Natori.fadeMotionList.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/Natori.fadeMotionList.asset index 4d4da11..20c858a 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/Natori.fadeMotionList.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/Natori.fadeMotionList.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 403ae2dd693bb1d4b924f6b8d206b053, type: 3} m_Name: Natori.fadeMotionList m_EditorClassIdentifier: - MotionInstanceIds: 10d20000d8d10000f8d1000020d200003ad20000d4d1000056d20000bad100002cd20000 + MotionInstanceIds: 5cd200000cd2000032d200007ed20000b4d2000002d20000ecd20000d6d10000a2d20000 CubismFadeMotionObjects: - {fileID: 11400000, guid: b211dc549c58a9e448169cc818f5edfa, type: 2} - {fileID: 11400000, guid: 145e4856478ab1540affc40745dbea64, type: 2} diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_00.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_00.anim index 14196ef..e16506c 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_00.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_00.anim @@ -731,5 +731,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53804 + intParameter: 53922 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_00.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_00.fade.asset index 759eed3..605d5f4 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_00.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_00.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_00.fade m_EditorClassIdentifier: - MotionName: Assets/Live2D/Cubism/Samples/Models/Natori/motions/mtn_00.motion3.json + MotionName: Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_00.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_01.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_01.anim index e828e8c..2cf088d 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_01.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_01.anim @@ -6842,5 +6842,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53792 + intParameter: 53886 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_01.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_01.fade.asset index a96ec73..f069fae 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_01.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_01.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_01.fade m_EditorClassIdentifier: - MotionName: Assets\Live2D\Cubism\Samples\Models\Natori/motions/mtn_01.motion3.json + MotionName: Assets\Plugins\Live2D\Cubism\Samples\Models\Natori/motions/mtn_01.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_02.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_02.anim index cfa71b0..44ec12a 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_02.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_02.anim @@ -9107,5 +9107,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53716 + intParameter: 53762 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_02.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_02.fade.asset index 37cd1ec..f698b66 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_02.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_02.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_02.fade m_EditorClassIdentifier: - MotionName: Assets\Live2D\Cubism\Samples\Models\Natori/motions/mtn_02.motion3.json + MotionName: Assets\Plugins\Live2D\Cubism\Samples\Models\Natori/motions/mtn_02.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_03.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_03.anim index 5731b3f..efd8cdc 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_03.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_03.anim @@ -9668,5 +9668,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53846 + intParameter: 53996 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_03.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_03.fade.asset index 1faf747..02b649f 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_03.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_03.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_03.fade m_EditorClassIdentifier: - MotionName: Assets\Live2D\Cubism\Samples\Models\Natori/motions/mtn_03.motion3.json + MotionName: Assets\Plugins\Live2D\Cubism\Samples\Models\Natori/motions/mtn_03.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_04.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_04.anim index 09e400b..2485b36 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_04.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_04.anim @@ -10931,5 +10931,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53752 + intParameter: 53810 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_04.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_04.fade.asset index 933b3a7..19c0b38 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_04.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_04.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_04.fade m_EditorClassIdentifier: - MotionName: Assets/Live2D/Cubism/Samples/Models/Natori/motions/mtn_04.motion3.json + MotionName: Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_04.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_05.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_05.anim index 1dd9384..5ca0846 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_05.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_05.anim @@ -10211,5 +10211,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53818 + intParameter: 53940 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_05.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_05.fade.asset index d4faf9d..df66149 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_05.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_05.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_05.fade m_EditorClassIdentifier: - MotionName: Assets\Live2D\Cubism\Samples\Models\Natori/motions/mtn_05.motion3.json + MotionName: Assets\Plugins\Live2D\Cubism\Samples\Models\Natori/motions/mtn_05.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_06.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_06.anim index a6d7d28..798fcc1 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_06.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_06.anim @@ -10199,5 +10199,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53720 + intParameter: 53772 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_06.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_06.fade.asset index fb5f7d7..98e3237 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_06.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_06.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_06.fade m_EditorClassIdentifier: - MotionName: Assets\Live2D\Cubism\Samples\Models\Natori/motions/mtn_06.motion3.json + MotionName: Assets\Plugins\Live2D\Cubism\Samples\Models\Natori/motions/mtn_06.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_07.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_07.anim index da79cf3..9d23e6b 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_07.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_07.anim @@ -9533,5 +9533,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53776 + intParameter: 53852 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_07.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_07.fade.asset index e317caf..501f020 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_07.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_07.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_07.fade m_EditorClassIdentifier: - MotionName: Assets\Live2D\Cubism\Samples\Models\Natori/motions/mtn_07.motion3.json + MotionName: Assets\Plugins\Live2D\Cubism\Samples\Models\Natori/motions/mtn_07.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_08.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_08.anim index d68f5ff..e242e1a 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_08.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_08.anim @@ -9083,5 +9083,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53690 + intParameter: 53718 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_08.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_08.fade.asset index e2173d1..6b3a873 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_08.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_08.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_08.fade m_EditorClassIdentifier: - MotionName: Assets\Live2D\Cubism\Samples\Models\Natori/motions/mtn_08.motion3.json + MotionName: Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_08.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_00.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_00.fade.asset index 6c34070..655c1d6 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_00.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_00.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_00.fade m_EditorClassIdentifier: - MotionName: Assets/Live2D/Cubism/Samples/Models/Rice/motions/mtn_00.motion3.json + MotionName: Assets\Plugins\Live2D\Cubism\Samples\Models\Rice/motions/mtn_00.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_01.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_01.fade.asset index dbac025..cc7b39e 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_01.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_01.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_01.fade m_EditorClassIdentifier: - MotionName: Assets\Live2D\Cubism\Samples\Models\Rice/motions/mtn_01.motion3.json + MotionName: Assets\Plugins\Live2D\Cubism\Samples\Models\Rice/motions/mtn_01.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_02.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_02.fade.asset index 66fc115..c293b7c 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_02.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_02.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_02.fade m_EditorClassIdentifier: - MotionName: Assets/Live2D/Cubism/Samples/Models/Rice/motions/mtn_02.motion3.json + MotionName: Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_02.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_03.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_03.fade.asset index 52bcdb0..533bdfa 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_03.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_03.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_03.fade m_EditorClassIdentifier: - MotionName: Assets/Live2D/Cubism/Samples/Models/Rice/motions/mtn_03.motion3.json + MotionName: Assets\Plugins\Live2D\Cubism\Samples\Models\Rice/motions/mtn_03.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 1 diff --git a/Assets/Tests/Runtime.meta b/Assets/Tests/Runtime.meta new file mode 100644 index 0000000..76a563c --- /dev/null +++ b/Assets/Tests/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0412d803e6fb9cd408e6cf0d4a917590 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Runtime/NetworkTestManager.cs b/Assets/Tests/Runtime/NetworkTestManager.cs new file mode 100644 index 0000000..e60a124 --- /dev/null +++ b/Assets/Tests/Runtime/NetworkTestManager.cs @@ -0,0 +1,847 @@ +using System; +using System.Threading; +using UnityEngine; +using Cysharp.Threading.Tasks; +using ProjectVG.Infrastructure.Network.WebSocket; +using ProjectVG.Infrastructure.Network.Services; +using ProjectVG.Infrastructure.Network.Configs; +using ProjectVG.Infrastructure.Network.Http; +using ProjectVG.Infrastructure.Network.DTOs.Chat; + +namespace ProjectVG.Tests.Runtime +{ + /// + /// WebSocket + HTTP 통합 테스트 매니저 + /// 더미 클라이언트와 동일한 방식으로 테스트합니다. + /// + public class NetworkTestManager : MonoBehaviour + { + [Header("테스트 설정")] + [SerializeField] private string testCharacterId = "44444444-4444-4444-4444-444444444444"; // 제로 + [SerializeField] private string testUserId = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"; + [SerializeField] private string testMessage = "안녕하세요! 테스트 메시지입니다."; + + [Header("자동 테스트")] + [SerializeField] private bool autoTest = false; + [SerializeField] private float testInterval = 15f; // 더 긴 간격으로 변경 + + // UI에서 접근할 수 있도록 public 프로퍼티 추가 + public bool AutoTest + { + get => autoTest; + set => autoTest = value; + } + + public float TestInterval + { + get => testInterval; + set => testInterval = value; + } + + private WebSocketManager _webSocketManager; + private ApiServiceManager _apiServiceManager; + private CancellationTokenSource _cancellationTokenSource; + private bool _isTestRunning = false; + private string _receivedSessionId = null; + private bool _chatResponseReceived = false; + private string _lastChatResponse = null; + private int _reconnectAttempts = 0; + private const int MAX_RECONNECT_ATTEMPTS = 3; + private bool _isIntentionalDisconnect = false; // 의도적인 연결 해제 여부 + + private void Awake() + { + _cancellationTokenSource = new CancellationTokenSource(); + + // HTTP 연결 허용 설정 + #if UNITY_EDITOR || UNITY_STANDALONE + UnityEngine.Networking.UnityWebRequest.ClearCookieCache(); + #endif + + // 매니저들이 없으면 생성 + EnsureManagersExist(); + + InitializeManagers(); + } + + /// + /// 필요한 매니저들이 존재하는지 확인하고 없으면 생성 + /// + private void EnsureManagersExist() + { + // HttpApiClient가 없으면 생성 + if (HttpApiClient.Instance == null) + { + Debug.Log("HttpApiClient를 생성합니다..."); + var httpApiClientGO = new GameObject("HttpApiClient"); + httpApiClientGO.AddComponent(); + DontDestroyOnLoad(httpApiClientGO); + } + + // WebSocketManager가 없으면 생성 + if (WebSocketManager.Instance == null) + { + Debug.Log("WebSocketManager를 생성합니다..."); + var webSocketManagerGO = new GameObject("WebSocketManager"); + webSocketManagerGO.AddComponent(); + DontDestroyOnLoad(webSocketManagerGO); + } + + // ApiServiceManager가 없으면 생성 + if (ApiServiceManager.Instance == null) + { + Debug.Log("ApiServiceManager를 생성합니다..."); + var apiServiceManagerGO = new GameObject("ApiServiceManager"); + apiServiceManagerGO.AddComponent(); + DontDestroyOnLoad(apiServiceManagerGO); + } + } + + private void Start() + { + if (autoTest) + { + StartAutoTest().Forget(); + } + } + + /// + /// UI에서 자동 테스트를 시작할 수 있도록 public 메서드 제공 + /// + public void StartAutoTestFromUI() + { + if (autoTest) + { + StartAutoTest().Forget(); + } + } + + private void OnDestroy() + { + _cancellationTokenSource?.Cancel(); + _cancellationTokenSource?.Dispose(); + } + + private void InitializeManagers() + { + try + { + // NetworkConfig 초기화 (앱 시작 시 환경 설정) + NetworkConfig.SetDevelopmentEnvironment(); + + // WebSocket 매니저 초기화 + _webSocketManager = WebSocketManager.Instance; + if (_webSocketManager == null) + { + Debug.LogError("WebSocketManager.Instance가 null입니다. 매니저가 생성되지 않았습니다."); + return; + } + + Debug.Log($"WebSocket 설정 적용: {NetworkConfig.GetWebSocketUrl()}"); + Debug.Log($"현재 환경: {NetworkConfig.CurrentEnvironment}"); + + // HTTP API 클라이언트 설정 + if (HttpApiClient.Instance != null) + { + Debug.Log($"API 설정 적용: {NetworkConfig.GetFullApiUrl("chat")}"); + } + + // API 서비스 매니저 초기화 + _apiServiceManager = ApiServiceManager.Instance; + if (_apiServiceManager == null) + { + Debug.LogError("ApiServiceManager.Instance가 null입니다. 매니저가 생성되지 않았습니다."); + return; + } + + // WebSocket 이벤트 구독 + _webSocketManager.OnConnected += OnWebSocketConnected; + _webSocketManager.OnDisconnected += OnWebSocketDisconnected; + _webSocketManager.OnError += OnWebSocketError; + _webSocketManager.OnSessionIdReceived += OnSessionIdReceived; + _webSocketManager.OnChatMessageReceived += OnChatMessageReceived; + + Debug.Log("NetworkTestManager 초기화 완료"); + NetworkConfig.LogCurrentSettings(); + } + catch (Exception ex) + { + Debug.LogError($"NetworkTestManager 초기화 중 오류: {ex.Message}"); + } + } + + #region 수동 테스트 메서드들 + + [ContextMenu("1. WebSocket 연결 (더미 클라이언트 방식)")] + public async void ConnectWebSocket() + { + if (_isTestRunning) + { + Debug.LogWarning("테스트가 이미 실행 중입니다."); + return; + } + + if (_webSocketManager == null) + { + Debug.LogError("WebSocketManager가 초기화되지 않았습니다."); + return; + } + + try + { + Debug.Log("=== WebSocket 연결 시작 (더미 클라이언트 방식) ==="); + + // 현재 설정 정보 출력 + Debug.Log($"현재 환경: {NetworkConfig.CurrentEnvironment}"); + Debug.Log($"WebSocket 서버: {NetworkConfig.GetWebSocketUrl()}"); + + _receivedSessionId = null; // 세션 ID 초기화 + _reconnectAttempts = 0; + _isIntentionalDisconnect = false; // 의도적 해제 플래그 초기화 + + // 더미 클라이언트처럼 세션 ID 없이 연결 + bool connected = await _webSocketManager.ConnectAsync(); + + if (connected) + { + Debug.Log("✅ WebSocket 연결 성공! 세션 ID 대기 중..."); + } + else + { + Debug.LogError("❌ WebSocket 연결 실패!"); + } + } + catch (Exception ex) + { + Debug.LogError($"WebSocket 연결 중 오류: {ex.Message}"); + } + } + + [ContextMenu("2. HTTP 채팅 요청 (더미 클라이언트 방식)")] + public async void SendChatRequest() + { + if (_webSocketManager == null || !_webSocketManager.IsConnected) + { + Debug.LogWarning("WebSocket이 연결되지 않았습니다. 먼저 연결해주세요."); + return; + } + + if (_apiServiceManager == null) + { + Debug.LogError("ApiServiceManager가 초기화되지 않았습니다."); + return; + } + + // 세션 ID가 아직 수신되지 않았으면 대기 + if (string.IsNullOrEmpty(_receivedSessionId)) + { + Debug.LogError("세션 ID가 없습니다. WebSocket에서 세션 ID를 먼저 받아야 합니다."); + return; + } + + try + { + Debug.Log("=== HTTP 채팅 요청 시작 (더미 클라이언트 방식) ==="); + + // 현재 설정 정보 출력 + Debug.Log($"현재 환경: {NetworkConfig.CurrentEnvironment}"); + Debug.Log($"API 서버: {NetworkConfig.GetFullApiUrl("chat")}"); + Debug.Log($"세션 ID: {_receivedSessionId}"); + + var chatRequest = new ChatRequest + { + message = testMessage, + characterId = testCharacterId, + userId = testUserId, + sessionId = _receivedSessionId, // WebSocket에서 받은 세션 ID 사용 + actor = "web_user", + action = "chat", // 클라이언트와 동일하게 명시적으로 설정 + requestedAt = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ") + }; + + var response = await _apiServiceManager.Chat.SendChatAsync(chatRequest); + + if (response != null && response.success) + { + Debug.Log($"✅ HTTP 채팅 요청 성공! 응답: {response.message}"); + Debug.Log($" - 세션 ID: {_receivedSessionId}"); + Debug.Log($" - 캐릭터 ID: {testCharacterId}"); + Debug.Log($" - 사용자 ID: {testUserId}"); + } + else + { + Debug.LogError($"❌ HTTP 채팅 요청 실패: {response?.message ?? "알 수 없는 오류"}"); + } + } + catch (Exception ex) + { + Debug.LogError($"HTTP 채팅 요청 중 오류: {ex.Message}"); + } + } + + [ContextMenu("3. HTTP 캐릭터 정보 요청")] + public async void GetCharacterInfo() + { + try + { + Debug.Log("=== HTTP 캐릭터 정보 요청 시작 ==="); + + var character = await _apiServiceManager.Character.GetCharacterAsync(testCharacterId); + + if (character != null) + { + Debug.Log($"✅ 캐릭터 정보 조회 성공!"); + Debug.Log($" - ID: {character.id}"); + Debug.Log($" - 이름: {character.name}"); + Debug.Log($" - 설명: {character.description}"); + Debug.Log($" - 역할: {character.role}"); + Debug.Log($" - 활성화: {character.isActive}"); + } + else + { + Debug.LogError($"❌ 캐릭터 정보 조회 실패: 캐릭터를 찾을 수 없습니다."); + } + } + catch (Exception ex) + { + Debug.LogError($"HTTP 캐릭터 정보 요청 중 오류: {ex.Message}"); + } + } + + [ContextMenu("4. WebSocket 메시지 전송")] + public async void SendWebSocketMessage() + { + if (!_webSocketManager.IsConnected) + { + Debug.LogWarning("WebSocket이 연결되지 않았습니다."); + return; + } + + try + { + Debug.Log("=== WebSocket 메시지 전송 시작 ==="); + + bool sent = await _webSocketManager.SendChatMessageAsync( + message: "WebSocket으로 직접 전송하는 테스트 메시지" + ); + + if (sent) + { + Debug.Log("✅ WebSocket 메시지 전송 성공!"); + } + else + { + Debug.LogError("❌ WebSocket 메시지 전송 실패!"); + } + } + catch (Exception ex) + { + Debug.LogError($"WebSocket 메시지 전송 중 오류: {ex.Message}"); + } + } + + [ContextMenu("5. WebSocket 연결 해제")] + public async void DisconnectWebSocket() + { + try + { + Debug.Log("=== WebSocket 연결 해제 시작 ==="); + _isIntentionalDisconnect = true; // 의도적 해제 플래그 설정 + await _webSocketManager.DisconnectAsync(); + _receivedSessionId = null; // 세션 ID 초기화 + Debug.Log("✅ WebSocket 연결 해제 완료!"); + } + catch (Exception ex) + { + Debug.LogError($"WebSocket 연결 해제 중 오류: {ex.Message}"); + } + } + + [ContextMenu("더미 클라이언트 방식 전체 테스트")] + public async void RunDummyClientTest() + { + if (_isTestRunning) + { + Debug.LogWarning("테스트가 이미 실행 중입니다."); + return; + } + + _isTestRunning = true; + + try + { + Debug.Log("🚀 === 더미 클라이언트 방식 전체 테스트 시작 ==="); + + // 현재 설정 정보 출력 + Debug.Log($"테스트 환경: {NetworkConfig.CurrentEnvironment}"); + Debug.Log($"API 서버: {NetworkConfig.GetFullApiUrl("")}"); + Debug.Log($"WebSocket 서버: {NetworkConfig.GetWebSocketUrl()}"); + + // 0. 기존 연결이 있으면 해제 + if (_webSocketManager.IsConnected) + { + Debug.Log("0️⃣ 기존 연결 해제 중..."); + _isIntentionalDisconnect = true; // 의도적 해제 플래그 설정 + await _webSocketManager.DisconnectAsync(); + await UniTask.Delay(1000); // 연결 해제 완료 대기 + } + + // 1. WebSocket 연결 (세션 ID 없이) + Debug.Log("1️⃣ WebSocket 연결 중..."); + bool connected = await _webSocketManager.ConnectAsync(); + if (!connected) + { + Debug.LogError("WebSocket 연결 실패로 테스트 중단"); + return; + } + + // 2. 세션 ID 수신 대기 (최대 10초) + Debug.Log("2️⃣ 세션 ID 수신 대기 중..."); + int waitCount = 0; + while (string.IsNullOrEmpty(_receivedSessionId) && waitCount < 100) // 10초로 증가 + { + await UniTask.Delay(100); + waitCount++; + if (waitCount % 10 == 0) // 1초마다 로그 + { + Debug.Log($"2️⃣ 세션 ID 대기 중... ({waitCount/10}초 경과)"); + } + } + + if (string.IsNullOrEmpty(_receivedSessionId)) + { + Debug.LogError("세션 ID를 받지 못했습니다. (10초 타임아웃)"); + Debug.LogWarning("서버에서 세션 ID 메시지를 보내지 않았거나, 메시지 형식이 다를 수 있습니다."); + return; + } + + Debug.Log($"✅ 세션 ID 수신: {_receivedSessionId}"); + + await UniTask.Delay(1000); // 안정화 대기 + + // 3. HTTP 채팅 요청 (세션 ID 포함) + Debug.Log("3️⃣ HTTP 채팅 요청 중..."); + await SendChatRequestInternal(); + + // 채팅 응답을 기다림 + await WaitForChatResponse(15); // 15초 타임아웃 + + // 4. HTTP 캐릭터 정보 요청 + Debug.Log("4️⃣ HTTP 캐릭터 정보 요청 중..."); + await GetCharacterInfoInternal(); + + await UniTask.Delay(1000); + + // 5. WebSocket 연결 해제 + Debug.Log("5️⃣ WebSocket 연결 해제 중..."); + _isIntentionalDisconnect = true; // 의도적 해제 플래그 설정 + await _webSocketManager.DisconnectAsync(); + _receivedSessionId = null; + + // 연결 해제 후 충분한 대기 시간 + await UniTask.Delay(2000); + + Debug.Log("✅ === 더미 클라이언트 방식 전체 테스트 완료 ==="); + } + catch (Exception ex) + { + Debug.LogError($"더미 클라이언트 방식 전체 테스트 중 오류: {ex.Message}"); + } + finally + { + _isTestRunning = false; + } + } + + [ContextMenu("전체 테스트 실행")] + public async void RunFullTest() + { + if (_isTestRunning) + { + Debug.LogWarning("테스트가 이미 실행 중입니다."); + return; + } + + _isTestRunning = true; + + try + { + Debug.Log("🚀 === 전체 테스트 시작 ==="); + + // 현재 설정 정보 출력 + Debug.Log($"테스트 환경: {NetworkConfig.CurrentEnvironment}"); + Debug.Log($"API 서버: {NetworkConfig.GetFullApiUrl("")}"); + Debug.Log($"WebSocket 서버: {NetworkConfig.GetWebSocketUrl()}"); + + // 1. WebSocket 연결 + Debug.Log("1️⃣ WebSocket 연결 중..."); + bool connected = await _webSocketManager.ConnectAsync(); + if (!connected) + { + Debug.LogError("WebSocket 연결 실패로 테스트 중단"); + return; + } + + // 2. 세션 ID 수신 대기 (최대 10초) + Debug.Log("2️⃣ 세션 ID 수신 대기 중..."); + int waitCount = 0; + while (string.IsNullOrEmpty(_receivedSessionId) && waitCount < 100) + { + await UniTask.Delay(100); + waitCount++; + if (waitCount % 10 == 0) // 1초마다 로그 + { + Debug.Log($"2️⃣ 세션 ID 대기 중... ({waitCount/10}초 경과)"); + } + } + + if (string.IsNullOrEmpty(_receivedSessionId)) + { + Debug.LogError("세션 ID를 받지 못했습니다. (10초 타임아웃)"); + return; + } + + Debug.Log($"✅ 세션 ID 수신: {_receivedSessionId}"); + await UniTask.Delay(1000); // 안정화 대기 + + // 3. HTTP 채팅 요청 + Debug.Log("3️⃣ HTTP 채팅 요청 중..."); + await SendChatRequestInternal(); + + // 채팅 응답을 기다림 + await WaitForChatResponse(15); // 15초 타임아웃 + + // 4. HTTP 캐릭터 정보 요청 + Debug.Log("4️⃣ HTTP 캐릭터 정보 요청 중..."); + await GetCharacterInfoInternal(); + + await UniTask.Delay(1000); + + // 5. WebSocket 메시지 전송 + Debug.Log("5️⃣ WebSocket 메시지 전송 중..."); + await SendWebSocketMessageInternal(); + + await UniTask.Delay(1000); + + // 6. WebSocket 연결 해제 + Debug.Log("6️⃣ WebSocket 연결 해제 중..."); + _isIntentionalDisconnect = true; // 의도적 해제 플래그 설정 + await _webSocketManager.DisconnectAsync(); + + Debug.Log("✅ === 전체 테스트 완료 ==="); + } + catch (Exception ex) + { + Debug.LogError($"전체 테스트 중 오류: {ex.Message}"); + } + finally + { + _isTestRunning = false; + } + } + + [ContextMenu("현재 네트워크 설정 정보 출력")] + public void LogCurrentNetworkConfig() + { + Debug.Log("=== 현재 네트워크 설정 정보 ==="); + NetworkConfig.LogCurrentSettings(); + } + + #endregion + + #region 자동 테스트 + + private async UniTaskVoid StartAutoTest() + { + Debug.Log("🔄 자동 테스트 시작..."); + + // 매니저 초기화 확인 + if (_webSocketManager == null || _apiServiceManager == null) + { + Debug.LogError("매니저가 초기화되지 않았습니다. 자동 테스트를 중단합니다."); + return; + } + + // HttpApiClient 확인 + if (HttpApiClient.Instance == null) + { + Debug.LogError("HttpApiClient가 초기화되지 않았습니다. 자동 테스트를 중단합니다."); + return; + } + + // 현재 설정 정보 출력 + Debug.Log($"자동 테스트 환경: {NetworkConfig.CurrentEnvironment}"); + Debug.Log($"API 서버: {NetworkConfig.GetFullApiUrl("")}"); + Debug.Log($"WebSocket 서버: {NetworkConfig.GetWebSocketUrl()}"); + + while (!_cancellationTokenSource.Token.IsCancellationRequested) + { + try + { + await RunDummyClientTestInternal(); + await UniTask.Delay(TimeSpan.FromSeconds(testInterval), cancellationToken: _cancellationTokenSource.Token); + } + catch (OperationCanceledException) + { + Debug.Log("자동 테스트가 취소되었습니다."); + break; + } + catch (Exception ex) + { + Debug.LogError($"자동 테스트 중 오류: {ex.Message}"); + Debug.LogError($"스택 트레이스: {ex.StackTrace}"); + await UniTask.Delay(5000, cancellationToken: _cancellationTokenSource.Token); + } + } + + Debug.Log("🔄 자동 테스트 종료"); + } + + private async UniTask RunDummyClientTestInternal() + { + // 매니저 null 체크 + if (_webSocketManager == null) + { + Debug.LogError("WebSocketManager가 null입니다. 초기화를 확인해주세요."); + return; + } + + if (_apiServiceManager == null) + { + Debug.LogError("ApiServiceManager가 null입니다. 초기화를 확인해주세요."); + return; + } + + try + { + // 현재 설정 정보 출력 + Debug.Log($"자동 테스트 환경: {NetworkConfig.CurrentEnvironment}"); + + // 0. 기존 연결이 있으면 해제 + if (_webSocketManager.IsConnected) + { + Debug.Log("자동 테스트 - 기존 연결 해제 중..."); + _isIntentionalDisconnect = true; // 의도적 해제 플래그 설정 + await _webSocketManager.DisconnectAsync(); + await UniTask.Delay(1000); + } + + // 1. WebSocket 연결 (세션 ID 없이) + Debug.Log("자동 테스트 - WebSocket 연결 중..."); + bool connected = await _webSocketManager.ConnectAsync(); + if (!connected) + { + Debug.LogError("자동 테스트 - WebSocket 연결 실패"); + return; + } + + // 2. 세션 ID 수신 대기 + Debug.Log("자동 테스트 - 세션 ID 수신 대기 중..."); + int waitCount = 0; + while (string.IsNullOrEmpty(_receivedSessionId) && waitCount < 100) // 10초로 증가 + { + await UniTask.Delay(100); + waitCount++; + if (waitCount % 10 == 0) + { + Debug.Log($"자동 테스트 - 세션 ID 대기 중... ({waitCount/10}초 경과)"); + } + } + + if (string.IsNullOrEmpty(_receivedSessionId)) + { + Debug.LogError("자동 테스트 - 세션 ID를 받지 못했습니다. (10초 타임아웃)"); + Debug.LogWarning("서버에서 세션 ID 메시지를 보내지 않았거나, 메시지 형식이 다를 수 있습니다."); + return; + } + + Debug.Log($"자동 테스트 - 세션 ID 수신: {_receivedSessionId}"); + await UniTask.Delay(500); + + // 3. HTTP 채팅 요청 (세션 ID 포함) + Debug.Log("자동 테스트 - HTTP 채팅 요청 중..."); + await SendChatRequestInternal(); + Debug.Log("자동 테스트 - HTTP 요청 완료, 채팅 응답 대기 중..."); + + // 채팅 응답을 기다림 + await WaitForChatResponse(15); // 15초 타임아웃 + + // 4. HTTP 캐릭터 정보 요청 + Debug.Log("자동 테스트 - HTTP 캐릭터 정보 요청 중..."); + await GetCharacterInfoInternal(); + await UniTask.Delay(500); + + // 5. 연결 해제 (더 오래 기다린 후) + Debug.Log("자동 테스트 - WebSocket 응답 대기 완료, 연결 해제 중..."); + _isIntentionalDisconnect = true; // 의도적 해제 플래그 설정 + await UniTask.Delay(2000); // 추가 대기 시간 + await _webSocketManager.DisconnectAsync(); + _receivedSessionId = null; + + // 연결 해제 후 충분한 대기 시간 + await UniTask.Delay(2000); + + Debug.Log("자동 테스트 - 더미 클라이언트 방식 테스트 완료"); + } + catch (Exception ex) + { + Debug.LogError($"자동 테스트 중 오류: {ex.Message}"); + throw; // 상위로 예외 전파 + } + } + + private async UniTask SendChatRequestInternal() + { + if (_apiServiceManager?.Chat == null) + { + Debug.LogError("ChatApiService가 null입니다."); + return; + } + + // 세션 ID가 없으면 HTTP 요청을 보내지 않음 + if (string.IsNullOrEmpty(_receivedSessionId)) + { + Debug.LogError("세션 ID가 없습니다. WebSocket에서 세션 ID를 먼저 받아야 합니다."); + return; + } + + var chatRequest = new ChatRequest + { + message = $"자동 테스트 메시지 - {DateTime.Now:HH:mm:ss}", + characterId = testCharacterId, + userId = testUserId, + sessionId = _receivedSessionId, // 서버에서 받은 세션 ID 사용 + actor = "web_user", + action = "chat", // 클라이언트와 동일하게 명시적으로 설정 + requestedAt = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ") + }; + + Debug.Log($"HTTP 채팅 요청 전송: {JsonUtility.ToJson(chatRequest)}"); + + // 채팅 응답 수신 상태 초기화 + _chatResponseReceived = false; + _lastChatResponse = null; + + try + { + var response = await _apiServiceManager.Chat.SendChatAsync(chatRequest); + Debug.Log($"HTTP 채팅 요청 성공: {JsonUtility.ToJson(response)}"); + } + catch (Exception ex) + { + Debug.LogError($"HTTP 채팅 요청 실패: {ex.Message}"); + } + } + + /// + /// 채팅 응답을 기다리는 메서드 + /// + private async UniTask WaitForChatResponse(int timeoutSeconds = 15) + { + Debug.Log($"채팅 응답 대기 시작 (타임아웃: {timeoutSeconds}초)"); + + var startTime = DateTime.Now; + while (!_chatResponseReceived && (DateTime.Now - startTime).TotalSeconds < timeoutSeconds) + { + await UniTask.Delay(100); + + // WebSocket 연결 상태 확인 + if (_webSocketManager != null && !_webSocketManager.IsConnected) + { + Debug.LogWarning("WebSocket 연결이 끊어졌습니다. 응답 대기 중단"); + break; + } + } + + if (_chatResponseReceived) + { + Debug.Log($"✅ 채팅 응답 수신 완료: {_lastChatResponse}"); + } + else + { + Debug.LogWarning($"⚠️ 채팅 응답 타임아웃 ({timeoutSeconds}초)"); + } + } + + private async UniTask GetCharacterInfoInternal() + { + if (_apiServiceManager?.Character == null) + { + Debug.LogError("CharacterApiService가 null입니다."); + return; + } + + var character = await _apiServiceManager.Character.GetCharacterAsync(testCharacterId); + if (character != null) + { + Debug.Log($"자동 테스트 - 캐릭터 정보 조회 성공: {character.name}"); + } + } + + private async UniTask SendWebSocketMessageInternal() + { + if (_webSocketManager == null) + { + Debug.LogError("WebSocketManager가 null입니다."); + return; + } + + await _webSocketManager.SendChatMessageAsync( + message: $"자동 WebSocket 메시지 - {DateTime.Now:HH:mm:ss}" + ); + } + + #endregion + + #region WebSocket 이벤트 핸들러 + + private void OnWebSocketConnected() + { + Debug.Log("🎉 WebSocket 연결됨!"); + _reconnectAttempts = 0; // 연결 성공 시 재연결 시도 횟수 초기화 + } + + private void OnWebSocketDisconnected() + { + Debug.Log("🔌 WebSocket 연결 해제됨!"); + + // 의도적인 연결 해제가 아닌 경우에만 자동 재연결 시도 + if (!_isIntentionalDisconnect && _reconnectAttempts < MAX_RECONNECT_ATTEMPTS) + { + _reconnectAttempts++; + Debug.Log($"재연결 시도 {_reconnectAttempts}/{MAX_RECONNECT_ATTEMPTS}"); + ConnectWebSocket(); + } + else if (_isIntentionalDisconnect) + { + Debug.Log("의도적인 연결 해제로 재연결을 시도하지 않습니다."); + _isIntentionalDisconnect = false; // 플래그 초기화 + } + else + { + Debug.LogError("최대 재연결 시도 횟수 초과"); + } + } + + private void OnWebSocketError(string error) + { + Debug.LogError($"❌ WebSocket 오류: {error}"); + } + + private void OnSessionIdReceived(string sessionId) + { + _receivedSessionId = sessionId; + Debug.Log($"🆔 WebSocket 세션 ID 수신: {_receivedSessionId}"); + Debug.Log($"✅ 세션 ID가 성공적으로 저장되었습니다!"); + } + + private void OnChatMessageReceived(string message) + { + Debug.Log($"💬 WebSocket 채팅 메시지 수신: {message}"); + _chatResponseReceived = true; + _lastChatResponse = message; + } + + #endregion + } +} \ No newline at end of file diff --git a/Assets/Tests/Runtime/NetworkTestManager.cs.meta b/Assets/Tests/Runtime/NetworkTestManager.cs.meta new file mode 100644 index 0000000..6e6f889 --- /dev/null +++ b/Assets/Tests/Runtime/NetworkTestManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d0a3e75ea471e5f47b758f5a032a094e \ No newline at end of file diff --git a/Assets/Tests/Runtime/NetworkTestUI.cs b/Assets/Tests/Runtime/NetworkTestUI.cs new file mode 100644 index 0000000..eebc0b0 --- /dev/null +++ b/Assets/Tests/Runtime/NetworkTestUI.cs @@ -0,0 +1,355 @@ +using UnityEngine; +using UnityEngine.UI; +using TMPro; + +namespace ProjectVG.Tests.Runtime +{ + /// + /// 네트워크 테스트를 위한 UI 매니저 + /// 더미 클라이언트 방식 테스트를 지원합니다. + /// + public class NetworkTestUI : MonoBehaviour + { + [Header("UI References")] + [SerializeField] private Button connectButton; + [SerializeField] private Button disconnectButton; + [SerializeField] private Button chatRequestButton; + [SerializeField] private Button characterInfoButton; + [SerializeField] private Button webSocketMessageButton; + [SerializeField] private Button fullTestButton; + [SerializeField] private Button dummyClientTestButton; + [SerializeField] private Button autoTestButton; + + [Header("Status Display")] + [SerializeField] private TextMeshProUGUI statusText; + [SerializeField] private TextMeshProUGUI logText; + [SerializeField] private ScrollRect logScrollRect; + + [Header("Input Fields")] + [SerializeField] private TMP_InputField characterIdInput; + [SerializeField] private TMP_InputField userIdInput; + [SerializeField] private TMP_InputField messageInput; + + [Header("Test Settings")] + [SerializeField] private Toggle autoTestToggle; + [SerializeField] private Slider testIntervalSlider; + [SerializeField] private TextMeshProUGUI intervalText; + + private NetworkTestManager _testManager; + private bool _isAutoTestRunning = false; + + private void Start() + { + _testManager = FindFirstObjectByType(); + if (_testManager == null) + { + Debug.LogError("NetworkTestManager를 찾을 수 없습니다!"); + return; + } + + InitializeUI(); + UpdateStatus("대기 중..."); + } + + private void InitializeUI() + { + // 버튼 이벤트 연결 + if (connectButton != null) + connectButton.onClick.AddListener(OnConnectButtonClicked); + + if (disconnectButton != null) + disconnectButton.onClick.AddListener(OnDisconnectButtonClicked); + + if (chatRequestButton != null) + chatRequestButton.onClick.AddListener(OnChatRequestButtonClicked); + + if (characterInfoButton != null) + characterInfoButton.onClick.AddListener(OnCharacterInfoButtonClicked); + + if (webSocketMessageButton != null) + webSocketMessageButton.onClick.AddListener(OnWebSocketMessageButtonClicked); + + if (fullTestButton != null) + fullTestButton.onClick.AddListener(OnFullTestButtonClicked); + + if (dummyClientTestButton != null) + dummyClientTestButton.onClick.AddListener(OnDummyClientTestButtonClicked); + + if (autoTestButton != null) + autoTestButton.onClick.AddListener(OnAutoTestButtonClicked); + + // 초기값 설정 + if (characterIdInput != null) + characterIdInput.text = "44444444-4444-4444-4444-444444444444"; // 제로 + + if (userIdInput != null) + userIdInput.text = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"; + + if (messageInput != null) + messageInput.text = "안녕하세요! 테스트 메시지입니다."; + + // 자동 테스트 설정 + if (autoTestToggle != null) + { + autoTestToggle.isOn = _testManager.AutoTest; + autoTestToggle.onValueChanged.AddListener(OnAutoTestToggleChanged); + } + + if (testIntervalSlider != null) + { + testIntervalSlider.minValue = 5f; + testIntervalSlider.maxValue = 30f; + testIntervalSlider.value = _testManager.TestInterval; + testIntervalSlider.onValueChanged.AddListener(OnTestIntervalChanged); + UpdateIntervalText(); + } + + // 초기 버튼 상태 설정 + UpdateButtonStates(false); + } + + private void UpdateButtonStates(bool isConnected) + { + if (connectButton != null) + connectButton.interactable = !isConnected; + + if (disconnectButton != null) + disconnectButton.interactable = isConnected; + + if (chatRequestButton != null) + chatRequestButton.interactable = isConnected; + + if (characterInfoButton != null) + characterInfoButton.interactable = true; // HTTP 요청은 연결 없이도 가능 + + if (webSocketMessageButton != null) + webSocketMessageButton.interactable = isConnected; + + if (fullTestButton != null) + fullTestButton.interactable = !_isAutoTestRunning; + + if (dummyClientTestButton != null) + dummyClientTestButton.interactable = !_isAutoTestRunning; + + if (autoTestButton != null) + { + autoTestButton.interactable = !_isAutoTestRunning; + autoTestButton.GetComponentInChildren().text = + _isAutoTestRunning ? "자동 테스트 중지" : "자동 테스트 시작"; + } + } + + private void UpdateStatus(string status) + { + if (statusText != null) + { + statusText.text = $"상태: {status}"; + } + } + + private void AddLog(string message) + { + if (logText != null) + { + logText.text += $"[{System.DateTime.Now:HH:mm:ss}] {message}\n"; + + // 스크롤을 맨 아래로 이동 + if (logScrollRect != null) + { + Canvas.ForceUpdateCanvases(); + logScrollRect.verticalNormalizedPosition = 0f; + } + } + } + + private void UpdateIntervalText() + { + if (intervalText != null && testIntervalSlider != null) + { + intervalText.text = $"테스트 간격: {testIntervalSlider.value:F1}초"; + } + } + + #region Button Event Handlers + + private void OnConnectButtonClicked() + { + AddLog("WebSocket 연결 시도 (더미 클라이언트 방식)..."); + UpdateStatus("연결 중..."); + _testManager.ConnectWebSocket(); + } + + private void OnDisconnectButtonClicked() + { + AddLog("WebSocket 연결 해제..."); + UpdateStatus("연결 해제 중..."); + _testManager.DisconnectWebSocket(); + } + + private void OnChatRequestButtonClicked() + { + AddLog("HTTP 채팅 요청 전송 (더미 클라이언트 방식)..."); + UpdateStatus("채팅 요청 중..."); + _testManager.SendChatRequest(); + } + + private void OnCharacterInfoButtonClicked() + { + AddLog("HTTP 캐릭터 정보 요청..."); + UpdateStatus("캐릭터 정보 요청 중..."); + _testManager.GetCharacterInfo(); + } + + private void OnWebSocketMessageButtonClicked() + { + string message = messageInput != null ? messageInput.text : "테스트 메시지"; + AddLog($"WebSocket 메시지 전송: {message}"); + UpdateStatus("WebSocket 메시지 전송 중..."); + _testManager.SendWebSocketMessage(); + } + + private void OnFullTestButtonClicked() + { + AddLog("전체 테스트 시작..."); + UpdateStatus("전체 테스트 실행 중..."); + _testManager.RunFullTest(); + } + + private void OnDummyClientTestButtonClicked() + { + AddLog("더미 클라이언트 방식 전체 테스트 시작..."); + UpdateStatus("더미 클라이언트 테스트 실행 중..."); + _testManager.RunDummyClientTest(); + } + + private void OnAutoTestButtonClicked() + { + if (!_isAutoTestRunning) + { + _isAutoTestRunning = true; + AddLog("자동 테스트 시작 (더미 클라이언트 방식)..."); + UpdateStatus("자동 테스트 실행 중..."); + UpdateButtonStates(true); + + // 자동 테스트 시작 + _testManager.AutoTest = true; + _testManager.StartAutoTestFromUI(); + } + else + { + _isAutoTestRunning = false; + AddLog("자동 테스트 중지..."); + UpdateStatus("대기 중..."); + UpdateButtonStates(false); + + // 자동 테스트 중지 + _testManager.AutoTest = false; + } + } + + private void OnAutoTestToggleChanged(bool isOn) + { + _testManager.AutoTest = isOn; + AddLog($"자동 테스트 설정: {(isOn ? "활성화" : "비활성화")}"); + } + + private void OnTestIntervalChanged(float value) + { + _testManager.TestInterval = value; + UpdateIntervalText(); + AddLog($"테스트 간격 변경: {value:F1}초"); + } + + #endregion + + #region Public Methods for External Updates + + public void OnWebSocketConnected() + { + UpdateStatus("WebSocket 연결됨"); + UpdateButtonStates(true); + AddLog("✅ WebSocket 연결 성공!"); + } + + public void OnWebSocketDisconnected() + { + UpdateStatus("WebSocket 연결 해제됨"); + UpdateButtonStates(false); + AddLog("🔌 WebSocket 연결 해제됨!"); + } + + public void OnWebSocketError(string error) + { + UpdateStatus("WebSocket 오류"); + AddLog($"❌ WebSocket 오류: {error}"); + } + + public void OnChatMessageReceived(string message) + { + AddLog($"💬 채팅 메시지 수신: {message}"); + } + + public void OnSessionIdMessageReceived(string sessionId) + { + AddLog($"🆔 세션 ID 수신: {sessionId}"); + } + + public void OnHttpRequestSuccess(string operation) + { + AddLog($"✅ HTTP {operation} 성공!"); + } + + public void OnHttpRequestFailed(string operation, string error) + { + AddLog($"❌ HTTP {operation} 실패: {error}"); + } + + public void OnReconnectAttempt(int attempt, int maxAttempts) + { + AddLog($"🔄 재연결 시도 {attempt}/{maxAttempts}"); + UpdateStatus($"재연결 시도 중... ({attempt}/{maxAttempts})"); + } + + public void OnReconnectFailed() + { + AddLog("❌ 최대 재연결 시도 횟수 초과"); + UpdateStatus("재연결 실패"); + } + + #endregion + + private void OnDestroy() + { + // 버튼 이벤트 해제 + if (connectButton != null) + connectButton.onClick.RemoveListener(OnConnectButtonClicked); + + if (disconnectButton != null) + disconnectButton.onClick.RemoveListener(OnDisconnectButtonClicked); + + if (chatRequestButton != null) + chatRequestButton.onClick.RemoveListener(OnChatRequestButtonClicked); + + if (characterInfoButton != null) + characterInfoButton.onClick.RemoveListener(OnCharacterInfoButtonClicked); + + if (webSocketMessageButton != null) + webSocketMessageButton.onClick.RemoveListener(OnWebSocketMessageButtonClicked); + + if (fullTestButton != null) + fullTestButton.onClick.RemoveListener(OnFullTestButtonClicked); + + if (dummyClientTestButton != null) + dummyClientTestButton.onClick.RemoveListener(OnDummyClientTestButtonClicked); + + if (autoTestButton != null) + autoTestButton.onClick.RemoveListener(OnAutoTestButtonClicked); + + if (autoTestToggle != null) + autoTestToggle.onValueChanged.RemoveListener(OnAutoTestToggleChanged); + + if (testIntervalSlider != null) + testIntervalSlider.onValueChanged.RemoveListener(OnTestIntervalChanged); + } + } +} \ No newline at end of file diff --git a/Assets/Tests/Runtime/NetworkTestUI.cs.meta b/Assets/Tests/Runtime/NetworkTestUI.cs.meta new file mode 100644 index 0000000..492659f --- /dev/null +++ b/Assets/Tests/Runtime/NetworkTestUI.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 729ea7f71af70c8429f9b8bc96104350 \ No newline at end of file diff --git a/Assets/Tests/Sences.meta b/Assets/Tests/Sences.meta new file mode 100644 index 0000000..53f0200 --- /dev/null +++ b/Assets/Tests/Sences.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 77f308e5225109541b47748e59a0fd92 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Sences/NetworkTestScene.unity b/Assets/Tests/Sences/NetworkTestScene.unity new file mode 100644 index 0000000..0d1d109 --- /dev/null +++ b/Assets/Tests/Sences/NetworkTestScene.unity @@ -0,0 +1,423 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 10 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 3 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 13 + m_BakeOnSceneLoad: 0 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 0 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &519420028 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 519420032} + - component: {fileID: 519420031} + - component: {fileID: 519420029} + - component: {fileID: 519420033} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!81 &519420029 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 519420028} + m_Enabled: 1 +--- !u!20 &519420031 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 519420028} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 0 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &519420032 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 519420028} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1, z: -10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &519420033 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 519420028} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3} + m_Name: + m_EditorClassIdentifier: + m_RenderShadows: 1 + m_RequiresDepthTextureOption: 2 + m_RequiresOpaqueTextureOption: 2 + m_CameraType: 0 + m_Cameras: [] + m_RendererIndex: -1 + m_VolumeLayerMask: + serializedVersion: 2 + m_Bits: 1 + m_VolumeTrigger: {fileID: 0} + m_VolumeFrameworkUpdateModeOption: 2 + m_RenderPostProcessing: 0 + m_Antialiasing: 0 + m_AntialiasingQuality: 2 + m_StopNaN: 0 + m_Dithering: 0 + m_ClearDepth: 1 + m_AllowXRRendering: 1 + m_AllowHDROutput: 1 + m_UseScreenCoordOverride: 0 + m_ScreenSizeOverride: {x: 0, y: 0, z: 0, w: 0} + m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0} + m_RequiresDepthTexture: 0 + m_RequiresColorTexture: 0 + m_Version: 2 + m_TaaSettings: + m_Quality: 3 + m_FrameInfluence: 0.1 + m_JitterScale: 1 + m_MipBias: 0 + m_VarianceClampScale: 0.9 + m_ContrastAdaptiveSharpening: 0 +--- !u!1001 &757794372 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 0} + m_Modifications: + - target: {fileID: 1234567890, guid: 39117f8af9ef0d742ac7d549538819b1, type: 3} + propertyPath: m_Name + value: NetworkTestCanvas + objectReference: {fileID: 0} + - target: {fileID: 1234567891, guid: 39117f8af9ef0d742ac7d549538819b1, type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1234567891, guid: 39117f8af9ef0d742ac7d549538819b1, type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1234567891, guid: 39117f8af9ef0d742ac7d549538819b1, type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1234567891, guid: 39117f8af9ef0d742ac7d549538819b1, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 1234567891, guid: 39117f8af9ef0d742ac7d549538819b1, type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1234567891, guid: 39117f8af9ef0d742ac7d549538819b1, type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1234567891, guid: 39117f8af9ef0d742ac7d549538819b1, type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1234567891, guid: 39117f8af9ef0d742ac7d549538819b1, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1234567891, guid: 39117f8af9ef0d742ac7d549538819b1, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1234567891, guid: 39117f8af9ef0d742ac7d549538819b1, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: + - targetCorrespondingSourceObject: {fileID: 1234567890, guid: 39117f8af9ef0d742ac7d549538819b1, type: 3} + insertIndex: -1 + addedObject: {fileID: 1075127431} + m_SourcePrefab: {fileID: 100100000, guid: 39117f8af9ef0d742ac7d549538819b1, type: 3} +--- !u!1 &1075127430 stripped +GameObject: + m_CorrespondingSourceObject: {fileID: 1234567890, guid: 39117f8af9ef0d742ac7d549538819b1, type: 3} + m_PrefabInstance: {fileID: 757794372} + m_PrefabAsset: {fileID: 0} +--- !u!114 &1075127431 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1075127430} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3} + m_Name: + m_EditorClassIdentifier: + m_RenderShadows: 1 + m_RequiresDepthTextureOption: 2 + m_RequiresOpaqueTextureOption: 2 + m_CameraType: 0 + m_Cameras: [] + m_RendererIndex: -1 + m_VolumeLayerMask: + serializedVersion: 2 + m_Bits: 1 + m_VolumeTrigger: {fileID: 0} + m_VolumeFrameworkUpdateModeOption: 2 + m_RenderPostProcessing: 0 + m_Antialiasing: 0 + m_AntialiasingQuality: 2 + m_StopNaN: 0 + m_Dithering: 0 + m_ClearDepth: 1 + m_AllowXRRendering: 1 + m_AllowHDROutput: 1 + m_UseScreenCoordOverride: 0 + m_ScreenSizeOverride: {x: 0, y: 0, z: 0, w: 0} + m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0} + m_RequiresDepthTexture: 0 + m_RequiresColorTexture: 0 + m_Version: 2 + m_TaaSettings: + m_Quality: 3 + m_FrameInfluence: 0.1 + m_JitterScale: 1 + m_MipBias: 0 + m_VarianceClampScale: 0.9 + m_ContrastAdaptiveSharpening: 0 +--- !u!1 &1234567890 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1234567891} + - component: {fileID: 1234567892} + m_Layer: 0 + m_Name: NetworkTestManager + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1234567891 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1234567890} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1234567892 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1234567890} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0a3e75ea471e5f47b758f5a032a094e, type: 3} + m_Name: + m_EditorClassIdentifier: + testSessionId: test-session-123 + testCharacterId: 44444444-4444-4444-4444-444444444444 + testUserId: bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb + autoTest: 1 + testInterval: 1 +--- !u!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 519420032} + - {fileID: 1234567891} + - {fileID: 757794372} diff --git a/Assets/Tests/Sences/NetworkTestScene.unity.meta b/Assets/Tests/Sences/NetworkTestScene.unity.meta new file mode 100644 index 0000000..bf59580 --- /dev/null +++ b/Assets/Tests/Sences/NetworkTestScene.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 6877c3ec4230db044838b9e74fb80cba +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/UI.meta b/Assets/Tests/UI.meta new file mode 100644 index 0000000..05b6495 --- /dev/null +++ b/Assets/Tests/UI.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e536c7952bfc22f49b22d94b7f4dcb8c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/UI/NetworkTestCanvas.prefab b/Assets/Tests/UI/NetworkTestCanvas.prefab new file mode 100644 index 0000000..6454229 --- /dev/null +++ b/Assets/Tests/UI/NetworkTestCanvas.prefab @@ -0,0 +1,221 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &1234567890 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2572838951417541798} + - component: {fileID: 1234567892} + - component: {fileID: 1234567893} + - component: {fileID: 3180861085867238070} + m_Layer: 5 + m_Name: NetworkTestCanvas + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2572838951417541798 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1234567890} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 0, y: 0, z: 0} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1234567896} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0, y: 0} +--- !u!20 &1234567892 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1234567890} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 0 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 0 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!223 &1234567893 +Canvas: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1234567890} + m_Enabled: 1 + serializedVersion: 3 + m_RenderMode: 0 + m_Camera: {fileID: 1234567892} + m_PlaneDistance: 100 + m_PixelPerfect: 0 + m_ReceivesEvents: 1 + m_OverrideSorting: 0 + m_OverridePixelPerfect: 0 + m_SortingBucketNormalizedSize: 0 + m_VertexColorAlwaysGammaSpace: 0 + m_AdditionalShaderChannelsFlag: 0 + m_UpdateRectTransformForStandalone: 0 + m_SortingLayerID: 0 + m_SortingOrder: 0 + m_TargetDisplay: 0 +--- !u!114 &3180861085867238070 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1234567890} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3} + m_Name: + m_EditorClassIdentifier: + m_RenderShadows: 1 + m_RequiresDepthTextureOption: 2 + m_RequiresOpaqueTextureOption: 2 + m_CameraType: 0 + m_Cameras: [] + m_RendererIndex: -1 + m_VolumeLayerMask: + serializedVersion: 2 + m_Bits: 1 + m_VolumeTrigger: {fileID: 0} + m_VolumeFrameworkUpdateModeOption: 2 + m_RenderPostProcessing: 0 + m_Antialiasing: 0 + m_AntialiasingQuality: 2 + m_StopNaN: 0 + m_Dithering: 0 + m_ClearDepth: 1 + m_AllowXRRendering: 1 + m_AllowHDROutput: 1 + m_UseScreenCoordOverride: 0 + m_ScreenSizeOverride: {x: 0, y: 0, z: 0, w: 0} + m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0} + m_RequiresDepthTexture: 0 + m_RequiresColorTexture: 0 + m_Version: 2 + m_TaaSettings: + m_Quality: 3 + m_FrameInfluence: 0.1 + m_JitterScale: 1 + m_MipBias: 0 + m_VarianceClampScale: 0.9 + m_ContrastAdaptiveSharpening: 0 +--- !u!1 &1234567894 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1234567896} + - component: {fileID: 9090619396326344028} + m_Layer: 5 + m_Name: TestPanel + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1234567896 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1234567894} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 2572838951417541798} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &9090619396326344028 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1234567894} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 729ea7f71af70c8429f9b8bc96104350, type: 3} + m_Name: + m_EditorClassIdentifier: + connectButton: {fileID: 0} + disconnectButton: {fileID: 0} + chatRequestButton: {fileID: 0} + characterInfoButton: {fileID: 0} + webSocketMessageButton: {fileID: 0} + fullTestButton: {fileID: 0} + autoTestButton: {fileID: 0} + statusText: {fileID: 0} + logText: {fileID: 0} + logScrollRect: {fileID: 0} + sessionIdInput: {fileID: 0} + characterIdInput: {fileID: 0} + userIdInput: {fileID: 0} + messageInput: {fileID: 0} diff --git a/Assets/Tests/UI/NetworkTestCanvas.prefab.meta b/Assets/Tests/UI/NetworkTestCanvas.prefab.meta new file mode 100644 index 0000000..48cb39b --- /dev/null +++ b/Assets/Tests/UI/NetworkTestCanvas.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 39117f8af9ef0d742ac7d549538819b1 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/manifest.json b/Packages/manifest.json index 0f53190..22f7d83 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -1,5 +1,7 @@ { "dependencies": { + "com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask", + "com.unity.nuget.newtonsoft-json": "3.2.1", "com.unity.collab-proxy": "2.8.2", "com.unity.feature.2d": "2.0.1", "com.unity.ide.rider": "3.0.36", @@ -40,8 +42,6 @@ "com.unity.modules.unitywebrequestwww": "1.0.0", "com.unity.modules.vehicles": "1.0.0", "com.unity.modules.video": "1.0.0", - "com.unity.modules.vr": "1.0.0", - "com.unity.modules.wind": "1.0.0", - "com.unity.modules.xr": "1.0.0" + "com.unity.modules.wind": "1.0.0" } } diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index dc200af..c643a11 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -1,5 +1,12 @@ { "dependencies": { + "com.cysharp.unitask": { + "version": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask", + "depth": 0, + "source": "git", + "dependencies": {}, + "hash": "f213ff497e4ff462a77319cf677cf20cc0860ca9" + }, "com.unity.2d.animation": { "version": "10.2.1", "depth": 1, @@ -191,6 +198,13 @@ "dependencies": {}, "url": "https://packages.unity.com" }, + "com.unity.nuget.newtonsoft-json": { + "version": "3.2.1", + "depth": 0, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, "com.unity.render-pipelines.core": { "version": "17.0.4", "depth": 1, @@ -404,14 +418,6 @@ "com.unity.modules.imageconversion": "1.0.0" } }, - "com.unity.modules.subsystems": { - "version": "1.0.0", - "depth": 1, - "source": "builtin", - "dependencies": { - "com.unity.modules.jsonserialize": "1.0.0" - } - }, "com.unity.modules.terrain": { "version": "1.0.0", "depth": 0, @@ -531,31 +537,11 @@ "com.unity.modules.unitywebrequest": "1.0.0" } }, - "com.unity.modules.vr": { - "version": "1.0.0", - "depth": 0, - "source": "builtin", - "dependencies": { - "com.unity.modules.jsonserialize": "1.0.0", - "com.unity.modules.physics": "1.0.0", - "com.unity.modules.xr": "1.0.0" - } - }, "com.unity.modules.wind": { "version": "1.0.0", "depth": 0, "source": "builtin", "dependencies": {} - }, - "com.unity.modules.xr": { - "version": "1.0.0", - "depth": 0, - "source": "builtin", - "dependencies": { - "com.unity.modules.physics": "1.0.0", - "com.unity.modules.jsonserialize": "1.0.0", - "com.unity.modules.subsystems": "1.0.0" - } } } } diff --git a/ProjectSettings/ProjectSettings.asset b/ProjectSettings/ProjectSettings.asset index 8c4ad5b..6286a4b 100644 --- a/ProjectSettings/ProjectSettings.asset +++ b/ProjectSettings/ProjectSettings.asset @@ -776,6 +776,6 @@ PlayerSettings: hmiLoadingImage: {fileID: 0} platformRequiresReadableAssets: 0 virtualTexturingSupportEnabled: 0 - insecureHttpOption: 0 + insecureHttpOption: 2 androidVulkanDenyFilterList: [] androidVulkanAllowFilterList: [] diff --git a/ProjectSettings/SceneTemplateSettings.json b/ProjectSettings/SceneTemplateSettings.json new file mode 100644 index 0000000..ede5887 --- /dev/null +++ b/ProjectSettings/SceneTemplateSettings.json @@ -0,0 +1,121 @@ +{ + "templatePinStates": [], + "dependencyTypeInfos": [ + { + "userAdded": false, + "type": "UnityEngine.AnimationClip", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEditor.Animations.AnimatorController", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.AnimatorOverrideController", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEditor.Audio.AudioMixerController", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.ComputeShader", + "defaultInstantiationMode": 1 + }, + { + "userAdded": false, + "type": "UnityEngine.Cubemap", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.GameObject", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEditor.LightingDataAsset", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.LightingSettings", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.Material", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEditor.MonoScript", + "defaultInstantiationMode": 1 + }, + { + "userAdded": false, + "type": "UnityEngine.PhysicsMaterial", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.PhysicsMaterial2D", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.Rendering.PostProcessing.PostProcessProfile", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.Rendering.PostProcessing.PostProcessResources", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.Rendering.VolumeProfile", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEditor.SceneAsset", + "defaultInstantiationMode": 1 + }, + { + "userAdded": false, + "type": "UnityEngine.Shader", + "defaultInstantiationMode": 1 + }, + { + "userAdded": false, + "type": "UnityEngine.ShaderVariantCollection", + "defaultInstantiationMode": 1 + }, + { + "userAdded": false, + "type": "UnityEngine.Texture", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.Texture2D", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.Timeline.TimelineAsset", + "defaultInstantiationMode": 0 + } + ], + "defaultDependencyTypeInfo": { + "userAdded": false, + "type": "", + "defaultInstantiationMode": 1 + }, + "newSceneOverride": 0 +} \ No newline at end of file