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