From a5b9d70f04262a67538e8095062f6f7deac38a28 Mon Sep 17 00:00:00 2001 From: KoalaTrapLord Date: Wed, 18 Mar 2026 00:55:40 -0400 Subject: [PATCH] Unity U2: Add ApiClient.cs singleton HTTP client for FastAPI backend (Epic #45) --- .../Assets/Scripts/Services/ApiClient.cs | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 unity-client/Assets/Scripts/Services/ApiClient.cs diff --git a/unity-client/Assets/Scripts/Services/ApiClient.cs b/unity-client/Assets/Scripts/Services/ApiClient.cs new file mode 100644 index 0000000..9ba8e1f --- /dev/null +++ b/unity-client/Assets/Scripts/Services/ApiClient.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections; +using System.Text; +using UnityEngine; +using UnityEngine.Networking; + +namespace CommanderAILab.Services +{ + /// + /// Singleton HTTP client for Commander AI Lab FastAPI backend. + /// All REST calls go through this service. + /// + public class ApiClient : MonoBehaviour + { + public static ApiClient Instance { get; private set; } + + [SerializeField] private string baseUrl = "http://localhost:8080"; + [SerializeField] private float timeoutSeconds = 30f; + + public string BaseUrl + { + get => baseUrl; + set + { + baseUrl = value.TrimEnd('/'); + PlayerPrefs.SetString("ApiBaseUrl", baseUrl); + } + } + + private void Awake() + { + if (Instance != null && Instance != this) { Destroy(gameObject); return; } + Instance = this; + DontDestroyOnLoad(gameObject); + baseUrl = PlayerPrefs.GetString("ApiBaseUrl", baseUrl); + } + + // ── Generic Helpers ────────────────────────────────────── + + public Coroutine Get(string path, Action onSuccess, Action onError = null) + { + return StartCoroutine(GetCoroutine(path, onSuccess, onError)); + } + + public Coroutine Post(string path, TReq body, Action onSuccess, Action onError = null) + { + return StartCoroutine(PostCoroutine(path, body, onSuccess, onError)); + } + + private IEnumerator GetCoroutine(string path, Action onSuccess, Action onError) + { + var url = $"{baseUrl}{path}"; + using var request = UnityWebRequest.Get(url); + request.timeout = (int)timeoutSeconds; + yield return request.SendWebRequest(); + + if (request.result != UnityWebRequest.Result.Success) + { + Debug.LogWarning($"[ApiClient] GET {path} failed: {request.error}"); + onError?.Invoke(request.error); + yield break; + } + var obj = JsonUtility.FromJson(request.downloadHandler.text); + onSuccess?.Invoke(obj); + } + + private IEnumerator PostCoroutine(string path, TReq body, Action onSuccess, Action onError) + { + var url = $"{baseUrl}{path}"; + var json = JsonUtility.ToJson(body); + using var request = new UnityWebRequest(url, "POST"); + request.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(json)); + request.downloadHandler = new DownloadHandlerBuffer(); + request.SetRequestHeader("Content-Type", "application/json"); + request.timeout = (int)timeoutSeconds; + yield return request.SendWebRequest(); + + if (request.result != UnityWebRequest.Result.Success) + { + Debug.LogWarning($"[ApiClient] POST {path} failed: {request.error}"); + onError?.Invoke(request.error); + yield break; + } + var obj = JsonUtility.FromJson(request.downloadHandler.text); + onSuccess?.Invoke(obj); + } + + // ── Health Check ───────────────────────────────────────── + + public Coroutine HealthCheck(Action callback) + { + return StartCoroutine(HealthCheckCoroutine(callback)); + } + + private IEnumerator HealthCheckCoroutine(Action callback) + { + using var request = UnityWebRequest.Get($"{baseUrl}/docs"); + request.timeout = 5; + yield return request.SendWebRequest(); + callback?.Invoke(request.result == UnityWebRequest.Result.Success); + } + + // ── Collection ─────────────────────────────────────────── + // GET /api/collection + // GET /api/collection?search=... + + // ── Decks ──────────────────────────────────────────────── + // GET /api/decks + // POST /api/decks + // GET /api/decks/{id} + // PATCH /api/decks/{id} + // DELETE /api/decks/{id} + + // ── Simulator ──────────────────────────────────────────── + // POST /api/lab/start + // GET /api/lab/status/{id} + // GET /api/lab/results/{id} + + // ── Coach ──────────────────────────────────────────────── + // POST /api/coach/chat + + // ── Deck Generator ─────────────────────────────────────── + // POST /api/deckgen/v3 + + // ── Scanner ────────────────────────────────────────────── + // POST /api/scanner/scan + + // ── Meta ───────────────────────────────────────────────── + // GET /api/lab/meta/commanders + // GET /api/lab/meta/search?q=... + // GET /api/lab/precons + } +}