using Language.Lua; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using System.Xml.Linq; using UnityEngine; using static UnityEngine.PlayerLoop.PreUpdate; namespace KCM.StateManagement.Observers { public class Observer : MonoBehaviour, IObserver { public object state { get; set; } public List monitoredFields { get; set; } public List monitoredProperties { get; set; } public long lastUpdate { get; set; } public int updateInterval { get; set; } public long currentMs => DateTimeOffset.Now.ToUnixTimeMilliseconds(); public Dictionary values { get; set; } public Dictionary changedValues { get; set; } public GameObject observerObject { get; set; } public EventHandler StateUpdated { get; set; } public EventHandler SendUpdate { get; set; } public long lastPacket { get; set; } public int packetInterval = 300; public class ListTypeVariables { public List fields; public List properties; public ListTypeVariables(Type type) { fields = new List(); properties = new List(); var bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static; foreach (var field in type.GetFields(bindingFlags).OrderBy(field => field.Name)) fields.Add(field); foreach (var prop in type.GetProperties(bindingFlags).OrderBy(prop => prop.Name)) properties.Add(prop); Main.helper.Log($"ListTypeVariables: {type.Name} has {fields.Count} fields and {properties.Count} properties"); } } public Dictionary listVariables = new Dictionary(); public void Initialise(T state, string[] monitoredFields, GameObject observerObject, int updateInterval = 100) { this.state = state; this.monitoredFields = new List(); this.monitoredProperties = new List(); this.lastUpdate = DateTimeOffset.Now.ToUnixTimeMilliseconds(); this.observerObject = observerObject; this.updateInterval = updateInterval; this.values = new Dictionary(); this.changedValues = new Dictionary(); var bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static; foreach (var field in monitoredFields) { if (state.GetType().GetField(field, bindingFlags) != null) this.monitoredFields.Add(state.GetType().GetField(field, bindingFlags)); else if (state.GetType().GetProperty(field, bindingFlags) != null) this.monitoredProperties.Add(state.GetType().GetProperty(field, bindingFlags)); } // This will store all the fields and properties of the list type on start for efficient access and later comparison foreach (var field in this.monitoredFields) { if (field.FieldType.IsGenericType && field.FieldType.GetGenericTypeDefinition() == typeof(List<>)) { var listType = field.FieldType.GetGenericArguments()[0]; listVariables.Add(field.Name, new ListTypeVariables(listType)); } if (field.FieldType.IsGenericType && field.FieldType.GetGenericTypeDefinition() == typeof(ArrayExt<>)) { var listType = field.FieldType.GetField("data").FieldType.GetElementType(); listVariables.Add(field.Name, new ListTypeVariables(listType)); } } foreach (var prop in this.monitoredProperties) { if (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(List<>)) { var listType = prop.PropertyType.GetGenericArguments()[0]; if (listType.IsPrimitive) continue; listVariables.Add(prop.Name, new ListTypeVariables(listType)); } if (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(ArrayExt<>)) { var listType = prop.PropertyType.GetField("data").FieldType.GetElementType(); listVariables.Add(prop.Name, new ListTypeVariables(listType)); } } Main.helper.Log($"Observer created for {state.GetType().Name} with {this.monitoredFields.Count} fields, {this.monitoredProperties.Count} properties, and {listVariables.Count} non-primitive list variables"); } public void Update() { if (this.state == null) return; // Unity uses "fake null" for destroyed objects. Since our state is stored as object, // we must explicitly detect that case to avoid exceptions + log spam. try { UnityEngine.Object unityObj = this.state as UnityEngine.Object; if (this.state is UnityEngine.Object && unityObj == null) { try { StateObserver.observers.Remove(this.state.GetHashCode()); } catch { } try { if (observerObject != null) UnityEngine.Object.Destroy(observerObject); else UnityEngine.Object.Destroy(this.gameObject); } catch { } return; } } catch { } if (!(currentMs - lastUpdate > updateInterval)) // Don't run if the update interval hasn't passed (default 100 milliseconds); return; foreach (var field in monitoredFields) UpdateValue(field.Name, field.GetValue(state)); foreach (var prop in monitoredProperties) UpdateValue(prop.Name, prop.GetValue(state)); if ((currentMs - lastPacket > packetInterval) && changedValues.Count > 0) { try { SendUpdate?.Invoke(this, null); lastPacket = currentMs; changedValues.Clear(); } catch (Exception e) { Main.helper.Log($"Error sending update: {e.Message}"); Main.helper.Log($"Stack trace: {e.StackTrace}"); } } } public void UpdateValue(string name, object value) { if (values.ContainsKey(name)) { if (!AreEqual(name, values[name], value)) { StateChanged(name, value); if (isArrayExt(values[name])) values[name].GetType().GetField("data").SetValue(values[name], value); else values[name] = value; this.lastUpdate = DateTimeOffset.Now.ToUnixTimeMilliseconds(); if (isArrayExt(values[name])) changedValues[name].GetType().GetField("data").SetValue(values[name], value); else changedValues[name] = value; } } else { if (!changedValues.ContainsKey(name)) changedValues.Add(name, value); values.Add(name, value); StateChanged(name, value); } } public class StateUpdateEventArgs : EventArgs { public string name; public object value; } public void StateChanged(string name, object value) { StateUpdated?.Invoke(this, new StateUpdateEventArgs { name = name, value = value }); //Main.helper.Log($"{name} state changed to {value}"); } bool IsListButNotOfPrimitives(object obj) { return obj.GetType().IsGenericType && obj.GetType().GetGenericTypeDefinition() == typeof(List<>) && !obj.GetType().GetGenericArguments()[0].IsPrimitive; } bool isArrayExt(object obj) { return obj.GetType().IsGenericType && obj.GetType().GetGenericTypeDefinition() == typeof(ArrayExt<>); } object[] ToObjectArray(object obj) { return ((IEnumerable)obj).Cast().ToArray(); } public bool AreEqual(string fieldName, object a, object b) { object current = b; object previous = a; if (isArrayExt(a) && isArrayExt(b)) { object dataA = a.GetType().GetField("data").GetValue(previous); object dataB = b.GetType().GetField("data").GetValue(current); return DeepArrayTypeEqualsCheck(fieldName, (Array)dataA, (Array)dataB); } if (IsListButNotOfPrimitives(a) && IsListButNotOfPrimitives(b)) { var aAsObjectArray = ToObjectArray(a); var bAsObjectArray = ToObjectArray(b); return DeepArrayTypeEqualsCheck(fieldName, aAsObjectArray, bAsObjectArray); } if ((current.GetType().IsArray && previous.GetType().IsArray) && !(current.GetType().IsPrimitive && previous.GetType().IsPrimitive)) return DeepArrayTypeEqualsCheck(fieldName, (Array)a, (Array)b); // Check if both are null or are the same instance if (ReferenceEquals(a, b)) return true; if (a == null || b == null) return false; if (current.GetType().IsArray && previous.GetType().IsArray) { // Check for single-dimensional arrays if (current is Array aArray && previous is Array bArray) { // Different lengths mean they are not equal if (aArray.Length != bArray.Length) return false; // Handle 2D arrays specifically if (aArray.Rank == 2 && bArray.Rank == 2) { return Are2DArraysEqual(aArray, bArray); } // Handle 1D arrays else if (aArray.Rank == 1 && bArray.Rank == 1) { return Enumerable.SequenceEqual(aArray.Cast(), bArray.Cast()); } } } // Fallback to default Equals for other types return Equals(current, previous); } private bool Are2DArraysEqual(Array a, Array b) { if (a.GetLength(0) != b.GetLength(0) || a.GetLength(1) != b.GetLength(1)) return false; for (int i = 0; i < a.GetLength(0); i++) { for (int j = 0; j < a.GetLength(1); j++) { if (!Equals(a.GetValue(i, j), b.GetValue(i, j))) return false; } } return true; } private bool DeepArrayTypeEqualsCheck(string varName, Array a, Array b) { try { // Check for reference equality and nulls if (ReferenceEquals(a, b)) return true; if (a == null || b == null) return false; // Compare counts if (a.Length != b.Length) return false; for (int i = 0; i < a.Length; i++) { foreach (var field in listVariables[varName].fields) { if (!Equals(field.GetValue(a.GetValue(i)), field.GetValue(b.GetValue(i)))) return false; } foreach (var prop in listVariables[varName].properties) { if (!Equals(prop.GetValue(a.GetValue(i)), prop.GetValue(b.GetValue(i)))) return false; } } } catch (Exception e) { Main.helper.Log($"Error comparing {varName} arrays: {e.Message}"); return true; } return true; } } }