#if UGX_BRIDGE || UNITY_EDITOR using System; using System.Collections.Generic; using Uigenx.Binding; using Uigenx.Protocol; using UnityEngine; namespace Uigenx.Bridge { /// /// Serves `data.subscribe`, `data.unsubscribe` and `data.set`. Each session samples only the /// paths it asked for, at its own rate, and only changed values leave the process. /// public sealed class UgxDataStreamHandler : IDisposable { /// Upper bound on live sessions. The studio opens one per inspector. public const int MaxSessions = 8; /// Upper bound on watched paths per session. public const int MaxPathsPerSession = 512; readonly UgxBridgeClient _client; readonly Func _sourceProvider; readonly Dictionary _sessions = new Dictionary(StringComparer.Ordinal); public UgxDataStreamHandler(UgxBridgeClient client, Func sourceProvider) { _client = client ?? throw new ArgumentNullException(nameof(client)); _sourceProvider = sourceProvider ?? throw new ArgumentNullException(nameof(sourceProvider)); client.Register(UgxMessageTypes.DataSubscribe, OnSubscribe); client.Register(UgxMessageTypes.DataUnsubscribe, OnUnsubscribe); client.Register(UgxMessageTypes.DataSet, OnSet); } public int SessionCount => _sessions.Count; public void Dispose() { _client.Unregister(UgxMessageTypes.DataSubscribe); _client.Unregister(UgxMessageTypes.DataUnsubscribe); _client.Unregister(UgxMessageTypes.DataSet); foreach (var session in _sessions.Values) _client.ClearDeltas(session.Id); _sessions.Clear(); } /// Main thread. Samples every due session and flushes the coalesced deltas. public void Tick(double now) { if (_sessions.Count == 0) return; var source = _sourceProvider(); if (source == null) return; bool queued = false; foreach (var session in _sessions.Values) { if (now < session.NextSampleTime) continue; session.NextSampleTime = now + session.Interval; queued |= session.Sample(source, _client); } if (queued) _client.FlushDeltas(); } void OnSubscribe(UgxEnvelope envelope) { var body = envelope.BodyAs(); if (string.IsNullOrEmpty(body.Session)) { _client.SendError(UgxErrorCodes.BadRequest, "data.subscribe needs a session", envelope.Id); return; } if (!_sessions.ContainsKey(body.Session) && _sessions.Count >= MaxSessions) { _client.SendError(UgxErrorCodes.BadRequest, "too many live data sessions, the limit is " + MaxSessions, envelope.Id); return; } if (body.Paths == null) body.Paths = new List(); if (body.Paths.Count > MaxPathsPerSession) { _client.SendError(UgxErrorCodes.BadRequest, "a session may watch at most " + MaxPathsPerSession + " paths", envelope.Id); return; } var session = new Session(body.Session, body.Paths, body.Hz); _sessions[body.Session] = session; var source = _sourceProvider(); var snapshot = new UgxDataSnapshot { Session = session.Id, Seq = 0 }; if (source != null) { foreach (var path in session.Paths) { if (!source.TryGet(path, out var value)) continue; snapshot.Values[path] = value; session.Remember(path, value); } } _client.Send(UgxMessageTypes.DataSnapshot, snapshot, re: envelope.Id); } void OnUnsubscribe(UgxEnvelope envelope) { var body = envelope.BodyAs(); if (string.IsNullOrEmpty(body.Session)) return; _sessions.Remove(body.Session); _client.ClearDeltas(body.Session); } void OnSet(UgxEnvelope envelope) { var body = envelope.BodyAs(); var source = _sourceProvider(); if (source == null) { _client.SendError(UgxErrorCodes.NotPlaying, "no data source is active", envelope.Id); return; } if (!source.TrySet(body.Path, body.Value)) _client.SendError(UgxErrorCodes.BadRequest, "cannot write '" + body.Path + "' on the active data source", envelope.Id); } sealed class Session { public readonly string Id; public readonly List Paths; public readonly double Interval; public double NextSampleTime; readonly Dictionary _last; public Session(string id, List paths, double hz) { Id = id; Paths = paths ?? new List(); Interval = hz > 0d ? 1d / hz : 1d / UgxSettings.DefaultDataStreamHz; _last = new Dictionary(Paths.Count, StringComparer.Ordinal); } public void Remember(string path, object value) => _last[path] = value; /// Returns true when at least one change was queued. public bool Sample(IUgxDataSource source, UgxBridgeClient client) { bool any = false; for (int i = 0; i < Paths.Count; i++) { var path = Paths[i]; if (!source.TryGet(path, out var value)) continue; if (_last.TryGetValue(path, out var previous) && Equals(previous, value)) continue; _last[path] = value; client.QueueDelta(Id, path, value); any = true; } return any; } } } } #endif