using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using UnityEngine; using UnityEngine.UIElements; namespace Uigenx.Binding { /// /// Path keyed value bag. Keys are full paths (`player.hp`, `inventory.items[0].count`), so /// every get and set is O(1) with no tree walk. This is what the studio preview, the live data /// stream and the tests drive. /// public sealed class UgxDictionaryDataSource : IUgxDataSource, INotifyBindablePropertyChanged, IDataSourceViewHashProvider { readonly Dictionary _values; long _version; public UgxDictionaryDataSource(int capacity = 16) => _values = new Dictionary(capacity, StringComparer.Ordinal); public event EventHandler propertyChanged; /// Raised with the path that changed, before . public event Action PathChanged; public long GetViewHashCode() => _version; public int Count => _values.Count; public IEnumerable Paths => _values.Keys; public bool TryGet(string path, out object value) { if (string.IsNullOrEmpty(path)) { value = null; return false; } return _values.TryGetValue(path, out value); } public bool TrySet(string path, object value) { if (string.IsNullOrEmpty(path)) return false; Set(path, value); return true; } public void Set(string path, object value) { if (string.IsNullOrEmpty(path)) throw new ArgumentException("path is required", nameof(path)); if (_values.TryGetValue(path, out var existing) && Equals(existing, value)) return; _values[path] = value; Raise(path); } public void Remove(string path) { if (!_values.Remove(path)) return; Raise(path); } public void Clear() { if (_values.Count == 0) return; _values.Clear(); Raise(string.Empty); } public T Get(string path, T fallback = default) { if (!_values.TryGetValue(path, out var raw) || raw == null) return fallback; if (raw is T typed) return typed; try { return (T)Convert.ChangeType(raw, typeof(T), CultureInfo.InvariantCulture); } catch (Exception) { return fallback; } } /// /// Flattens a nested tree (the shape the daemon sends in `preview.request.data`) into path /// keys and merges it in. Cost is linear in the size of the tree. /// public void Merge(IDictionary nested, string prefix = null) { if (nested == null) return; foreach (var pair in nested) Flatten(Join(prefix, pair.Key), pair.Value); } /// Applies a flat path to value map, one change notification per changed path. public void Apply(IReadOnlyDictionary flat) { if (flat == null) return; foreach (var pair in flat) Set(pair.Key, pair.Value); } void Flatten(string path, object value) { switch (value) { case IDictionary map: foreach (var pair in map) Flatten(Join(path, pair.Key), pair.Value); return; case IDictionary raw: foreach (DictionaryEntry entry in raw) Flatten(Join(path, Convert.ToString(entry.Key, CultureInfo.InvariantCulture)), entry.Value); return; case string _: Set(path, value); return; case IList list: Set(path + ".length", list.Count); for (int i = 0; i < list.Count; i++) Flatten(path + "[" + i.ToString(CultureInfo.InvariantCulture) + "]", list[i]); return; default: Set(path, value); return; } } static string Join(string prefix, string key) => string.IsNullOrEmpty(prefix) ? key : prefix + "." + key; void Raise(string path) { _version++; // Listeners are foreign code: one throwing must not stop the others. try { PathChanged?.Invoke(path); } catch (Exception e) { Debug.LogError("[ugx] a data source listener threw for '" + path + "': " + e); } try { propertyChanged?.Invoke(this, new BindablePropertyChangedEventArgs(path)); } catch (Exception e) { Debug.LogError("[ugx] a binding listener threw for '" + path + "': " + e); } } } }