Download Runtime/SceneWorld.cs from ErenAta00/Maverick-Unity: direct link, hf CLI and curl.
- Browser
- Download file 19.4 kB
-
https://huggingface.co/ErenAta00/Maverick-Unity/resolve/main/Runtime/SceneWorld.cs
- Command line
-
hf download hf://ErenAta00/Maverick-Unity/Runtime/SceneWorld.cs
-
curl -L -o SceneWorld.cs https://huggingface.co/ErenAta00/Maverick-Unity/resolve/main/Runtime/SceneWorld.cs
19.4 kB
| // Scene state + tool semantics on real GameObjects. The rules match the simulator the model was trained on, so the | |
| // model's calls behave here exactly as they did in training. | |
| using System; | |
| using System.Collections.Generic; | |
| using System.Globalization; | |
| using System.Linq; | |
| using UnityEngine; | |
| namespace SceneAgent | |
| { | |
| public class ToolError : Exception { public ToolError(string m) : base(m) { } } | |
| public class SceneWorld : MonoBehaviour | |
| { | |
| public string playerRoom; | |
| public string gaze; | |
| public string leftHand, rightHand; | |
| public List<string> rooms = new List<string>(); | |
| public Transform leftHandAnchor, rightHandAnchor; | |
| public readonly SortedSet<string> highlighted = new SortedSet<string>(StringComparer.Ordinal); | |
| public SortedSet<string> clarify = new SortedSet<string>(StringComparer.Ordinal); | |
| public readonly SortedDictionary<string, double> pressed = new SortedDictionary<string, double>(StringComparer.Ordinal); | |
| public string lastClarifyQuestion; | |
| /// <summary>Where a room sits in the Unity scene (for example, rooms side by side). The model only ever sees | |
| /// room-local positions; null = every room at the origin.</summary> | |
| [] public Func<string, Vector3> roomOrigin; | |
| [ | |
| ] | |
| public bool initFromTransforms; | |
| /// <summary>Raised after every tool call Unity accepted: (tool, arguments, result). Hook animations, sounds or the | |
| /// camera here, e.g. move the player rig when "teleport" changes playerRoom.</summary> | |
| public event Action<string, Dictionary<string, object>, object> ToolExecuted; | |
| // affordances per label (SceneCatalog.Generated.cs): the same catalogue the model was trained on, so Unity | |
| // accepts exactly the calls the model learned to make | |
| static Dictionary<string, double> Top => SceneCatalog.Top; | |
| static HashSet<string> Grasp => SceneCatalog.Grasp; | |
| static HashSet<string> Container => SceneCatalog.Container; | |
| static HashSet<string> Fixed => SceneCatalog.Fixed; | |
| static Dictionary<string, string[]> States => SceneCatalog.States; | |
| static readonly Dictionary<string, string[]> Required = new Dictionary<string, string[]> { | |
| { "grab", new[] { "object_id", "hand" } }, { "release", new[] { "hand" } }, { "place", new[] { "object_id", "target_id" } }, | |
| { "move_by", new[] { "object_id", "direction", "distance", "unit" } }, { "rotate", new[] { "object_id", "degrees" } }, | |
| { "set_state", new[] { "object_id", "state" } }, { "press", new[] { "object_id" } }, { "highlight", new[] { "object_ids" } }, | |
| { "teleport", new[] { "location_id" } }, { "find_objects", new[] { "label" } }, { "ask_clarification", new[] { "question", "candidate_ids" } } }; | |
| static readonly Dictionary<string, Vector3> Dir = new Dictionary<string, Vector3> { | |
| { "left", Vector3.left }, { "right", Vector3.right }, { "forward", Vector3.forward }, | |
| { "backward", Vector3.back }, { "up", Vector3.up }, { "down", Vector3.down } }; | |
| static readonly string[] HandSlots = { "left_hand", "right_hand", "both_hands" }; | |
| // what an item can do: the catalogue by label, unless the item overrides it (SceneItem.overrideAffordances) | |
| static bool CanGrasp(SceneItem o) => o.overrideAffordances ? o.grabbable : Grasp.Contains(o.label); | |
| static bool TryTop(SceneItem t, out double h) | |
| { | |
| if (t.overrideAffordances) { h = t.surfaceHeight; return t.isSurface; } | |
| return Top.TryGetValue(t.label, out h); | |
| } | |
| static bool IsContainer(SceneItem o) => o.overrideAffordances ? o.isContainer : Container.Contains(o.label); | |
| static bool IsFixed(SceneItem o) => o.overrideAffordances ? o.isFixed : Fixed.Contains(o.label); | |
| static bool IsButton(SceneItem o) => o.overrideAffordances ? o.isButton : SceneCatalog.Button.Contains(o.label); | |
| static bool CanBe(SceneItem o, string st) => o.overrideAffordances | |
| ? (o.states ?? new string[0]).Contains(st) : States.ContainsKey(o.label) && States[o.label].Contains(st); | |
| void Start() | |
| { | |
| if (!initFromTransforms) return; | |
| foreach (var o in Items) | |
| { | |
| if (string.IsNullOrEmpty(o.itemId) || !o.hasPos || HandSlots.Contains(o.on)) continue; | |
| var tr = o.transform; | |
| var p = tr.position - (roomOrigin != null ? roomOrigin(o.room) : Vector3.zero); | |
| o.pos = new[] { SceneMath.Round(p.x, 3), SceneMath.Round(p.y - tr.localScale.y / 2f, 3), SceneMath.Round(p.z, 3) }; | |
| var e = tr.rotation.eulerAngles; | |
| o.yaw = SceneMath.Round(e.y, 3); o.pitch = SceneMath.Round(e.x, 3); o.roll = SceneMath.Round(e.z, 3); | |
| } | |
| if (rooms.Count == 0) rooms = Items.Select(o => o.room).Where(r => !string.IsNullOrEmpty(r)).Distinct().ToList(); | |
| if (string.IsNullOrEmpty(playerRoom) && rooms.Count > 0) playerRoom = rooms[0]; | |
| } | |
| // Held items are parented to the hand anchors while they are shown in the hand; when the anchors live outside | |
| // this hierarchy (for example, on the camera rig) they must still count as scene items, or every tool call on a | |
| // held item fails with "unknown object" ("put it on the counter" after a grab). | |
| public IEnumerable<SceneItem> Items | |
| { | |
| get | |
| { | |
| IEnumerable<SceneItem> all = GetComponentsInChildren<SceneItem>(true); | |
| foreach (var anchor in new[] { leftHandAnchor, rightHandAnchor }) | |
| if (anchor != null && !anchor.IsChildOf(transform)) | |
| all = all.Concat(anchor.GetComponentsInChildren<SceneItem>(true)); | |
| return all.Distinct(); | |
| } | |
| } | |
| SceneItem Get(string id) | |
| { | |
| var o = Items.FirstOrDefault(x => x.itemId == id); | |
| if (o == null) throw new ToolError($"unknown object '{id}'"); | |
| if (o.room != playerRoom) throw new ToolError($"object '{id}' is not visible"); | |
| return o; | |
| } | |
| // ------------------------------------------------------------------ scene JSON for the prompt | |
| public string BuildSceneJson() | |
| { | |
| var vis = new List<object>(); | |
| // hierarchy order, not sorted: training scenes list objects in random order, and the model does not depend | |
| // on the order | |
| foreach (var o in Items.Where(x => x.room == playerRoom)) | |
| { | |
| var v = new Dictionary<string, object> { { "id", o.itemId }, { "label", o.label } }; | |
| if (!string.IsNullOrEmpty(o.color)) v["color"] = o.color; | |
| v["on"] = o.on; | |
| if (!string.IsNullOrEmpty(o.state)) v["state"] = o.state; | |
| if (o.hasPos) v["pos"] = o.pos.Select(c => (object)SceneMath.Round(c, 1)).ToList(); | |
| vis.Add(v); | |
| } | |
| var scene = new Dictionary<string, object> { | |
| { "player", new Dictionary<string, object> { { "room", playerRoom }, { "gaze", Nz(gaze) }, { "left_hand", Nz(leftHand) }, { "right_hand", Nz(rightHand) } } }, | |
| { "locations", rooms.Cast<object>().ToList() }, { "visible_objects", vis } }; | |
| return MiniJson.Serialize(scene); | |
| } | |
| static object Nz(string s) => string.IsNullOrEmpty(s) ? null : s; | |
| // ------------------------------------------------------------------ tools | |
| static string S(Dictionary<string, object> a, string k) => a.TryGetValue(k, out var v) && v != null ? Convert.ToString(v, CultureInfo.InvariantCulture) : null; | |
| static double D(Dictionary<string, object> a, string k, double def = 0) => a.TryGetValue(k, out var v) && v != null ? Convert.ToDouble(v, CultureInfo.InvariantCulture) : def; | |
| static List<string> L(Dictionary<string, object> a, string k) => a.TryGetValue(k, out var v) && v is List<object> l ? l.Select(x => Convert.ToString(x)).ToList() : new List<string>(); | |
| static string[] HandsFor(string hand) => | |
| hand == "left" ? new[] { "left" } : hand == "right" ? new[] { "right" } : hand == "both" ? new[] { "left", "right" } : throw new ToolError($"invalid hand '{hand}'"); | |
| string HandGet(string h) => h == "left" ? leftHand : rightHand; | |
| void HandSet(string h, string v) { if (h == "left") leftHand = v; else rightHand = v; } | |
| void FreeHand(SceneItem o) { if (leftHand == o.itemId) leftHand = null; if (rightHand == o.itemId) rightHand = null; } | |
| void MoveTree(SceneItem o, double[] d) | |
| { | |
| for (int i = 0; i < 3; i++) o.pos[i] = SceneMath.Round(o.pos[i] + d[i], 3); | |
| foreach (var c in Items) if (c.on == o.itemId && c.hasPos) MoveTree(c, d); | |
| } | |
| /// <summary>Executes one call; returns the tool result object (serialised back to the model). Throws ToolError | |
| /// when the call is not possible here; ToolExecuted is raised only for accepted calls.</summary> | |
| public object Execute(string name, Dictionary<string, object> a) | |
| { | |
| var result = ExecuteTool(name, a); | |
| ToolExecuted?.Invoke(name, a, result); | |
| return result; | |
| } | |
| object ExecuteTool(string name, Dictionary<string, object> a) | |
| { | |
| if (!Required.ContainsKey(name ?? "")) throw new ToolError($"unknown tool '{name}'"); | |
| var missing = Required[name].Where(k => !a.ContainsKey(k) || a[k] == null).ToList(); | |
| if (missing.Count > 0) throw new ToolError("missing arguments [" + string.Join(", ", missing.Select(m => $"'{m}'")) + "]"); | |
| var ok = new Dictionary<string, object> { { "ok", true } }; | |
| switch (name) | |
| { | |
| case "grab": | |
| { | |
| var o = Get(S(a, "object_id")); | |
| if (!CanGrasp(o)) throw new ToolError($"'{o.itemId}' cannot be picked up"); | |
| if (HandSlots.Contains(o.on)) throw new ToolError($"'{o.itemId}' is already held"); | |
| string hand = S(a, "hand"); | |
| var hs = HandsFor(hand); | |
| if (hs.Any(h => !string.IsNullOrEmpty(HandGet(h)))) throw new ToolError($"{hand} hand is not free"); | |
| foreach (var h in hs) HandSet(h, o.itemId); | |
| o.on = hand == "both" ? "both_hands" : hand + "_hand"; o.hasPos = false; | |
| Sync(o); return ok; | |
| } | |
| case "release": | |
| { | |
| string hand = S(a, "hand"); | |
| var held = new SortedSet<string>(HandsFor(hand).Select(HandGet).Where(x => !string.IsNullOrEmpty(x)), StringComparer.Ordinal); | |
| if (held.Count == 0) throw new ToolError($"nothing held in {hand} hand"); | |
| foreach (var id in held) { var o = Get(id); FreeHand(o); o.on = "floor"; o.hasPos = true; o.pos = new[] { 0.0, 0.0, 0.6 }; Sync(o); } | |
| return ok; | |
| } | |
| case "place": | |
| { | |
| var o = Get(S(a, "object_id")); var t = Get(S(a, "target_id")); | |
| string rel = S(a, "relation") ?? "on", orient = S(a, "orientation") ?? "default"; | |
| if (!CanGrasp(o)) throw new ToolError($"'{o.itemId}' cannot be carried"); | |
| if (o.itemId == t.itemId || HandSlots.Contains(t.on)) throw new ToolError("invalid target"); | |
| if (rel == "on" && !TryTop(t, out _)) throw new ToolError($"'{t.itemId}' is not a surface"); | |
| if (rel == "in" && !IsContainer(t)) throw new ToolError($"'{t.itemId}' is not a container"); | |
| if (rel == "in" && (t.state == "closed" || t.state == "locked")) throw new ToolError($"'{t.itemId}' is closed"); | |
| if (!new[] { "default", "upright", "horizontal", "vertical", "upside_down" }.Contains(orient)) throw new ToolError($"invalid orientation '{orient}'"); | |
| FreeHand(o); | |
| double top = rel == "on" && TryTop(t, out var th) ? th : 0.0; | |
| o.on = t.itemId; o.relation = rel; o.orientation = orient; o.hasPos = true; | |
| o.pos = new[] { t.pos[0], SceneMath.Round(t.pos[1] + top, 3), t.pos[2] }; | |
| Sync(o); return ok; | |
| } | |
| case "move_by": | |
| { | |
| var o = Get(S(a, "object_id")); | |
| if (HandSlots.Contains(o.on) || IsFixed(o)) throw new ToolError($"'{o.itemId}' cannot be moved"); | |
| string dir = S(a, "direction"), unit = S(a, "unit"); | |
| if (dir == null || !Dir.ContainsKey(dir) || (unit != "m" && unit != "cm")) throw new ToolError("invalid direction or unit"); | |
| double dist = D(a, "distance") * (unit == "cm" ? 0.01 : 1.0); | |
| var v = Dir[dir]; | |
| MoveTree(o, new[] { v.x * dist, v.y * dist, v.z * dist }); | |
| foreach (var x in Items) Sync(x); | |
| return ok; | |
| } | |
| case "rotate": | |
| { | |
| var o = Get(S(a, "object_id")); | |
| if (HandSlots.Contains(o.on) || IsFixed(o)) throw new ToolError($"'{o.itemId}' cannot be rotated"); | |
| string axis = S(a, "axis") ?? "yaw"; | |
| double Wrap(double v) => SceneMath.Round(((v % 360) + 360) % 360, 3); | |
| double deg = D(a, "degrees"); | |
| if (axis == "yaw") o.yaw = Wrap(o.yaw + deg); | |
| else if (axis == "pitch") o.pitch = Wrap(o.pitch + deg); | |
| else if (axis == "roll") o.roll = Wrap(o.roll + deg); | |
| else throw new ToolError($"invalid axis '{axis}'"); | |
| Sync(o); return ok; | |
| } | |
| case "set_state": | |
| { | |
| var o = Get(S(a, "object_id")); string st = S(a, "state"); | |
| if (!CanBe(o, st)) throw new ToolError($"'{o.itemId}' cannot be {st}"); | |
| o.state = st; Sync(o); return ok; | |
| } | |
| case "press": | |
| { | |
| var o = Get(S(a, "object_id")); | |
| if (!IsButton(o)) throw new ToolError($"'{o.itemId}' is not a button"); | |
| pressed[o.itemId] = D(a, "hold_seconds"); return ok; | |
| } | |
| case "highlight": | |
| { | |
| var ids = L(a, "object_ids"); | |
| if (ids.Count == 0) throw new ToolError("nothing to highlight"); | |
| foreach (var id in ids) Get(id); | |
| foreach (var id in ids) { highlighted.Add(id); Sync(Get(id)); } | |
| return ok; | |
| } | |
| case "teleport": | |
| { | |
| string loc = S(a, "location_id"); | |
| if (!rooms.Contains(loc)) throw new ToolError($"unknown location '{loc}'"); | |
| foreach (var o in Items) if (HandSlots.Contains(o.on)) o.room = loc; | |
| playerRoom = loc; return ok; | |
| } | |
| case "find_objects": | |
| { | |
| string label = S(a, "label"), color = S(a, "color"), room = S(a, "room"); | |
| // hierarchy order, like the training simulator's world order | |
| return Items.Where(o => o.label == label && (color == null || o.color == color) && (room == null || o.room == room)) | |
| .Select(o => | |
| { | |
| var d = new Dictionary<string, object> { { "id", o.itemId }, { "label", o.label } }; | |
| if (!string.IsNullOrEmpty(o.color)) d["color"] = o.color; | |
| d["room"] = o.room; d["on"] = o.on; | |
| return (object)d; | |
| }).ToList(); | |
| } | |
| case "ask_clarification": | |
| clarify = new SortedSet<string>(L(a, "candidate_ids"), StringComparer.Ordinal); | |
| lastClarifyQuestion = S(a, "question"); | |
| return ok; | |
| } | |
| throw new ToolError($"unknown tool '{name}'"); | |
| } | |
| // ------------------------------------------------------------------ visuals | |
| public void Sync(SceneItem o) | |
| { | |
| var tr = o.transform; | |
| if (HandSlots.Contains(o.on)) | |
| { | |
| var anchor = o.on == "left_hand" ? leftHandAnchor : rightHandAnchor; | |
| if (anchor != null) { tr.SetParent(anchor, false); tr.localPosition = Vector3.zero; } | |
| } | |
| else | |
| { | |
| tr.SetParent(transform, true); | |
| float half = tr.localScale.y / 2f; | |
| var origin = roomOrigin != null ? roomOrigin(o.room) : Vector3.zero; | |
| tr.position = origin + new Vector3((float)o.pos[0], (float)o.pos[1] + half, (float)o.pos[2]); | |
| } | |
| tr.rotation = Quaternion.Euler((float)o.pitch, (float)o.yaw, (float)o.roll); | |
| var r = o.GetComponent<Renderer>(); | |
| if (r != null && Application.isPlaying) | |
| r.material.SetColor("_EmissionColor", highlighted.Contains(o.itemId) ? Color.yellow : Color.black); | |
| var light = o.GetComponentInChildren<Light>(); | |
| if (light != null) light.enabled = o.state == "on"; | |
| } | |
| // ------------------------------------------------------------------ same text form as sim.signature_str | |
| static string Num(double v, int digits) | |
| { | |
| if (Math.Abs(v) < 0.5 * Math.Pow(10, -digits)) v = 0.0; | |
| return SceneMath.Fixed(v, digits); | |
| } | |
| public string Signature() | |
| { | |
| var parts = new List<string>(); | |
| foreach (var o in Items.OrderBy(x => x.itemId, StringComparer.Ordinal)) | |
| { | |
| string p = o.hasPos ? string.Join(",", o.pos.Select(v => Num(v, 2))) : "-"; | |
| parts.Add(string.Join("|", o.itemId, o.room, o.on ?? "", o.relation ?? "", string.IsNullOrEmpty(o.orientation) ? "default" : o.orientation, | |
| p, $"{Num(o.yaw, 1)},{Num(o.pitch, 1)},{Num(o.roll, 1)}", o.state ?? "")); | |
| } | |
| parts.Add($"P:{playerRoom},{leftHand ?? ""},{rightHand ?? ""}"); | |
| parts.Add("H:" + string.Join(",", highlighted)); | |
| parts.Add("C:" + string.Join(",", clarify)); | |
| parts.Add("X:" + string.Join(",", pressed.Select(kv => $"{kv.Key}={Num(kv.Value, 1)}"))); | |
| return string.Join(";", parts); | |
| } | |
| } | |
| } | |