Files
commander-ai-lab/unity-client/Assets/Scripts/Services/ApiClient.cs
T

283 lines
12 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;
namespace CommanderAILab.Services
{
/// <summary>
/// Singleton HTTP client for Commander AI Lab FastAPI backend.
/// All REST calls go through this service.
/// </summary>
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<T>(string path, Action<T> onSuccess, Action<string> onError = null)
{
return StartCoroutine(GetCoroutine(path, onSuccess, onError));
}
public Coroutine Post<TReq, TRes>(string path, TReq body, Action<TRes> onSuccess, Action<string> onError = null)
{
return StartCoroutine(PostCoroutine(path, body, onSuccess, onError));
}
public Coroutine PostRaw(string path, string json, Action<string> onSuccess, Action<string> onError = null)
{
return StartCoroutine(PostRawCoroutine(path, json, onSuccess, onError));
}
public Coroutine GetRaw(string path, Action<string> onSuccess, Action<string> onError = null)
{
return StartCoroutine(GetRawCoroutine(path, onSuccess, onError));
}
public Coroutine PatchRaw(string path, string json, Action<string> onSuccess, Action<string> onError = null)
{
return StartCoroutine(PatchRawCoroutine(path, json, onSuccess, onError));
}
public Coroutine Delete(string path, Action<string> onSuccess, Action<string> onError = null)
{
return StartCoroutine(DeleteCoroutine(path, onSuccess, onError));
}
// -- Coroutine implementations --------------------------------------
private IEnumerator GetCoroutine<T>(string path, Action<T> onSuccess, Action<string> 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<T>(request.downloadHandler.text);
onSuccess?.Invoke(obj);
}
private IEnumerator PostCoroutine<TReq, TRes>(string path, TReq body, Action<TRes> onSuccess, Action<string> 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<TRes>(request.downloadHandler.text);
onSuccess?.Invoke(obj);
}
private IEnumerator PostRawCoroutine(string path, string json, Action<string> onSuccess, Action<string> onError)
{
var url = $"{baseUrl}{path}";
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;
}
onSuccess?.Invoke(request.downloadHandler.text);
}
private IEnumerator PatchRawCoroutine(string path, string json, Action<string> onSuccess, Action<string> onError)
{
var url = $"{baseUrl}{path}";
using var request = new UnityWebRequest(url, "PATCH");
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] PATCH {path} failed: {request.error}");
onError?.Invoke(request.error);
yield break;
}
onSuccess?.Invoke(request.downloadHandler.text);
}
private IEnumerator DeleteCoroutine(string path, Action<string> onSuccess, Action<string> onError)
{
var url = $"{baseUrl}{path}";
using var request = UnityWebRequest.Delete(url);
request.downloadHandler = new DownloadHandlerBuffer();
request.timeout = (int)timeoutSeconds;
yield return request.SendWebRequest();
if (request.result != UnityWebRequest.Result.Success)
{
Debug.LogWarning($"[ApiClient] DELETE {path} failed: {request.error}");
onError?.Invoke(request.error);
yield break;
}
onSuccess?.Invoke(request.downloadHandler.text);
}
// -- Raw GET helper (returns JSON string) ---------------------------
private IEnumerator GetRawCoroutine(string path, Action<string> onSuccess, Action<string> 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;
}
onSuccess?.Invoke(request.downloadHandler.text);
}
// -- Health Check ---------------------------------------------------
public Coroutine HealthCheck(Action<bool> callback)
{
return StartCoroutine(HealthCheckCoroutine(callback));
}
private IEnumerator HealthCheckCoroutine(Action<bool> callback)
{
using var request = UnityWebRequest.Get($"{baseUrl}/api/health");
request.timeout = 5;
yield return request.SendWebRequest();
callback?.Invoke(request.result == UnityWebRequest.Result.Success);
}
// -- Collection -----------------------------------------------------
public Coroutine GetCollection(Action<string> onSuccess, Action<string> onError = null, string search = null)
{
var path = string.IsNullOrEmpty(search)
? "/api/collection"
: $"/api/collection?search={UnityWebRequest.EscapeURL(search)}";
return StartCoroutine(GetRawCoroutine(path, onSuccess, onError));
}
// -- Decks ----------------------------------------------------------
public Coroutine GetDecks(Action<string> onSuccess, Action<string> onError = null)
{
return StartCoroutine(GetRawCoroutine("/api/decks", onSuccess, onError));
}
public Coroutine CreateDeck(string deckJson, Action<string> onSuccess, Action<string> onError = null)
{
return PostRaw("/api/decks", deckJson, onSuccess, onError);
}
// -- Simulator ------------------------------------------------------
public Coroutine StartSim(string configJson, Action<string> onSuccess, Action<string> onError = null)
{
return PostRaw("/api/lab/start", configJson, onSuccess, onError);
}
public Coroutine GetSimStatus(string simId, Action<string> onSuccess, Action<string> onError = null)
{
return StartCoroutine(GetRawCoroutine($"/api/lab/status/{simId}", onSuccess, onError));
}
public Coroutine GetSimResults(string simId, Action<string> onSuccess, Action<string> onError = null)
{
return StartCoroutine(GetRawCoroutine($"/api/lab/results/{simId}", onSuccess, onError));
}
// -- Coach ----------------------------------------------------------
public Coroutine CoachChat(string messageJson, Action<string> onSuccess, Action<string> onError = null)
{
return PostRaw("/api/coach/chat", messageJson, onSuccess, onError);
}
// -- Deck Generator -------------------------------------------------
public Coroutine GenerateDeck(string requestJson, Action<string> onSuccess, Action<string> onError = null)
{
return PostRaw("/api/deckgen/v3", requestJson, onSuccess, onError);
}
// -- Meta -----------------------------------------------------------
public Coroutine GetCommanders(Action<string> onSuccess, Action<string> onError = null)
{
return StartCoroutine(GetRawCoroutine("/api/lab/meta/commanders", onSuccess, onError));
}
public Coroutine GetPrecons(Action<string> onSuccess, Action<string> onError = null)
{
return StartCoroutine(GetRawCoroutine("/api/lab/precons", onSuccess, onError));
}
// -- Scanner --------------------------------------------------------
public Coroutine ScanCard(byte[] imageBytes, Action<string> onSuccess, Action<string> onError = null)
{
return StartCoroutine(ScanCardCoroutine(imageBytes, onSuccess, onError));
}
private IEnumerator ScanCardCoroutine(byte[] imageBytes, Action<string> onSuccess, Action<string> onError)
{
var url = $"{baseUrl}/api/scanner/scan";
var form = new WWWForm();
form.AddBinaryData("file", imageBytes, "card.jpg", "image/jpeg");
using var request = UnityWebRequest.Post(url, form);
request.timeout = (int)timeoutSeconds;
yield return request.SendWebRequest();
if (request.result != UnityWebRequest.Result.Success)
{
Debug.LogWarning($"[ApiClient] POST /api/scanner/scan failed: {request.error}");
onError?.Invoke(request.error);
yield break;
}
onSuccess?.Invoke(request.downloadHandler.text);
}
}
}