465 lines
16 KiB
C#
465 lines
16 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// BossRaid 웹 제어판 서버.
|
|
/// HTTP로 UI를 서빙하고, WebSocket으로 실시간 상태/제어를 처리합니다.
|
|
/// </summary>
|
|
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<Action> _mainThreadActions = new ConcurrentQueue<Action>();
|
|
|
|
// 웹 리소스 (Resources 폴더에서 로드)
|
|
private string _htmlTemplate;
|
|
private string _cssContent;
|
|
private string _jsContent;
|
|
|
|
#endregion
|
|
|
|
#region Unity Messages
|
|
|
|
private void Awake()
|
|
{
|
|
_raidManager = GetComponentInParent<BossRaidManager>();
|
|
if (_raidManager == null)
|
|
_raidManager = GetComponent<BossRaidManager>();
|
|
_audio = GetComponentInParent<BossRaidAudio>();
|
|
if (_audio == null)
|
|
_audio = FindObjectOfType<BossRaidAudio>();
|
|
}
|
|
|
|
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
|
|
|
|
/// <summary>
|
|
/// 모든 클라이언트에 상태 업데이트를 브로드캐스트합니다.
|
|
/// </summary>
|
|
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<BossRaidWebSocketService>("/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<TextAsset>("BossRaidWeb/bossraid_template");
|
|
var css = Resources.Load<TextAsset>("BossRaidWeb/bossraid_style");
|
|
var js = Resources.Load<TextAsset>("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 "<html><body><h1>BossRaid Web Controller</h1><p>Resources/BossRaidWeb/ 리소스를 찾을 수 없습니다.</p></body></html>";
|
|
}
|
|
|
|
#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<int>() ?? -1;
|
|
bool crit = msg["critical"]?.Value<bool>() ?? 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<float>() ?? 1f;
|
|
_raidManager?.SetBossHP(ratio);
|
|
break;
|
|
case "force_phase":
|
|
int phase = msg["phase"]?.Value<int>() ?? 0;
|
|
_raidManager?.ForcePhase(phase);
|
|
break;
|
|
case "force_kill":
|
|
_raidManager?.ForceKill();
|
|
break;
|
|
case "set_hp_lock":
|
|
float lockRatio = msg["ratio"]?.Value<float>() ?? 0f;
|
|
if (_safety != null) _safety.HPLockRatio = lockRatio;
|
|
BroadcastState();
|
|
break;
|
|
case "set_cooldown":
|
|
float cd = msg["value"]?.Value<float>() ?? 0.3f;
|
|
if (_safety != null) _safety.HitCooldown = cd;
|
|
break;
|
|
case "set_damage_cap":
|
|
int cap = msg["value"]?.Value<int>() ?? 0;
|
|
if (_safety != null) _safety.MaxDamagePerHit = cap;
|
|
break;
|
|
case "set_bgm_volume":
|
|
float bgmVol = msg["value"]?.Value<float>() ?? 0.5f;
|
|
if (_audio != null) _audio.BGMVolume = bgmVol;
|
|
BroadcastState();
|
|
break;
|
|
case "set_sfx_volume":
|
|
float sfxVol = msg["value"]?.Value<float>() ?? 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
|
|
}
|