using System; using System.Collections.Concurrent; using System.Net; using System.Text; using System.Threading; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using UnityEngine; using WebSocketSharp; using WebSocketSharp.Server; namespace Streamingle.Contents.BossRaid { /// /// BossRaid 웹 제어판 서버. /// HTTP로 UI를 서빙하고, WebSocket으로 실시간 상태/제어를 처리합니다. /// public class BossRaidWebServer : MonoBehaviour { #region Fields [Header("서버 설정")] [SerializeField] private int httpPort = 64220; [SerializeField] private int wsPort = 64221; private HttpListener _httpListener; private Thread _httpThread; private WebSocketServer _wsServer; private bool _isRunning; private BossRaidManager _raidManager; private BossController _bossController; private BossRaidSafety _safety; private BossRaidAudio _audio; private readonly ConcurrentQueue _mainThreadActions = new ConcurrentQueue(); // 웹 리소스 (Resources 폴더에서 로드) private string _htmlTemplate; private string _cssContent; private string _jsContent; #endregion #region Unity Messages private void Awake() { _raidManager = GetComponentInParent(); if (_raidManager == null) _raidManager = GetComponent(); _audio = GetComponentInParent(); if (_audio == null) _audio = FindObjectOfType(); } private void Start() { LoadWebResources(); StartServers(); } private void Update() { // 메인 스레드 액션 처리 while (_mainThreadActions.TryDequeue(out var action)) { try { action?.Invoke(); } catch (Exception e) { Debug.LogError($"[BossRaidWeb] 메인 스레드 액션 오류: {e}"); } } } private void OnDestroy() { StopServers(); } #endregion #region Public Methods /// /// 모든 클라이언트에 상태 업데이트를 브로드캐스트합니다. /// public void BroadcastState() { if (_wsServer == null || !_isRunning) return; var state = BuildStateJson(); var json = JsonConvert.SerializeObject(new { type = "state_update", data = state }); try { _wsServer.WebSocketServices["/bossraid"].Sessions.Broadcast(json); } catch (Exception e) { Debug.LogWarning($"[BossRaidWeb] 브로드캐스트 실패: {e.Message}"); } } #endregion #region Private Methods - Server private void StartServers() { try { // === HTTP 서버 (기존 대시보드 서버와 동일 패턴) === _httpListener = new HttpListener(); bool wildcardBound = false; try { _httpListener.Prefixes.Add($"http://+:{httpPort}/"); _httpListener.Start(); wildcardBound = true; Debug.Log($"[BossRaidWeb] HTTP 와일드카드 바인딩 성공 (LAN 접속 가능)"); } catch (Exception) { // 와일드카드 실패 시 리스너 재생성 try { _httpListener.Close(); } catch { } _httpListener = new HttpListener(); _httpListener.Prefixes.Add($"http://localhost:{httpPort}/"); _httpListener.Prefixes.Add($"http://127.0.0.1:{httpPort}/"); string localIP = GetLocalIPAddress(); if (!string.IsNullOrEmpty(localIP)) { try { _httpListener.Prefixes.Add($"http://{localIP}:{httpPort}/"); } catch { } } _httpListener.Start(); Debug.Log($"[BossRaidWeb] HTTP localhost/127.0.0.1 바인딩 성공"); } _isRunning = true; _httpThread = new Thread(HttpListenerLoop) { IsBackground = true }; _httpThread.Start(); // === WebSocket 서버 === _wsServer = new WebSocketServer($"ws://0.0.0.0:{wsPort}"); _wsServer.KeepClean = true; _wsServer.WaitTime = TimeSpan.FromSeconds(3); _wsServer.AddWebSocketService("/bossraid", service => service.Initialize(this)); _wsServer.Start(); Debug.Log($"[BossRaidWeb] 서버 시작 - HTTP: http://localhost:{httpPort} | WS: ws://localhost:{wsPort}/bossraid"); // 이벤트 구독 SubscribeRaidEvents(); } catch (Exception e) { Debug.LogError($"[BossRaidWeb] 서버 시작 실패: {e}"); } } private static string GetLocalIPAddress() { try { foreach (var ni in System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces()) { if (ni.OperationalStatus != System.Net.NetworkInformation.OperationalStatus.Up) continue; if (ni.NetworkInterfaceType == System.Net.NetworkInformation.NetworkInterfaceType.Loopback) continue; foreach (var addr in ni.GetIPProperties().UnicastAddresses) { if (addr.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) return addr.Address.ToString(); } } } catch { } return null; } private void StopServers() { _isRunning = false; UnsubscribeRaidEvents(); try { _httpListener?.Stop(); } catch { } try { _wsServer?.Stop(); } catch { } Debug.Log("[BossRaidWeb] 서버 종료"); } private void HttpListenerLoop() { while (_isRunning && _httpListener != null && _httpListener.IsListening) { try { var context = _httpListener.GetContext(); HandleHttpRequest(context); } catch (HttpListenerException) { break; } catch (Exception e) { if (_isRunning) Debug.LogWarning($"[BossRaidWeb] HTTP 오류: {e.Message}"); } } } private void HandleHttpRequest(HttpListenerContext context) { var request = context.Request; var response = context.Response; string path = request.Url.AbsolutePath.TrimEnd('/'); if (string.IsNullOrEmpty(path) || path == "/index.html") path = "/"; byte[] buffer; string contentType; switch (path) { case "/": contentType = "text/html; charset=utf-8"; buffer = Encoding.UTF8.GetBytes(_htmlTemplate ?? GetFallbackHtml()); break; case "/style.css": contentType = "text/css; charset=utf-8"; buffer = Encoding.UTF8.GetBytes(_cssContent ?? ""); break; case "/script.js": contentType = "application/javascript; charset=utf-8"; string js = (_jsContent ?? "").Replace("{{WS_PORT}}", wsPort.ToString()); buffer = Encoding.UTF8.GetBytes(js); break; default: response.StatusCode = 404; buffer = Encoding.UTF8.GetBytes("Not Found"); contentType = "text/plain"; break; } response.ContentType = contentType; response.Headers.Add("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0"); response.ContentLength64 = buffer.Length; response.OutputStream.Write(buffer, 0, buffer.Length); response.OutputStream.Close(); } private void LoadWebResources() { var html = Resources.Load("BossRaidWeb/bossraid_template"); var css = Resources.Load("BossRaidWeb/bossraid_style"); var js = Resources.Load("BossRaidWeb/bossraid_script"); _htmlTemplate = html != null ? html.text : GetFallbackHtml(); _cssContent = css != null ? css.text : ""; _jsContent = js != null ? js.text : ""; if (html == null) Debug.LogWarning("[BossRaidWeb] HTML 템플릿 로드 실패, 폴백 사용"); } private string GetFallbackHtml() { return "

BossRaid Web Controller

Resources/BossRaidWeb/ 리소스를 찾을 수 없습니다.

"; } #endregion #region Private Methods - Events private void SubscribeRaidEvents() { if (_raidManager == null) return; _raidManager.OnRaidStateChanged += OnRaidStateChanged; // Boss 이벤트는 StartRaid 후에 연결됨 — Update에서 동적으로 체크 InvokeRepeating(nameof(CheckBossEvents), 0.5f, 0.5f); } private void UnsubscribeRaidEvents() { if (_raidManager != null) _raidManager.OnRaidStateChanged -= OnRaidStateChanged; CancelInvoke(nameof(CheckBossEvents)); UnsubscribeBossEvents(); } private void CheckBossEvents() { var newBoss = _raidManager?.Boss; if (newBoss != null && newBoss != _bossController) { UnsubscribeBossEvents(); _bossController = newBoss; _safety = _raidManager.Safety; _bossController.OnHPChanged += OnHPChanged; _bossController.OnDamaged += OnDamaged; _bossController.OnPhaseChanged += OnPhaseChanged; _bossController.OnDeath += OnBossDeath; } } private void UnsubscribeBossEvents() { if (_bossController != null) { _bossController.OnHPChanged -= OnHPChanged; _bossController.OnDamaged -= OnDamaged; _bossController.OnPhaseChanged -= OnPhaseChanged; _bossController.OnDeath -= OnBossDeath; _bossController = null; } } private void OnRaidStateChanged(BossRaidManager.RaidState state) => BroadcastState(); private void OnHPChanged(int current, int max) => BroadcastState(); private void OnDamaged(int dmg, bool crit, int hp, int max) => BroadcastState(); private void OnPhaseChanged(int idx, BossData.PhaseData phase) => BroadcastState(); private void OnBossDeath() => BroadcastState(); #endregion #region Private Methods - State private object BuildStateJson() { var boss = _raidManager?.Boss; return new { raidState = _raidManager?.CurrentState.ToString() ?? "Idle", bossName = boss?.Data?.bossName ?? "", currentHP = boss?.CurrentHP ?? 0, maxHP = boss?.MaxHP ?? 0, hpRatio = boss?.HPRatio ?? 0f, phase = boss?.CurrentPhaseIndex ?? 0, phaseName = boss?.CurrentPhase?.phaseName ?? "", bossState = boss?.CurrentState.ToString() ?? "", isDead = boss?.IsDead ?? false, isPaused = _safety?.IsPaused ?? false, hpLockRatio = _safety?.HPLockRatio ?? 0f, bgmVolume = _audio?.BGMVolume ?? 0.5f, sfxVolume = _audio?.SFXVolume ?? 1f, }; } #endregion #region Internal - WebSocket Message Handling internal void HandleWebSocketMessage(string message) { try { var msg = JObject.Parse(message); string action = msg["action"]?.ToString(); _mainThreadActions.Enqueue(() => { switch (action) { case "start_raid": _raidManager?.StartRaid(); break; case "stop_raid": _raidManager?.StopRaid(); break; case "manual_hit": int dmg = msg["damage"]?.Value() ?? -1; bool crit = msg["critical"]?.Value() ?? false; _raidManager?.ManualHit(dmg, crit); break; case "auto_hit": _raidManager?.ManualHit(); break; case "toggle_pause": _raidManager?.TogglePause(); break; case "set_hp": float ratio = msg["ratio"]?.Value() ?? 1f; _raidManager?.SetBossHP(ratio); break; case "force_phase": int phase = msg["phase"]?.Value() ?? 0; _raidManager?.ForcePhase(phase); break; case "force_kill": _raidManager?.ForceKill(); break; case "set_hp_lock": float lockRatio = msg["ratio"]?.Value() ?? 0f; if (_safety != null) _safety.HPLockRatio = lockRatio; BroadcastState(); break; case "set_cooldown": float cd = msg["value"]?.Value() ?? 0.3f; if (_safety != null) _safety.HitCooldown = cd; break; case "set_damage_cap": int cap = msg["value"]?.Value() ?? 0; if (_safety != null) _safety.MaxDamagePerHit = cap; break; case "set_bgm_volume": float bgmVol = msg["value"]?.Value() ?? 0.5f; if (_audio != null) _audio.BGMVolume = bgmVol; BroadcastState(); break; case "set_sfx_volume": float sfxVol = msg["value"]?.Value() ?? 1f; if (_audio != null) _audio.SFXVolume = sfxVol; BroadcastState(); break; case "get_state": BroadcastState(); break; default: Debug.LogWarning($"[BossRaidWeb] 알 수 없는 액션: {action}"); break; } }); } catch (Exception e) { Debug.LogError($"[BossRaidWeb] 메시지 파싱 오류: {e.Message}"); } } #endregion } #region WebSocket Service public class BossRaidWebSocketService : WebSocketBehavior { private BossRaidWebServer _server; public void Initialize(BossRaidWebServer server) { _server = server; } protected override void OnOpen() { Debug.Log("[BossRaidWeb] 클라이언트 연결"); _server?.BroadcastState(); } protected override void OnMessage(MessageEventArgs e) { _server?.HandleWebSocketMessage(e.Data); } protected override void OnClose(CloseEventArgs e) { Debug.Log($"[BossRaidWeb] 클라이언트 연결 해제: {e.Reason}"); } } #endregion }