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 { }
}