using System;
using System.Collections.Generic;
using System.Linq;
namespace codelanguage
{
///
/// Represents a single object that can be replicated to clients
///
public class ReplicatedObject
{
public int ObjectId { get; set; }
public string ObjectType { get; set; } // e.g., "/mob/player", "/obj/door"
public float X { get; set; }
public float Y { get; set; }
public float Z { get; set; }
public string Icon { get; set; }
public string IconState { get; set; }
public int Direction { get; set; }
public Dictionary Variables { get; set; } = new();
// Ownership and visibility
public int? OwnerClientId { get; set; }
public List VisibleToClients { get; set; } = new();
// Replication state
public DateTime LastUpdated { get; set; }
public bool IsDirty { get; set; } = true;
public int? MovementDestX { get; set; }
public int? MovementDestY { get; set; }
public int? MovementDuration { get; set; }
public ReplicatedObject(int objectId, string objectType)
{
ObjectId = objectId;
ObjectType = objectType;
LastUpdated = DateTime.UtcNow;
}
///
/// Get distance to another object (Euclidean)
///
public float Distance(ReplicatedObject other)
{
float dx = X - other.X;
float dy = Y - other.Y;
return (float)Math.Sqrt(dx * dx + dy * dy);
}
///
/// Mark this object as modified
///
public void MarkDirty()
{
IsDirty = true;
LastUpdated = DateTime.UtcNow;
}
public override string ToString()
{
return $"ReplicatedObject(Id={ObjectId}, Type={ObjectType}, Pos=({X},{Y},{Z}), Owner={OwnerClientId})";
}
}
///
/// Manages visibility and interest for object replication
/// Determines which clients should see which objects
///
public class InterestManager
{
///
/// Default visibility range (same world)
///
public const float DefaultVisibilityRange = 15f;
private Dictionary _clientPositions = new();
private float _visibilityRange = DefaultVisibilityRange;
public InterestManager(float visibilityRange = DefaultVisibilityRange)
{
_visibilityRange = visibilityRange;
}
///
/// Update a client's position for interest calculations
///
public void UpdateClientPosition(int clientId, float x, float y)
{
_clientPositions[clientId] = (x, y);
}
///
/// Remove a client from tracking
///
public void RemoveClient(int clientId)
{
_clientPositions.Remove(clientId);
}
///
/// Check if client should see an object
///
public bool CanSeeObject(int clientId, ReplicatedObject obj, int? clientPlayerOwnerId)
{
// Always see your own objects
if (obj.OwnerClientId == clientId)
return true;
// If object is explicitly visible to this client
if (obj.VisibleToClients.Contains(clientId))
return true;
// Distance-based visibility (if client has position)
if (_clientPositions.TryGetValue(clientId, out var clientPos))
{
float distance = (float)Math.Sqrt(
(obj.X - clientPos.x) * (obj.X - clientPos.x) +
(obj.Y - clientPos.y) * (obj.Y - clientPos.y)
);
return distance <= _visibilityRange;
}
// Clients without a tracked position (just connected) can see everything
// within a generous default so the world is not invisible on first login.
return true;
}
///
/// Get all clients that should see an object
///
public List GetInterestedClients(ReplicatedObject obj, List allClientIds, int? clientPlayerOwnerId = null)
{
var interested = new List();
foreach (var clientId in allClientIds)
{
if (CanSeeObject(clientId, obj, clientPlayerOwnerId))
interested.Add(clientId);
}
return interested;
}
public void SetVisibilityRange(float range)
{
_visibilityRange = range;
}
public float GetVisibilityRange()
{
return _visibilityRange;
}
}
///
/// Server-side object replication manager
/// Tracks replicated objects and manages visibility
///
public class ObjectReplicator
{
private Dictionary _objects = new();
private int _nextObjectId = 1;
private object _objectLock = new object();
private InterestManager _interestManager;
private ConnectionManager _connectionManager;
public ObjectReplicator(ConnectionManager connectionManager, InterestManager interestManager = null)
{
_connectionManager = connectionManager;
_interestManager = interestManager ?? new InterestManager();
}
///
/// Create a new replicated object
///
public ReplicatedObject CreateObject(string objectType, float x = 0f, float y = 0f, float z = 0f, int? ownerClientId = null)
{
lock (_objectLock)
{
int objectId = _nextObjectId++;
var obj = new ReplicatedObject(objectId, objectType)
{
X = x,
Y = y,
Z = z,
OwnerClientId = ownerClientId
};
_objects[objectId] = obj;
return obj;
}
}
///
/// Get an object by ID
///
public ReplicatedObject? GetObject(int objectId)
{
lock (_objectLock)
{
_objects.TryGetValue(objectId, out var obj);
return obj;
}
}
///
/// Delete a replicated object
///
public bool DeleteObject(int objectId)
{
lock (_objectLock)
{
return _objects.Remove(objectId);
}
}
///
/// Get all replicated objects
///
public List GetAllObjects()
{
lock (_objectLock)
{
return new List(_objects.Values);
}
}
///
/// Get objects visible to a specific client
///
public List GetVisibleObjects(int clientId)
{
lock (_objectLock)
{
var allClientIds = _connectionManager.GetConnectedClients().Select(c => c.ClientId).ToList();
var visible = new List();
foreach (var obj in _objects.Values)
{
if (_interestManager.CanSeeObject(clientId, obj, null))
visible.Add(obj);
}
return visible;
}
}
///
/// Send spawn messages for all visible objects to a client
///
public void SendVisibleObjectsToClient(int clientId)
{
var visibleObjects = GetVisibleObjects(clientId);
foreach (var obj in visibleObjects)
{
var spawnMsg = new NetworkMessage(MessageType.ObjectSpawned, clientId: 0)
{
Data = new Dictionary
{
{ "objectId", obj.ObjectId },
{ "type", obj.ObjectType },
{ "x", obj.X },
{ "y", obj.Y },
{ "z", obj.Z },
{ "icon", obj.Icon ?? "default.cpi" },
{ "iconState", obj.IconState ?? "0" },
{ "direction", obj.Direction },
{ "variables", new Dictionary(obj.Variables) }
}
};
_connectionManager.SendToClient(clientId, spawnMsg);
}
}
///
/// Broadcast object update to interested clients
///
public void BroadcastObjectUpdate(int objectId)
{
var obj = GetObject(objectId);
if (obj == null)
return;
var allClients = _connectionManager.GetConnectedClients();
var interestedClients = _interestManager.GetInterestedClients(obj, allClients.Select(c => c.ClientId).ToList());
var updateMsg = new NetworkMessage(MessageType.ObjectUpdated, clientId: 0)
{
Data = new Dictionary
{
{ "objectId", obj.ObjectId },
{ "x", obj.X },
{ "y", obj.Y },
{ "z", obj.Z },
{ "direction", obj.Direction },
{ "variables", new Dictionary(obj.Variables) }
}
};
foreach (var clientId in interestedClients)
{
_connectionManager.SendToClient(clientId, updateMsg);
}
obj.IsDirty = false;
}
///
/// Broadcast object despawn to interested clients
///
public void BroadcastObjectDespawn(int objectId)
{
var allClients = _connectionManager.GetConnectedClients();
var despawnMsg = new NetworkMessage(MessageType.ObjectDespawned, clientId: 0)
{
Data = new Dictionary
{
{ "objectId", objectId }
}
};
foreach (var client in allClients)
{
_connectionManager.SendToClient(client.ClientId, despawnMsg);
}
}
///
/// Update object position and broadcast
///
public void MoveObject(int objectId, float newX, float newY, float newZ = 0f, int durationMs = 0)
{
var obj = GetObject(objectId);
if (obj == null)
return;
obj.X = newX;
obj.Y = newY;
obj.Z = newZ;
if (durationMs > 0)
{
obj.MovementDuration = durationMs;
}
BroadcastObjectUpdate(objectId);
}
///
/// Update client position for interest management
///
public void UpdateClientPosition(int clientId, float x, float y)
{
_interestManager.UpdateClientPosition(clientId, x, y);
}
///
/// Handle client disconnect - update interest manager
///
public void HandleClientDisconnect(int clientId)
{
_interestManager.RemoveClient(clientId);
lock (_objectLock)
{
_clientSyncBaselines.Remove(clientId);
}
}
///
/// Get total object count
///
public int GetObjectCount()
{
lock (_objectLock)
{
return _objects.Count;
}
}
///
/// Clear all objects
///
public void ClearAllObjects()
{
lock (_objectLock)
{
_objects.Clear();
_clientSyncBaselines.Clear();
_forcedSyncObjects.Clear();
}
}
private readonly Dictionary> _clientSyncBaselines = new();
private readonly HashSet _forcedSyncObjects = new();
///
/// Plan-compatible alias for coordinated object creation.
///
public ReplicatedObject SpawnObject(string objectType, int ownerId, int x, int y, int z = 0)
{
return CreateObject(objectType, x, y, z, ownerClientId: ownerId);
}
///
/// Plan-compatible alias for object deletion.
///
public bool DespawnObject(int objectId)
{
var objectWasRemoved = DeleteObject(objectId);
if (objectWasRemoved)
{
lock (_objectLock)
{
foreach (var baseline in _clientSyncBaselines.Values)
baseline.Remove(objectId);
}
var despawnMessage = new NetworkMessage(MessageType.ObjectDespawned, clientId: 0)
{
Data = new Dictionary { { "objectId", objectId } }
};
if (_connectionManager != null)
_connectionManager.BroadcastToClients(despawnMessage);
}
return objectWasRemoved;
}
///
/// Updates a replicated variable and marks the object for synchronization.
///
public bool UpdateObjectVariable(int objectId, string variableName, object? value)
{
var replicatedObject = GetObject(objectId);
if (replicatedObject == null)
return false;
replicatedObject.Variables[variableName] = value;
MarkDirty(objectId);
return true;
}
///
/// Explicitly marks an object as changing and requests a queued re-send.
///
public void MarkDirty(int objectId)
{
lock (_objectLock)
{
_forcedSyncObjects.Add(objectId);
var replicatedObject = _objects.TryGetValue(objectId, out var obj) ? obj : null;
if (replicatedObject != null)
{
replicatedObject.IsDirty = true;
replicatedObject.MarkDirty();
}
}
}
///
/// Clears a previous force-synchronization request.
///
public void ClearDirtyFlag(int objectId)
{
lock (_objectLock)
{
_forcedSyncObjects.Remove(objectId);
if (_objects.TryGetValue(objectId, out var replicatedObject))
replicatedObject.IsDirty = false;
}
}
///
/// Plan-compatible coordinate query (Euclidean radius around a world point).
///
public List GetVisibleObjects(float x, float y, float z, float radius)
{
lock (_objectLock)
{
var visibleObjects = new List();
foreach (var replicatedObject in _objects.Values)
{
float deltaX = replicatedObject.X - x;
float deltaY = replicatedObject.Y - y;
float deltaZ = replicatedObject.Z - z;
if ((deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ) <= (radius * radius))
visibleObjects.Add(replicatedObject);
}
return visibleObjects;
}
}
///
/// Plan-compatible by-object-id alias.
///
public ReplicatedObject? GetObjectById(int objectId) => GetObject(objectId);
///
/// Coordinates all potentiallychanged properties for one client into a delta
/// update list. Newly visible objects are spawned, modified objects are
/// diffed, and objects leaving visibility are despawned.
///
public List GetDeltaUpdates(int clientId)
{
var deltaMessages = new List();
lock (_objectLock)
{
var connectedClient = _connectionManager.GetConnection(clientId);
if (connectedClient == null)
return deltaMessages;
var allClientIds = _connectionManager.GetConnectedClients().Select(connection => connection.ClientId).ToList();
var currentlyVisible = new List();
foreach (var replicatedObject in _objects.Values)
{
if (_interestManager.CanSeeObject(clientId, replicatedObject, null))
currentlyVisible.Add(replicatedObject);
}
if (!_clientSyncBaselines.TryGetValue(clientId, out var baseline))
{
baseline = new Dictionary();
_clientSyncBaselines[clientId] = baseline;
}
var baselineObjectIds = new HashSet(baseline.Keys);
var visibleObjectIds = new HashSet();
foreach (var replicatedObject in currentlyVisible)
{
visibleObjectIds.Add(replicatedObject.ObjectId);
var snapshot = CreateSyncSnapshot(replicatedObject);
bool wasForced = _forcedSyncObjects.Remove(replicatedObject.ObjectId);
if (!baseline.TryGetValue(replicatedObject.ObjectId, out var previousSnapshot) ||
wasForced ||
IsDeltaSnapshotChanged(previousSnapshot, snapshot))
{
if (previousSnapshot == null)
{
deltaMessages.Add(CreateObjectSpawnMessage(replicatedObject));
}
else
{
var updatedProperties = CreateObjectDeltaProperties(previousSnapshot, snapshot, wasForced);
var updateMessage = new NetworkMessage(MessageType.ObjectUpdated, clientId: 0)
{
Data = new Dictionary
{
{ "objectId", replicatedObject.ObjectId },
{ "changedProperties", updatedProperties }
}
};
deltaMessages.Add(updateMessage);
}
}
baseline[replicatedObject.ObjectId] = snapshot;
}
foreach (var invisibleObjectId in baselineObjectIds.Except(visibleObjectIds))
{
deltaMessages.Add(new NetworkMessage(MessageType.ObjectDespawned, clientId: 0)
{
Data = new Dictionary { { "objectId", invisibleObjectId } }
});
baseline.Remove(invisibleObjectId);
}
}
// Broadcast messages to each consumer only if called explicitly; this method only
// creates a reusable list as required by the phase plan.
return deltaMessages;
}
private static ObjectSyncBaseline CreateSyncSnapshot(ReplicatedObject replicatedObject)
{
var variableSnapshot = new Dictionary(StringComparer.Ordinal);
foreach (var variableEntry in replicatedObject.Variables)
{
// Clone dictionary references so later mutation of mutable runtime values
// is treated as an actual change.
variableSnapshot[variableEntry.Key] = variableEntry.Value;
}
return new ObjectSyncBaseline
{
X = replicatedObject.X,
Y = replicatedObject.Y,
Z = replicatedObject.Z,
Direction = replicatedObject.Direction,
Icon = replicatedObject.Icon,
IconState = replicatedObject.IconState,
Variables = variableSnapshot,
HasMovement = replicatedObject.MovementDestX.HasValue || replicatedObject.MovementDestY.HasValue,
MovementDestX = replicatedObject.MovementDestX,
MovementDestY = replicatedObject.MovementDestY,
MovementDuration = replicatedObject.MovementDuration
};
}
private static bool IsDeltaSnapshotChanged(ObjectSyncBaseline previousSnapshot, ObjectSyncBaseline currentSnapshot)
{
return !previousSnapshot.X.Equals(currentSnapshot.X) ||
!previousSnapshot.Y.Equals(currentSnapshot.Y) ||
!previousSnapshot.Z.Equals(currentSnapshot.Z) ||
previousSnapshot.Direction != currentSnapshot.Direction ||
previousSnapshot.Icon != currentSnapshot.Icon ||
previousSnapshot.IconState != currentSnapshot.IconState ||
previousSnapshot.HasMovement != currentSnapshot.HasMovement ||
previousSnapshot.MovementDestX != currentSnapshot.MovementDestX ||
previousSnapshot.MovementDestY != currentSnapshot.MovementDestY ||
previousSnapshot.MovementDuration != currentSnapshot.MovementDuration ||
!AreVariableSnapshotsEqual(previousSnapshot.Variables, currentSnapshot.Variables);
}
private static Dictionary CreateObjectDeltaProperties(ObjectSyncBaseline previousSnapshot, ObjectSyncBaseline currentSnapshot, bool forceFullUpdate = false)
{
var changedProperties = new Dictionary(StringComparer.Ordinal);
if (forceFullUpdate || previousSnapshot.X != currentSnapshot.X)
changedProperties["x"] = currentSnapshot.X;
if (forceFullUpdate || previousSnapshot.Y != currentSnapshot.Y)
changedProperties["y"] = currentSnapshot.Y;
if (forceFullUpdate || previousSnapshot.Z != currentSnapshot.Z)
changedProperties["z"] = currentSnapshot.Z;
if (forceFullUpdate || previousSnapshot.Direction != currentSnapshot.Direction)
changedProperties["direction"] = currentSnapshot.Direction;
if (forceFullUpdate || previousSnapshot.Icon != currentSnapshot.Icon)
changedProperties["icon"] = currentSnapshot.Icon;
if (forceFullUpdate || previousSnapshot.IconState != currentSnapshot.IconState)
changedProperties["iconState"] = currentSnapshot.IconState;
if (forceFullUpdate || previousSnapshot.HasMovement != currentSnapshot.HasMovement ||
previousSnapshot.MovementDestX != currentSnapshot.MovementDestX)
changedProperties["movementDestX"] = currentSnapshot.MovementDestX;
if (forceFullUpdate || previousSnapshot.HasMovement != currentSnapshot.HasMovement ||
previousSnapshot.MovementDestY != currentSnapshot.MovementDestY)
changedProperties["movementDestY"] = currentSnapshot.MovementDestY;
if (forceFullUpdate || previousSnapshot.HasMovement != currentSnapshot.HasMovement ||
previousSnapshot.MovementDuration != currentSnapshot.MovementDuration)
changedProperties["movementDuration"] = currentSnapshot.MovementDuration;
var changedVariables = new Dictionary(StringComparer.Ordinal);
foreach (var variable in currentSnapshot.Variables)
{
if (!previousSnapshot.Variables.TryGetValue(variable.Key, out var previousValue) ||
previousValue != variable.Value)
{
changedVariables[variable.Key] = variable.Value;
}
}
foreach (var variable in previousSnapshot.Variables)
{
if (!currentSnapshot.Variables.ContainsKey(variable.Key))
changedVariables[variable.Key] = null;
}
if (changedVariables.Count > 0)
changedProperties["variables"] = changedVariables;
return changedProperties;
}
private static bool AreVariableSnapshotsEqual(Dictionary leftSnapshot, Dictionary rightSnapshot)
{
if (leftSnapshot.Count != rightSnapshot.Count)
return false;
foreach (var entry in leftSnapshot)
{
if (!rightSnapshot.TryGetValue(entry.Key, out var value) || value != entry.Value)
return false;
}
return true;
}
private static NetworkMessage CreateObjectSpawnMessage(ReplicatedObject replicatedObject)
{
return new NetworkMessage(MessageType.ObjectSpawned, clientId: 0)
{
Data = new Dictionary
{
{ "objectId", replicatedObject.ObjectId },
{ "type", replicatedObject.ObjectType },
{ "x", replicatedObject.X },
{ "y", replicatedObject.Y },
{ "z", replicatedObject.Z },
{ "icon", replicatedObject.Icon ?? "default.cpi" },
{ "iconState", replicatedObject.IconState ?? "0" },
{ "direction", replicatedObject.Direction },
{ "variables", new Dictionary(replicatedObject.Variables) },
{ "movementDestX", replicatedObject.MovementDestX ?? 0 },
{ "movementDestY", replicatedObject.MovementDestY ?? 0 },
{ "movementDuration", replicatedObject.MovementDuration ?? 0 }
}
};
}
private sealed class ObjectSyncBaseline
{
public float X { get; set; }
public float Y { get; set; }
public float Z { get; set; }
public int Direction { get; set; }
public string? Icon { get; set; }
public string? IconState { get; set; }
public bool HasMovement { get; set; }
public int? MovementDestX { get; set; }
public int? MovementDestY { get; set; }
public int? MovementDuration { get; set; }
public Dictionary Variables { get; set; } = new(StringComparer.Ordinal);
}
public InterestManager GetInterestManager()
{
return _interestManager;
}
}
}