using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Text; using System.Threading.Tasks; using UnityEngine; //Зависит от SettingsManager [System.Serializable] public class ServerResponse { public string message; public string timestamp; } public class WebServer : MonoBehaviour { private HttpListener listener; private bool isRunning = true; private static byte[] css; [Header("Auth")] [SerializeField] private string adminPasswordFallback = "1234"; // если файла нет [SerializeField] private string passwordFileName = "admin_password.txt"; // лежит в persistentDataPath private string AdminPassword; public List settingActions; async void Start() { if (ExternalFilesGetter.NeedToStartServer() == false) { //Debug.Log("Web сервер не запущен (по настройке)"); return; } AdminPassword = AuthManager.LoadPasswordOrFallback(adminPasswordFallback, passwordFileName); LoadCss(); settingActions = GameManager.instance.AdminPanel.GetComponent().settingActions; await StartWebServerAsync(); } async Task StartWebServerAsync() { listener = new HttpListener(); listener.Prefixes.Add("http://localhost:8080/"); listener.Prefixes.Add("http://*:8080/"); listener.Start(); Debug.Log("Async Web сервер запущен"); while (isRunning) { try { var context = await listener.GetContextAsync(); _ = Task.Run(() => ProcessRequestAsync(context)); } catch (Exception e) { Debug.LogError($"Ошибка: {e.Message}"); } } } async Task ProcessRequestAsync(HttpListenerContext context) { try { string path = context.Request.Url.LocalPath; string method = context.Request.HttpMethod; // Debug.Log($"Запрос: {method} {path}"); // Публичные маршруты (без авторизации) if (method == "GET" && path == "/style.css") { await HandleCssRequestAsync(context); return; } if (method == "GET" && path == "/login") { SendHtmlResponse(context, HtmlBuilder.GetLoginPage()); return; } if (method == "POST" && path == "/api/login") { await HandlePostLoginAsync(context); return; } // Дальше всё — только с авторизацией if (!AuthManager.IsAuthorized(context.Request, AdminPassword)) { // красивый редирект на /login Redirect(context, "/login"); return; } // Главная страница if (method == "GET" && path == "/") { await HandleGetSettingsAsync(context); return; } // Рилтайм апдейт 1 настройки if (method == "POST" && path == "/api/setting") { await HandlePostSingleSettingAsync(context); return; } // Кнопка "Добавить монеты" if (method == "POST" && path == "/api/coins") { await HandlePostCoinsAsync(context); return; } // LEGACY (оставил, если вдруг где-то дергается) if (method == "POST" && path == "/upd") { await HandlePostUpdateAsync(context); return; } if (method == "POST" && path == "/api/action") { await HandlePostActionAsync(context); return; } SendHtmlResponse(context, "

404 Not Found

", 404); } catch (Exception ex) { Debug.LogError($"Error processing request: {ex.Message}"); SendHtmlResponse(context, $"

Error: {ex.Message}

", 500); } } async Task HandleGetSettingsAsync(HttpListenerContext context) { await Task.Run(() => { try { var stopwatch = System.Diagnostics.Stopwatch.StartNew(); var htmlTask = Task.Run(() => { var actionsSnapshot = settingActions != null ? new List(settingActions) : new List(); return HtmlBuilder.GetSettingsPage(SettingsManager.Container.Settings, actionsSnapshot); }); if (htmlTask.Wait(TimeSpan.FromSeconds(5))) { string htmlContent = htmlTask.Result; stopwatch.Stop(); if (string.IsNullOrEmpty(htmlContent)) htmlContent = "

Error: Method returned empty string

"; SendHtmlResponse(context, htmlContent); } else { Debug.LogError("HTML generation TIMEOUT (5 seconds)!"); SendHtmlResponse(context, "

Error: Settings generation timeout

", 500); } } catch (Exception ex) { Debug.LogError($"Error in settings html: {ex.Message}"); SendHtmlResponse(context, $"

Error: {ex.Message}

", 500); } }); } async Task HandleCssRequestAsync(HttpListenerContext context) { context.Response.ContentType = "text/css; charset=utf-8"; context.Response.ContentLength64 = css.Length; await context.Response.OutputStream.WriteAsync(css, 0, css.Length); context.Response.OutputStream.Close(); } private static void LoadCss() { TextAsset cssAsset = Resources.Load("style"); if (cssAsset != null) { css = Encoding.UTF8.GetBytes(cssAsset.text); Debug.Log("CSS OK"); } else { Debug.LogWarning("CSS NOT OK"); css = Array.Empty(); } } // ========================= // AUTH // ========================= async Task HandlePostLoginAsync(HttpListenerContext context) { try { string body = await ReadBodyAsStringAsync(context.Request); // принимаем и form-url-encoded, и json string pass = AuthManager.ExtractPassword(body, context.Request.ContentType); if (string.IsNullOrEmpty(pass)) { SendJson(context, 400, "{\"ok\":false,\"message\":\"Пароль пустой\"}"); return; } if (!AuthManager.VerifyPassword(pass, AdminPassword)) { SendJson(context, 401, "{\"ok\":false,\"message\":\"Неверный пароль\"}"); return; } string token = AuthManager.GenerateToken(AdminPassword, daysValid: 30); AuthManager.SetAuthCookie(context.Response, token, daysValid: 30); SendJson(context, 200, "{\"ok\":true}"); } catch (Exception ex) { Debug.LogError($"Login error: {ex.Message}"); SendJson(context, 500, "{\"ok\":false,\"message\":\"Server error\"}"); } } // ========================= // REALTIME SETTINGS // ========================= [Serializable] private class SettingUpdateDto { public string id; public string value; } async Task HandlePostSingleSettingAsync(HttpListenerContext context) { try { string body = await ReadBodyAsStringAsync(context.Request); // Ждем JSON: {"id":"SETTING_ID","value":"on/off/123/1.5/SomeString"} var dto = JsonUtility.FromJson(body); if (dto == null || string.IsNullOrEmpty(dto.id)) { SendJson(context, 400, "{\"ok\":false,\"message\":\"Bad payload\"}"); return; } var (ok, log) = SettingsApplier.ApplySettings( new Dictionary { { dto.id, dto.value ?? "" } }, assumeMissingBoolOff: false // важно! иначе вырубишь все остальные bool ); if (!ok) { SendJson(context, 400, "{\"ok\":false,\"message\":\"Apply failed\"}"); return; } // применяем на главном потоке UnityMainThreadDispatcher.Instance.Enqueue(() => SettingsManager.OnSettingsChanged?.Invoke()); UnityMainThreadDispatcher.Instance.Enqueue(() => GameManager.instance.AdminPanel .GetComponent().RefreshViews()); SendJson(context, 200, "{\"ok\":true}"); } catch (Exception ex) { Debug.LogError($"POST /api/setting error: {ex.Message}\n{ex.StackTrace}"); SendJson(context, 500, "{\"ok\":false,\"message\":\"Server error\"}"); } } // ========================= // COINS HANDLER // ========================= [Serializable] private class CoinsDto { public int amount; } async Task HandlePostCoinsAsync(HttpListenerContext context) { try { string body = await ReadBodyAsStringAsync(context.Request); var dto = JsonUtility.FromJson(body); int amount = dto?.amount ?? 0; if (amount <= 0) { SendJson(context, 400, "{\"ok\":false,\"message\":\"amount must be > 0\"}"); return; } // ЗАГОТОВКА: сюда ты впишешь реальную логику UnityMainThreadDispatcher.Instance.Enqueue(() => OnAddCoins(amount)); SendJson(context, 200, "{\"ok\":true}"); } catch (Exception ex) { Debug.LogError($"POST /api/coins error: {ex.Message}\n{ex.StackTrace}"); SendJson(context, 500, "{\"ok\":false,\"message\":\"Server error\"}"); } } async Task HandlePostActionAsync(HttpListenerContext context) { try { string body = await ReadBodyAsStringAsync(context.Request); var dto = JsonUtility.FromJson(body); string name = dto?.name ?? ""; if (string.IsNullOrWhiteSpace(name)) { SendJson(context, 400, "{\"ok\":false,\"message\":\"name is empty\"}"); return; } bool exists = settingActions != null && settingActions.Exists(a => a != null && a.Name == name); if (!exists) { SendJson(context, 404, "{\"ok\":false,\"message\":\"action not found\"}"); return; } // Важно: UnityEvent только на главном потоке UnityMainThreadDispatcher.Instance.Enqueue(() => ExecuteSettingAction(name)); SendJson(context, 200, "{\"ok\":true}"); } catch (Exception ex) { Debug.LogError($"POST /api/action error: {ex.Message}\n{ex.StackTrace}"); SendJson(context, 500, "{\"ok\":false,\"message\":\"Server error\"}"); } } private void OnAddCoins(int amount) => SingleWallet.AddCreditNoCounters(amount); // ========================= // LEGACY /upd (как было) // ========================= async Task HandlePostUpdateAsync(HttpListenerContext context) { try { Encoding encoding = Encoding.UTF8; string contentType = context.Request.ContentType?.ToLower() ?? ""; if (contentType.Contains("charset=")) { try { string charset = contentType.Split("charset=")[1].Trim().Split(';')[0]; encoding = Encoding.GetEncoding(charset); } catch { encoding = Encoding.UTF8; } } string requestBody; using (var reader = new StreamReader(context.Request.InputStream, encoding)) requestBody = await reader.ReadToEndAsync(); var formDict = new Dictionary(); try { var formData = System.Web.HttpUtility.ParseQueryString(requestBody, encoding); foreach (string key in formData.AllKeys) { if (key != null) formDict[key] = formData[key] ?? ""; } } catch (Exception parseEx) { Debug.LogWarning($"ParseQueryString ошибка: {parseEx.Message}"); } SettingsApplier.ApplySettings(formDict, assumeMissingBoolOff: true); UnityMainThreadDispatcher.Instance.Enqueue(() => SettingsManager.OnSettingsChanged?.Invoke()); UnityMainThreadDispatcher.Instance.Enqueue(() => GameManager.instance.AdminPanel.GetComponent().RefreshViews()); SendHtmlResponse(context, "

OK

Back"); } catch (Exception ex) { Debug.LogError($"Ошибка обработки POST /upd: {ex.Message}\n{ex.StackTrace}"); SendHtmlResponse(context, $"

Ошибка: {ex.Message}

", 500); } } // ========================= // Helpers // ========================= private async Task ReadBodyAsStringAsync(HttpListenerRequest request) { Encoding encoding = Encoding.UTF8; // иногда Unity/браузер шлет json без charset — ок if (!string.IsNullOrEmpty(request.ContentType)) { var ct = request.ContentType.ToLower(); if (ct.Contains("charset=")) { try { string charset = ct.Split("charset=")[1].Trim().Split(';')[0]; encoding = Encoding.GetEncoding(charset); } catch { encoding = Encoding.UTF8; } } } using var reader = new StreamReader(request.InputStream, encoding); return await reader.ReadToEndAsync(); } void SendHtmlResponse(HttpListenerContext context, string html, int statusCode = 200) { try { byte[] buffer = Encoding.UTF8.GetBytes(html); context.Response.StatusCode = statusCode; context.Response.ContentType = "text/html; charset=utf-8"; context.Response.ContentLength64 = buffer.Length; context.Response.OutputStream.Write(buffer, 0, buffer.Length); context.Response.OutputStream.Close(); } catch (Exception ex) { Debug.LogError($"Error sending response: {ex.Message}"); } } private void SendJson(HttpListenerContext context, int statusCode, string json) { try { byte[] buffer = Encoding.UTF8.GetBytes(json); context.Response.StatusCode = statusCode; context.Response.ContentType = "application/json; charset=utf-8"; context.Response.ContentLength64 = buffer.Length; context.Response.OutputStream.Write(buffer, 0, buffer.Length); context.Response.OutputStream.Close(); } catch (Exception ex) { Debug.LogError($"Error sending json: {ex.Message}"); } } private void Redirect(HttpListenerContext context, string location, int statusCode = 302) { context.Response.StatusCode = statusCode; context.Response.Headers["Location"] = location; context.Response.OutputStream.Close(); } public void ExecuteSettingAction(string name) { SettingAction action = settingActions.Find(s => s.Name == name); if (action != null) { action.OnPressed?.Invoke(); } else { Debug.Log($"Действие с именем {name} не найдено."); } } void OnDestroy() { isRunning = false; listener?.Stop(); listener?.Close(); } private void OnApplicationQuit() { isRunning = false; listener?.Stop(); listener?.Close(); } [Serializable] private class ActionDto { public string name; } }