using System; using System.Collections.Generic; namespace codelanguage { /// /// Client-side replica of a server object. /// public sealed class ClientReplicatedObject { public int ObjectId { get; set; } public string ObjectType { get; set; } = string.Empty; public float X { get; set; } public float Y { get; set; } public float Z { get; set; } public string Icon { get; set; } = string.Empty; public string IconState { get; set; } = string.Empty; public int Direction { get; set; } public Dictionary Variables { get; } = new(StringComparer.Ordinal); public int? MovementDestX { get; set; } public int? MovementDestY { get; set; } public int? MovementDuration { get; set; } public DateTime LastUpdated { get; set; } = DateTime.UtcNow; public override string ToString() => $"ClientReplicatedObject(Id={ObjectId}, Type={ObjectType}, Pos=({X},{Y},{Z}), Dir={Direction})"; } /// /// Applies replicated server state to the client's local object world. /// public class ClientReplicationManager { private readonly Dictionary _objects = new(); private readonly object _objectLock = new(); public ClientRpcInvoker Rpc { get; } = new ClientRpcInvoker(); public int Count { get { lock (_objectLock) { return _objects.Count; } } } public IReadOnlyDictionary Objects { get { lock (_objectLock) { return new Dictionary(_objects); } } } public event Action? ObjectSpawned; public event Action? ObjectUpdated; public event Action? ObjectDespawned; public bool TryGetObject(int objectId, out ClientReplicatedObject? replicatedObject) { lock (_objectLock) { return _objects.TryGetValue(objectId, out replicatedObject); } } public ClientReplicatedObject? GetObject(int objectId) { TryGetObject(objectId, out ClientReplicatedObject? replicatedObject); return replicatedObject; } public bool ApplyMessage(NetworkMessage message) { if (message == null) throw new ArgumentNullException(nameof(message)); switch (message.Type) { case MessageType.ObjectSpawned: return ApplySpawned(message); case MessageType.ObjectUpdated: return ApplyUpdated(message); case MessageType.ObjectDespawned: return ApplyDespawned(message); case MessageType.ObjectMove: return ApplyMoved(message); case MessageType.VariableChanged: return ApplyVariableChanged(message); case MessageType.ProcReturn: case MessageType.ProcError: return ApplyRpcResponse(message); default: return false; } } // Compatibility aliases for callers/transport pipelines. public bool HandleMessage(NetworkMessage message) => ApplyMessage(message); public bool ProcessMessage(NetworkMessage message) => ApplyMessage(message); public bool ReceiveMessage(NetworkMessage message) => ApplyMessage(message); private bool ApplySpawned(NetworkMessage message) { int objectId = GetInt32(message, "objectId"); if (objectId <= 0) return false; var replicatedObject = new ClientReplicatedObject { ObjectId = objectId, ObjectType = GetString(message, "type"), X = GetFloat(message, "x"), Y = GetFloat(message, "y"), Z = GetFloat(message, "z"), Icon = GetString(message, "icon"), IconState = GetString(message, "iconState"), Direction = GetInt32(message, "direction"), MovementDestX = GetNullableInt(message, "movementDestX"), MovementDestY = GetNullableInt(message, "movementDestY"), MovementDuration = GetNullableInt(message, "movementDuration") }; if (message.Data.TryGetValue("variables", out var rawVariables) && rawVariables is IDictionary variableDictionary) { foreach (var variableEntry in variableDictionary) { replicatedObject.Variables[variableEntry.Key] = variableEntry.Value; } } lock (_objectLock) { _objects[objectId] = replicatedObject; } ObjectSpawned?.Invoke(replicatedObject); return true; } private bool ApplyUpdated(NetworkMessage message) { int objectId = GetInt32(message, "objectId"); if (objectId <= 0) return false; ClientReplicatedObject? replicatedObject; lock (_objectLock) { if (!_objects.TryGetValue(objectId, out replicatedObject)) { replicatedObject = new ClientReplicatedObject { ObjectId = objectId }; _objects[objectId] = replicatedObject; } } IDictionary? changedProperties = null; if (message.Data.TryGetValue("changedProperties", out var rawProperties) && rawProperties is IDictionary propertyDictionary) { changedProperties = propertyDictionary; } if (changedProperties != null) { ApplyChangedProperties(replicatedObject, changedProperties); replicatedObject.LastUpdated = DateTime.UtcNow; ObjectUpdated?.Invoke(replicatedObject); return true; } if (message.Data.TryGetValue("x", out var replacementX) || message.Data.TryGetValue("y", out _) || message.Data.TryGetValue("z", out _)) { ApplyChangedProperties(replicatedObject, message.Data); replicatedObject.LastUpdated = DateTime.UtcNow; ObjectUpdated?.Invoke(replicatedObject); return true; } replicatedObject.LastUpdated = DateTime.UtcNow; return false; } private static void ApplyChangedProperties( ClientReplicatedObject replicatedObject, IDictionary changedProperties) { if (changedProperties.TryGetValue("x", out var xValue)) replicatedObject.X = AsFloat(xValue); if (changedProperties.TryGetValue("y", out var yValue)) replicatedObject.Y = AsFloat(yValue); if (changedProperties.TryGetValue("z", out var zValue)) replicatedObject.Z = AsFloat(zValue); if (changedProperties.TryGetValue("direction", out var directionValue)) replicatedObject.Direction = AsInt(directionValue); if (changedProperties.TryGetValue("icon", out var iconValue)) replicatedObject.Icon = iconValue?.ToString() ?? string.Empty; if (changedProperties.TryGetValue("iconState", out var iconStateValue)) replicatedObject.IconState = iconStateValue?.ToString() ?? string.Empty; if (changedProperties.TryGetValue("movementDestX", out var movementDestXValue)) replicatedObject.MovementDestX = AsNullableInt(movementDestXValue); if (changedProperties.TryGetValue("movementDestY", out var movementDestYValue)) replicatedObject.MovementDestY = AsNullableInt(movementDestYValue); if (changedProperties.TryGetValue("movementDuration", out var movementDurationValue)) replicatedObject.MovementDuration = AsNullableInt(movementDurationValue); if (changedProperties.TryGetValue("variables", out var variablesValue) && variablesValue is IDictionary changedVariables) { foreach (var variableEntry in changedVariables) { if (variableEntry.Value == null) replicatedObject.Variables.Remove(variableEntry.Key); else replicatedObject.Variables[variableEntry.Key] = variableEntry.Value; } } } private bool ApplyDespawned(NetworkMessage message) { int objectId = GetInt32(message, "objectId"); if (objectId <= 0) return false; lock (_objectLock) { if (!_objects.Remove(objectId)) return false; } ObjectDespawned?.Invoke(objectId); return true; } private bool ApplyMoved(NetworkMessage message) { int objectId = GetInt32(message, "objectId"); if (objectId <= 0) return false; ClientReplicatedObject? replicatedObject; lock (_objectLock) { if (!_objects.TryGetValue(objectId, out replicatedObject)) return false; } replicatedObject.X = GetFloat(message, "x"); replicatedObject.Y = GetFloat(message, "y"); replicatedObject.Z = GetFloat(message, "z"); replicatedObject.MovementDestX = GetNullableInt(message, "movementDestX", "destX"); replicatedObject.MovementDestY = GetNullableInt(message, "movementDestY", "destY"); replicatedObject.MovementDuration = GetNullableInt(message, "movementDuration", "duration"); replicatedObject.LastUpdated = DateTime.UtcNow; ObjectUpdated?.Invoke(replicatedObject); return true; } private bool ApplyVariableChanged(NetworkMessage message) { int objectId = GetInt32(message, "objectId"); string variableName = GetString(message, "variableName", "variable", "name"); if (objectId <= 0 || string.IsNullOrWhiteSpace(variableName)) return false; ClientReplicatedObject? replicatedObject; lock (_objectLock) { if (!_objects.TryGetValue(objectId, out replicatedObject)) return false; } if (message.Data.TryGetValue("value", out var value)) { if (value == null) replicatedObject.Variables.Remove(variableName); else replicatedObject.Variables[variableName] = value; } replicatedObject.LastUpdated = DateTime.UtcNow; ObjectUpdated?.Invoke(replicatedObject); return true; } private bool ApplyRpcResponse(NetworkMessage message) { Rpc.StoreResponse(message.Data.TryGetValue("callId", out var rawCallId) ? AsLong(rawCallId) : message.MessageId, Rpc.ParseResponseMessage(message)); return true; } private static int GetInt32(NetworkMessage message, string first, params string[] fallbacks) { foreach (var name in new[] { first }.Concat(fallbacks)) { if (message.Data.TryGetValue(name, out var rawValue)) return AsInt(rawValue); } return 0; } private static string GetString(NetworkMessage message, string first, params string[] fallbacks) { foreach (var name in new[] { first }.Concat(fallbacks)) { if (message.Data.TryGetValue(name, out var rawValue) && rawValue != null) return rawValue.ToString() ?? string.Empty; } return string.Empty; } private static float GetFloat(NetworkMessage message, string name) { if (!message.Data.TryGetValue(name, out var rawValue)) return 0f; if (rawValue is float floatValue) return floatValue; if (rawValue is double doubleValue) return (float)doubleValue; if (rawValue is int intValue) return intValue; if (rawValue is long longValue) return longValue; return float.TryParse(rawValue?.ToString(), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float parsedValue) ? parsedValue : 0f; } private static int? GetNullableInt(NetworkMessage message, params string[] names) { foreach (var name in names) { if (message.Data.TryGetValue(name, out var rawValue) && rawValue != null) return AsNullableInt(rawValue); } return null; } private static int? AsNullableInt(object? value) { if (value == null) return null; return AsInt(value); } private static int AsInt(object? value) { if (value == null) return 0; if (value is int intValue) return intValue; if (value is long longValue) return checked((int)longValue); if (value is double doubleValue) return checked((int)Math.Round(doubleValue)); if (value is float floatValue) return checked((int)Math.Round(floatValue)); return int.TryParse(value.ToString(), System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out int parsedValue) ? parsedValue : 0; } private static float AsFloat(object? value) { if (value == null) return 0f; if (value is float floatValue) return floatValue; if (value is double doubleValue) return (float)doubleValue; if (value is int intValue) return intValue; if (value is long longValue) return longValue; return float.TryParse(value?.ToString(), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float parsedValue) ? parsedValue : 0f; } private static long AsLong(object? value) { if (value == null) return 0; if (value is long longValue) return longValue; if (value is int intValue) return intValue; if (value is double doubleValue) return checked((long)Math.Round(doubleValue)); return long.TryParse(value?.ToString(), out long parsedValue) ? parsedValue : 0; } } /// /// Convenience aliases kept in sync with the existing client component names. /// public class ClientReplicatedWorld : ClientReplicationManager { } public class ClientNetworkWorld : ClientReplicationManager { } }