using System; using System.Collections.Generic; using UnityEngine; using NativeWebSocket; using TheIsland.Models; namespace TheIsland.Network { /// /// Singleton WebSocket manager for server communication. /// Handles connection, message parsing, and event dispatching. /// public class NetworkManager : MonoBehaviour { #region Singleton private static NetworkManager _instance; public static NetworkManager Instance { get { if (_instance == null) { _instance = FindFirstObjectByType(); if (_instance == null) { var go = new GameObject("NetworkManager"); _instance = go.AddComponent(); } } return _instance; } } #endregion #region Configuration [Header("Server Configuration")] [SerializeField] private string serverUrl = "ws://localhost:8080/ws"; [SerializeField] private bool autoConnect = true; [SerializeField] private float reconnectDelay = 3f; [Header("User Settings")] [SerializeField] private string username = "UnityPlayer"; #endregion #region Events // Connection events public event Action OnConnected; public event Action OnDisconnected; public event Action OnError; // Game events public event Action> OnAgentsUpdate; public event Action OnAgentSpeak; public event Action OnAgentDied; public event Action OnFeed; public event Action OnTick; public event Action OnSystemMessage; public event Action OnUserUpdate; // New Phase events public event Action OnWeatherChange; public event Action OnPhaseChange; public event Action OnDayChange; public event Action OnHeal; public event Action OnEncourage; public event Action OnTalk; public event Action OnRevive; public event Action OnSocialInteraction; public event Action OnWorldUpdate; public event Action OnGiftEffect; // Phase 8: Gift/Donation effects public event Action OnAgentAction; // Phase 13: Autonomous Actions public event Action OnCraft; // Phase 16: Crafting public event Action OnUseItem; // Phase 16: Using items public event Action OnRandomEvent; // Phase 17-C: Random Events public event Action OnGiveItem; // Phase 23: Item Exchange public event Action OnGroupActivity; // Phase 24: Group Activities public event Action OnVFXEvent; // Phase 8: VFX // AI Director & Narrative Voting (Phase 9) public event Action OnModeChange; public event Action OnNarrativePlot; public event Action OnVoteStarted; public event Action OnVoteUpdate; public event Action OnVoteEnded; public event Action OnVoteResult; public event Action OnResolutionApplied; #endregion #region Private Fields private WebSocket _websocket; private bool _isConnecting; private bool _shouldReconnect = true; private bool _hasNotifiedConnected = false; #endregion #region Properties public bool IsConnected => _websocket?.State == WebSocketState.Open; public string Username { get => username; set => username = value; } public string ServerUrl { get => serverUrl; set => serverUrl = value; } #endregion #region Unity Lifecycle private void Awake() { // Singleton enforcement if (_instance != null && _instance != this) { Destroy(gameObject); return; } _instance = this; DontDestroyOnLoad(gameObject); } private async void Start() { if (autoConnect) { await Connect(); } } private void Update() { // CRITICAL: Dispatch message queue on main thread // NativeWebSocket requires this for callbacks to work #if !UNITY_WEBGL || UNITY_EDITOR if (_websocket != null) { _websocket.DispatchMessageQueue(); // Fallback: Check connection state directly if (_websocket.State == WebSocketState.Open && !_hasNotifiedConnected) { _hasNotifiedConnected = true; Debug.Log("[NetworkManager] Connection detected via state check!"); OnConnected?.Invoke(); } else if (_websocket.State != WebSocketState.Open && _hasNotifiedConnected) { _hasNotifiedConnected = false; } } #endif } private async void OnApplicationQuit() { _shouldReconnect = false; if (_websocket != null) { await _websocket.Close(); } } private void OnDestroy() { _shouldReconnect = false; _websocket?.Close(); } #endregion #region Connection Management public async System.Threading.Tasks.Task Connect() { if (_isConnecting || IsConnected) return; _isConnecting = true; Debug.Log($"[NetworkManager] Connecting to {serverUrl}..."); try { _websocket = new WebSocket(serverUrl); _websocket.OnOpen += HandleOpen; _websocket.OnClose += HandleClose; _websocket.OnError += HandleError; _websocket.OnMessage += HandleMessage; await _websocket.Connect(); } catch (Exception e) { Debug.LogError($"[NetworkManager] Connection failed: {e.Message}"); _isConnecting = false; OnError?.Invoke(e.Message); if (_shouldReconnect) { ScheduleReconnect(); } } } public async void Disconnect() { _shouldReconnect = false; if (_websocket != null) { await _websocket.Close(); } } private void ScheduleReconnect() { if (_shouldReconnect) { Debug.Log($"[NetworkManager] Reconnecting in {reconnectDelay}s..."); Invoke(nameof(TryReconnect), reconnectDelay); } } private async void TryReconnect() { await Connect(); } #endregion #region WebSocket Event Handlers private void HandleOpen() { _isConnecting = false; Debug.Log("[NetworkManager] Connected to server!"); OnConnected?.Invoke(); } private void HandleClose(WebSocketCloseCode code) { _isConnecting = false; Debug.Log($"[NetworkManager] Disconnected (code: {code})"); OnDisconnected?.Invoke(); if (_shouldReconnect && code != WebSocketCloseCode.Normal) { ScheduleReconnect(); } } private void HandleError(string error) { _isConnecting = false; Debug.LogError($"[NetworkManager] WebSocket error: {error}"); OnError?.Invoke(error); } private void HandleMessage(byte[] data) { string json = System.Text.Encoding.UTF8.GetString(data); ProcessMessage(json); } #endregion #region Message Processing private void ProcessMessage(string json) { try { // First, extract the event_type var baseMessage = JsonUtility.FromJson(json); if (string.IsNullOrEmpty(baseMessage.event_type)) { Debug.LogWarning("[NetworkManager] Received message without event_type"); return; } // Extract the data portion using regex (JsonUtility limitation workaround) string dataJson = ExtractDataJson(json); // Dispatch based on event type switch (baseMessage.event_type) { case EventTypes.AGENTS_UPDATE: var agentsData = JsonUtility.FromJson(dataJson); OnAgentsUpdate?.Invoke(agentsData.agents); break; case EventTypes.AGENT_SPEAK: var speakData = JsonUtility.FromJson(dataJson); OnAgentSpeak?.Invoke(speakData); break; case EventTypes.AGENT_DIED: var diedData = JsonUtility.FromJson(dataJson); OnAgentDied?.Invoke(diedData); break; case EventTypes.FEED: var feedData = JsonUtility.FromJson(dataJson); OnFeed?.Invoke(feedData); break; case EventTypes.TICK: var tickData = JsonUtility.FromJson(dataJson); OnTick?.Invoke(tickData); break; case EventTypes.SYSTEM: case EventTypes.ERROR: var sysData = JsonUtility.FromJson(dataJson); OnSystemMessage?.Invoke(sysData); break; case EventTypes.USER_UPDATE: var userData = JsonUtility.FromJson(dataJson); OnUserUpdate?.Invoke(userData); break; case EventTypes.WORLD_UPDATE: var worldData = JsonUtility.FromJson(dataJson); OnWorldUpdate?.Invoke(worldData); break; case EventTypes.WEATHER_CHANGE: var weatherData = JsonUtility.FromJson(dataJson); OnWeatherChange?.Invoke(weatherData); break; case EventTypes.PHASE_CHANGE: var phaseData = JsonUtility.FromJson(dataJson); OnPhaseChange?.Invoke(phaseData); break; case EventTypes.DAY_CHANGE: var dayData = JsonUtility.FromJson(dataJson); OnDayChange?.Invoke(dayData); break; case EventTypes.HEAL: var healData = JsonUtility.FromJson(dataJson); OnHeal?.Invoke(healData); break; case EventTypes.ENCOURAGE: var encourageData = JsonUtility.FromJson(dataJson); OnEncourage?.Invoke(encourageData); break; case EventTypes.TALK: var talkData = JsonUtility.FromJson(dataJson); OnTalk?.Invoke(talkData); break; case EventTypes.REVIVE: case EventTypes.AUTO_REVIVE: var reviveData = JsonUtility.FromJson(dataJson); OnRevive?.Invoke(reviveData); break; case EventTypes.SOCIAL_INTERACTION: var socialData = JsonUtility.FromJson(dataJson); OnSocialInteraction?.Invoke(socialData); break; case EventTypes.GIFT_EFFECT: var giftData = JsonUtility.FromJson(dataJson); OnGiftEffect?.Invoke(giftData); break; case EventTypes.AGENT_ACTION: var actionData = JsonUtility.FromJson(dataJson); OnAgentAction?.Invoke(actionData); break; case EventTypes.CRAFT: var craftData = JsonUtility.FromJson(dataJson); OnCraft?.Invoke(craftData); break; case EventTypes.USE_ITEM: var useItemData = JsonUtility.FromJson(dataJson); OnUseItem?.Invoke(useItemData); break; case EventTypes.COMMENT: // Comments can be logged but typically not displayed in 3D Debug.Log($"[Chat] {json}"); break; case EventTypes.RANDOM_EVENT: var randomEventData = JsonUtility.FromJson(dataJson); OnRandomEvent?.Invoke(randomEventData); Debug.Log($"[Random Event] {randomEventData.event_type}: {randomEventData.message}"); break; case EventTypes.GIVE_ITEM: var giveData = JsonUtility.FromJson(dataJson); OnGiveItem?.Invoke(giveData); break; case EventTypes.GROUP_ACTIVITY: var groupData = JsonUtility.FromJson(dataJson); OnGroupActivity?.Invoke(groupData); break; case EventTypes.VFX_EVENT: var vfxData = JsonUtility.FromJson(dataJson); OnVFXEvent?.Invoke(vfxData); break; // AI Director & Narrative Voting (Phase 9) case EventTypes.MODE_CHANGE: var modeData = JsonUtility.FromJson(dataJson); OnModeChange?.Invoke(modeData); break; case EventTypes.NARRATIVE_PLOT: var plotData = JsonUtility.FromJson(dataJson); OnNarrativePlot?.Invoke(plotData); break; case EventTypes.VOTE_STARTED: var voteStarted = JsonUtility.FromJson(dataJson); OnVoteStarted?.Invoke(voteStarted); break; case EventTypes.VOTE_UPDATE: var voteUpdate = JsonUtility.FromJson(dataJson); OnVoteUpdate?.Invoke(voteUpdate); break; case EventTypes.VOTE_ENDED: var voteEnded = JsonUtility.FromJson(dataJson); OnVoteEnded?.Invoke(voteEnded); break; case EventTypes.VOTE_RESULT: var voteResult = JsonUtility.FromJson(dataJson); OnVoteResult?.Invoke(voteResult); break; case EventTypes.RESOLUTION_APPLIED: var resolution = JsonUtility.FromJson(dataJson); OnResolutionApplied?.Invoke(resolution); break; default: Debug.Log($"[NetworkManager] Unhandled event type: {baseMessage.event_type}"); break; } } catch (Exception e) { Debug.LogError($"[NetworkManager] Failed to process message: {e.Message}\nJSON: {json}"); } } /// /// Extract the "data" object as a JSON string for nested deserialization. /// Uses a balanced bracket approach for better reliability. /// private string ExtractDataJson(string fullJson) { // Find the start of "data": int dataIndex = fullJson.IndexOf("\"data\""); if (dataIndex == -1) return "{}"; // Find the colon after "data" int colonIndex = fullJson.IndexOf(':', dataIndex); if (colonIndex == -1) return "{}"; // Skip whitespace after colon int startIndex = colonIndex + 1; while (startIndex < fullJson.Length && char.IsWhiteSpace(fullJson[startIndex])) { startIndex++; } if (startIndex >= fullJson.Length) return "{}"; char firstChar = fullJson[startIndex]; // Handle object if (firstChar == '{') { return ExtractBalancedBrackets(fullJson, startIndex, '{', '}'); } // Handle array else if (firstChar == '[') { return ExtractBalancedBrackets(fullJson, startIndex, '[', ']'); } // Handle primitive (string, number, bool, null) else { return "{}"; // Primitives not expected for our protocol } } /// /// Extract a balanced bracket section from JSON string. /// Handles nested brackets and escaped strings properly. /// private string ExtractBalancedBrackets(string json, int start, char open, char close) { int depth = 0; bool inString = false; bool escape = false; for (int i = start; i < json.Length; i++) { char c = json[i]; if (escape) { escape = false; continue; } if (c == '\\' && inString) { escape = true; continue; } if (c == '"') { inString = !inString; continue; } if (!inString) { if (c == open) depth++; else if (c == close) { depth--; if (depth == 0) { return json.Substring(start, i - start + 1); } } } } return "{}"; // Fallback if unbalanced } #endregion #region Sending Messages public async void SendCommand(string command) { if (!IsConnected) { Debug.LogWarning("[NetworkManager] Cannot send - not connected"); return; } var message = new ClientMessage { action = "send_comment", payload = new ClientPayload { user = username, message = command } }; string json = JsonUtility.ToJson(message); await _websocket.SendText(json); Debug.Log($"[NetworkManager] Sent: {json}"); } public void FeedAgent(string agentName) { SendCommand($"feed {agentName}"); } public void HealAgent(string agentName) { SendCommand($"heal {agentName}"); } public void EncourageAgent(string agentName) { SendCommand($"encourage {agentName}"); } public void TalkToAgent(string agentName, string topic = "") { string cmd = string.IsNullOrEmpty(topic) ? $"talk {agentName}" : $"talk {agentName} {topic}"; SendCommand(cmd); } public void ReviveAgent(string agentName) { SendCommand($"revive {agentName}"); } public void CheckStatus() { SendCommand("check"); } public void ResetGame() { SendCommand("reset"); } #endregion } }