ipin-vr/ondevice/Scripts/CommandBank.cs
Oskar Kapala 921a85d30b Add export subcommand and Unity PoC-1 scaffold (ONDEVICE §4)
Python (ipin_vr/export.py):
- generate_bank(): levels 1–2 enumerated exhaustively (120/720 combos),
  level 3 random with dedup; all deduplicated per-level
- run_export(): CLI --per-level / --out / --seed flags per ONDEVICE §4.1
- cli.py routes "export" subcommand before session args (backward-compat)
- 14 new tests: format, dedup for all 3 levels, space caps (L1=120, L2=720),
  seed determinism, file output

Unity scaffold (ondevice/Scripts/):
- CommandBank.cs: loads commands.json from StreamingAssets (WebRequest on Android)
- TtsManager.cs: sherpa-onnx integration with [SHERPA] stubs, StreamingAssets→
  persistentDataPath copy, defensive Speak() with onDone callback
- SessionController.cs: passthrough flow §6, level switching, busy guard on Next
- ondevice/README.md: full manual Editor steps §5 (Unity setup, Meta XR SDK,
  sherpa-onnx install, scene wiring, font, build, metrics)

pytest: 37/37 passed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 16:11:53 +02:00

119 lines
3.7 KiB
C#

// CommandBank.cs — ładuje commands.json z StreamingAssets, udostępnia losowe
// polecenia per-level. Samoistny MonoBehaviour; podepnij do dowolnego GameObject.
//
// Plik commands.json generuj offline:
// python -m ipin_vr export --per-level 300 --out commands.json
// Następnie umieść go w Assets/StreamingAssets/commands.json.
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
namespace IpinVr
{
[Serializable]
public class CommandEntry
{
public int level;
public string text;
}
[Serializable]
internal class BankMeta
{
public string version;
public int per_level;
}
[Serializable]
internal class BankData
{
public BankMeta meta;
public List<CommandEntry> commands;
}
public class CommandBank : MonoBehaviour
{
[SerializeField] private string jsonFileName = "commands.json";
private readonly Dictionary<int, List<string>> _byLevel = new();
private bool _loaded;
public bool IsLoaded => _loaded;
/// <summary>Wczytaj bank asynchronicznie. onDone wywoływane po załadowaniu.</summary>
public void Load(Action onDone = null)
{
StartCoroutine(LoadCoroutine(onDone));
}
private IEnumerator LoadCoroutine(Action onDone)
{
string srcPath = Path.Combine(Application.streamingAssetsPath, jsonFileName);
string json;
#if UNITY_ANDROID && !UNITY_EDITOR
// Na Androidzie StreamingAssets są wewnątrz APK — czytamy przez WebRequest.
using var req = UnityWebRequest.Get(srcPath);
yield return req.SendWebRequest();
if (req.result != UnityWebRequest.Result.Success)
{
Debug.LogError($"[CommandBank] Błąd wczytywania {jsonFileName}: {req.error}");
yield break;
}
json = req.downloadHandler.text;
#else
if (!File.Exists(srcPath))
{
Debug.LogError($"[CommandBank] Plik nie istnieje: {srcPath}");
yield break;
}
json = File.ReadAllText(srcPath);
yield return null;
#endif
var data = JsonUtility.FromJson<BankData>(json);
if (data?.commands == null)
{
Debug.LogError("[CommandBank] Nie można sparsować commands.json");
yield break;
}
_byLevel.Clear();
foreach (var entry in data.commands)
{
if (!_byLevel.TryGetValue(entry.level, out var list))
_byLevel[entry.level] = list = new List<string>();
list.Add(entry.text);
}
int total = data.commands.Count;
Debug.Log($"[CommandBank] Wczytano {total} poleceń (v{data.meta?.version})");
_loaded = true;
onDone?.Invoke();
}
/// <summary>Zwraca losowe polecenie dla danego poziomu, lub null jeśli brak.</summary>
public string GetRandom(int level)
{
if (!_loaded)
{
Debug.LogWarning("[CommandBank] Bank nie jest jeszcze wczytany");
return null;
}
if (!_byLevel.TryGetValue(level, out var list) || list.Count == 0)
{
Debug.LogWarning($"[CommandBank] Brak poleceń dla poziomu {level}");
return null;
}
return list[UnityEngine.Random.Range(0, list.Count)];
}
public int CountForLevel(int level) =>
_byLevel.TryGetValue(level, out var l) ? l.Count : 0;
}
}