diff --git a/Ragon.Client/Sources/Entity/RagonEntity.cs b/Ragon.Client/Sources/Entity/RagonEntity.cs index afb2e9b..930a15c 100644 --- a/Ragon.Client/Sources/Entity/RagonEntity.cs +++ b/Ragon.Client/Sources/Entity/RagonEntity.cs @@ -55,7 +55,6 @@ namespace Ragon.Client public ushort Id { get; private set; } public ushort Type { get; private set; } - public RagonAuthority Authority { get; private set; } public RagonPlayer Owner { get; private set; } public RagonEntityState State { get; private set; } diff --git a/Ragon.Client/Sources/Handler/PlayerUserDataHandler.cs b/Ragon.Client/Sources/Handler/PlayerUserDataHandler.cs new file mode 100644 index 0000000..2fdb8c4 --- /dev/null +++ b/Ragon.Client/Sources/Handler/PlayerUserDataHandler.cs @@ -0,0 +1,52 @@ +/* + * Copyright 2024 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Ragon.Protocol; + +namespace Ragon.Client +{ + + internal class PlayerUserDataHandler: IHandler + { + private RagonPlayerCache _playerCache; + private RagonListenerList _listenerList; + + public PlayerUserDataHandler( + RagonPlayerCache playerCache, + RagonListenerList listenerList + ) + { + _playerCache = playerCache; + _listenerList = listenerList; + } + public void Handle(RagonBuffer reader) + { + var playerPeerId = reader.ReadUShort(); + var player = _playerCache.GetPlayerByPeer(playerPeerId); + + if (player != null) + { + player.UserData.Read(reader); + + _listenerList.OnPlayerUserData(player); + + return; + } + + RagonLog.Warn("Received user data for unknown player."); + } + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Handler/RoomEventHandler.cs b/Ragon.Client/Sources/Handler/RoomEventHandler.cs index ff3b1a6..d3a7a6a 100644 --- a/Ragon.Client/Sources/Handler/RoomEventHandler.cs +++ b/Ragon.Client/Sources/Handler/RoomEventHandler.cs @@ -50,6 +50,6 @@ public class RoomEventHandler : IHandler if (player.IsLocal && executionMode == RagonReplicationMode.LocalAndServer) return; - _client.Room.Event(eventCode, player, buffer); + _client.Room.HandleEvent(eventCode, player, buffer); } } \ No newline at end of file diff --git a/Ragon.Client/Sources/Handler/RoomUserDataHandler.cs b/Ragon.Client/Sources/Handler/RoomUserDataHandler.cs new file mode 100644 index 0000000..8a8b7dc --- /dev/null +++ b/Ragon.Client/Sources/Handler/RoomUserDataHandler.cs @@ -0,0 +1,38 @@ +/* + * Copyright 2024 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Ragon.Protocol; + +namespace Ragon.Client +{ + internal class RoomUserDataHandler : IHandler + { + private readonly RagonClient _client; + private readonly RagonListenerList _listenerList; + + public RoomUserDataHandler(RagonClient client, RagonListenerList listenerList) + { + _client = client; + _listenerList = listenerList; + } + + public void Handle(RagonBuffer reader) + { + _client.Room?.HandleUserData(reader); + _listenerList.OnRoomUserData(); + } + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/IUserData.cs b/Ragon.Client/Sources/IUserData.cs new file mode 100644 index 0000000..184532e --- /dev/null +++ b/Ragon.Client/Sources/IUserData.cs @@ -0,0 +1,27 @@ +/* + * Copyright 2024 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Ragon.Protocol; + +namespace Ragon.Client; + +public interface IUserData +{ + public byte[] this[string key] { get; set; } + bool Dirty { get; } + void Read(RagonBuffer buffer); + void Write(RagonBuffer buffer); +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Listener/IRagonListener.cs b/Ragon.Client/Sources/Listener/IRagonListener.cs index 8b55a16..0a30589 100644 --- a/Ragon.Client/Sources/Listener/IRagonListener.cs +++ b/Ragon.Client/Sources/Listener/IRagonListener.cs @@ -25,7 +25,10 @@ namespace Ragon.Client IRagonSceneListener, IRagonOwnershipChangedListener, IRagonPlayerJoinListener, - IRagonPlayerLeftListener + IRagonPlayerLeftListener, + IRagonRoomListListener, + IRagonRoomUserDataListener, + IRagonPlayerUserDataListener { } diff --git a/Ragon.Client/Sources/Listener/IRagonPlayerUserDataListener.cs b/Ragon.Client/Sources/Listener/IRagonPlayerUserDataListener.cs new file mode 100644 index 0000000..a0d4f77 --- /dev/null +++ b/Ragon.Client/Sources/Listener/IRagonPlayerUserDataListener.cs @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Client; + +public interface IRagonPlayerUserDataListener +{ + void OnPlayerUserDataUpdated(RagonClient client, RagonPlayer player); +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Listener/IRagonRoomUserDataListener.cs b/Ragon.Client/Sources/Listener/IRagonRoomUserDataListener.cs new file mode 100644 index 0000000..04b62bd --- /dev/null +++ b/Ragon.Client/Sources/Listener/IRagonRoomUserDataListener.cs @@ -0,0 +1,6 @@ +namespace Ragon.Client; + +public interface IRagonRoomUserDataListener +{ + public void OnUserDataUpdated(RagonClient client); +} \ No newline at end of file diff --git a/Ragon.Client/Sources/RagonClient.cs b/Ragon.Client/Sources/RagonClient.cs index 8144981..a48962d 100644 --- a/Ragon.Client/Sources/RagonClient.cs +++ b/Ragon.Client/Sources/RagonClient.cs @@ -1,5 +1,5 @@ /* - * Copyright 2023 Eduard Kargin + * Copyright 2023-2024 Eduard Kargin * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,7 +47,7 @@ namespace Ragon.Client public RagonEntityCache Entity => _entityCache; public NetworkStatistics Statistics => _stats; public RagonRoom Room => _room; - + internal RagonBuffer Buffer => _writeBuffer; internal INetworkChannel Reliable => _connection.Reliable; internal INetworkChannel Unreliable => _connection.Unreliable; @@ -57,12 +57,12 @@ namespace Ragon.Client public RagonClient(INetworkConnection connection, int rate) { _listeners = new RagonListenerList(this); - + _connection = connection; _connection.OnData += OnData; _connection.OnConnected += OnConnected; _connection.OnDisconnected += OnDisconnected; - + _replicationRate = (1000.0f / rate) / 1000.0f; _replicationTime = 0; @@ -98,32 +98,38 @@ namespace Ragon.Client _writeBuffer = new RagonBuffer(); _readBuffer = new RagonBuffer(); - _session = new RagonSession(this, _writeBuffer); - _playerCache = new RagonPlayerCache(); + _session = new RagonSession(this, _writeBuffer); _entityCache = new RagonEntityCache(this, _playerCache, _sceneCollector); _handlers = new IHandler[byte.MaxValue]; _handlers[(byte)RagonOperation.AUTHORIZED_SUCCESS] = new AuthorizeSuccessHandler(this, _listeners); _handlers[(byte)RagonOperation.AUTHORIZED_FAILED] = new AuthorizeFailedHandler(_listeners); - _handlers[(byte)RagonOperation.JOIN_SUCCESS] = new JoinSuccessHandler(this, _listeners, _playerCache, _entityCache); + _handlers[(byte)RagonOperation.JOIN_SUCCESS] = + new JoinSuccessHandler(this, _listeners, _playerCache, _entityCache); _handlers[(byte)RagonOperation.JOIN_FAILED] = new JoinFailedHandler(_listeners); _handlers[(byte)RagonOperation.LEAVE_ROOM] = new LeaveRoomHandler(this, _listeners, _entityCache); - _handlers[(byte)RagonOperation.OWNERSHIP_ROOM_CHANGED] = new OwnershipRoomHandler(_listeners, _playerCache, _entityCache); - _handlers[(byte)RagonOperation.OWNERSHIP_ENTITY_CHANGED] = new EntityOwnershipHandler(_listeners, _playerCache, _entityCache); + _handlers[(byte)RagonOperation.OWNERSHIP_ROOM_CHANGED] = + new OwnershipRoomHandler(_listeners, _playerCache, _entityCache); + _handlers[(byte)RagonOperation.OWNERSHIP_ENTITY_CHANGED] = + new EntityOwnershipHandler(_listeners, _playerCache, _entityCache); _handlers[(byte)RagonOperation.PLAYER_JOINED] = new PlayerJoinHandler(_playerCache, _listeners); _handlers[(byte)RagonOperation.PLAYER_LEAVED] = new PlayerLeftHandler(_entityCache, _playerCache, _listeners); _handlers[(byte)RagonOperation.LOAD_SCENE] = new SceneLoadHandler(this, _listeners); - _handlers[(byte)RagonOperation.CREATE_ENTITY] = new EntityCreateHandler(this, _playerCache, _entityCache, _entityListener); + _handlers[(byte)RagonOperation.CREATE_ENTITY] = + new EntityCreateHandler(this, _playerCache, _entityCache, _entityListener); _handlers[(byte)RagonOperation.REMOVE_ENTITY] = new EntityRemoveHandler(_entityCache); _handlers[(byte)RagonOperation.REPLICATE_ENTITY_STATE] = new StateEntityHandler(_entityCache); _handlers[(byte)RagonOperation.REPLICATE_ENTITY_EVENT] = new EntityEventHandler(_playerCache, _entityCache); _handlers[(byte)RagonOperation.REPLICATE_ROOM_EVENT] = new RoomEventHandler(this, _playerCache); - _handlers[(byte)RagonOperation.SNAPSHOT] = new SnapshotHandler(this, _listeners, _entityCache, _playerCache, _entityListener); + _handlers[(byte)RagonOperation.SNAPSHOT] = + new SnapshotHandler(this, _listeners, _entityCache, _playerCache, _entityListener); _handlers[(byte)RagonOperation.TIMESTAMP_SYNCHRONIZATION] = new TimestampHandler(this); _handlers[(byte)RagonOperation.REPLICATE_RAW_DATA] = new RoomDataHandler(_playerCache, _listeners); _handlers[(byte)RagonOperation.ROOM_LIST_UPDATED] = new RoomListHandler(_session, _listeners); - + _handlers[(byte)RagonOperation.ROOM_DATA_UPDATED] = new RoomUserDataHandler(this, _listeners); + _handlers[(byte)RagonOperation.PLAYER_DATA_UPDATED] = new PlayerUserDataHandler(_playerCache, _listeners); + var protocolRaw = RagonVersion.Parse(protocol); _connection.Connect(address, port, protocolRaw); } @@ -148,6 +154,8 @@ namespace Ragon.Client _entityCache.WriteState(_writeBuffer); SendTimestamp(); + SendRoomUserData(); + SendPlayerUserData(); } _stats.Update(_connection.BytesSent, _connection.BytesReceived, _connection.Ping, dt); @@ -164,6 +172,7 @@ namespace Ragon.Client _status = RagonStatus.DISCONNECTED; _connection.Disconnect(); } + _connection.Dispose(); } @@ -180,6 +189,8 @@ namespace Ragon.Client public void AddListener(IRagonSceneRequestListener listener) => _listeners.Add(listener); public void AddListener(IRagonDataListener listener) => _listeners.Add(listener); public void AddListener(IRagonRoomListListener listener) => _listeners.Add(listener); + public void AddListener(IRagonPlayerUserDataListener listener) => _listeners.Add(listener); + public void AddListener(IRagonRoomUserDataListener listener) => _listeners.Add(listener); public void RemoveListener(IRagonListener listener) => _listeners.Remove(listener); public void RemoveListener(IRagonAuthorizationListener listener) => _listeners.Remove(listener); public void RemoveListener(IRagonConnectionListener listener) => _listeners.Remove(listener); @@ -193,6 +204,8 @@ namespace Ragon.Client public void RemoveListener(IRagonSceneRequestListener listener) => _listeners.Remove(listener); public void RemoveListener(IRagonDataListener listener) => _listeners.Remove(listener); public void RemoveListener(IRagonRoomListListener listener) => _listeners.Remove(listener); + public void RemoveListener(IRagonRoomUserDataListener listener) => _listeners.Remove(listener); + public void RemoveListener(IRagonPlayerUserDataListener listener) => _listeners.Remove(listener); #endregion @@ -232,6 +245,38 @@ namespace Ragon.Client _writeBuffer.Write(value.Int1, 32); } + private void SendRoomUserData() + { + if (_room == null) return; + + var props = _room.UserData; + if (!props.Dirty) return; + + _writeBuffer.Clear(); + _writeBuffer.WriteOperation(RagonOperation.ROOM_DATA_UPDATED); + + props.Write(_writeBuffer); + + var sendData = _writeBuffer.ToArray(); + _connection.Reliable.Send(sendData); + } + + private void SendPlayerUserData() + { + if (_playerCache.Local == null) return; + + var props = _playerCache.Local.UserData; + if (!props.Dirty) return; + + _writeBuffer.Clear(); + _writeBuffer.WriteOperation(RagonOperation.PLAYER_DATA_UPDATED); + + props.Write(_writeBuffer); + + var sendData = _writeBuffer.ToArray(); + _connection.Reliable.Send(sendData); + } + private void OnConnected() { RagonLog.Trace("Connected"); diff --git a/Ragon.Client/Sources/RagonListenerList.cs b/Ragon.Client/Sources/RagonListenerList.cs index c7c6053..83e87b1 100644 --- a/Ragon.Client/Sources/RagonListenerList.cs +++ b/Ragon.Client/Sources/RagonListenerList.cs @@ -1,5 +1,5 @@ /* - * Copyright 2023 Eduard Kargin + * Copyright 2023-2024 Eduard Kargin * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,8 @@ namespace Ragon.Client private readonly List _playerLeftListeners = new(); private readonly List _dataListeners = new(); private readonly List _roomListListeners = new(); + private readonly List _roomUserDataListeners = new(); + private readonly List _playerUserDataListeners = new(); private readonly List _delayedActions = new(); public RagonListenerList(RagonClient client) @@ -51,6 +53,8 @@ namespace Ragon.Client _ownershipChangedListeners.Add(listener); _playerJoinListeners.Add(listener); _playerLeftListeners.Add(listener); + _roomUserDataListeners.Add(listener); + _playerUserDataListeners.Add(listener); } public void Remove(IRagonListener listener) @@ -66,6 +70,8 @@ namespace Ragon.Client _ownershipChangedListeners.Remove(listener); _playerJoinListeners.Remove(listener); _playerLeftListeners.Remove(listener); + _roomUserDataListeners.Remove(listener); + _playerUserDataListeners.Remove(listener); }); } @@ -81,7 +87,7 @@ namespace Ragon.Client { _dataListeners.Add(dataListener); } - + public void Add(IRagonAuthorizationListener listener) { _authorizationListeners.Add(listener); @@ -131,17 +137,27 @@ namespace Ragon.Client { _playerLeftListeners.Add(listener); } - + public void Add(IRagonRoomListListener listener) { _roomListListeners.Add(listener); } + + public void Add(IRagonRoomUserDataListener listener) + { + _roomUserDataListeners.Add(listener); + } + public void Add(IRagonPlayerUserDataListener listener) + { + _playerUserDataListeners.Add(listener); + } + public void Remove(IRagonDataListener listener) { _delayedActions.Add(() => _dataListeners.Remove(listener)); } - + public void Remove(IRagonSceneRequestListener listener) { _delayedActions.Add(() => _sceneRequestListeners.Remove(listener)); @@ -191,12 +207,22 @@ namespace Ragon.Client { _delayedActions.Add(() => _playerLeftListeners.Remove(listener)); } - + public void Remove(IRagonRoomListListener listener) { _delayedActions.Add(() => _roomListListeners.Remove(listener)); } + public void Remove(IRagonRoomUserDataListener listener) + { + _delayedActions.Add(() => _roomUserDataListeners.Remove(listener)); + } + + public void Remove(IRagonPlayerUserDataListener listener) + { + _delayedActions.Add(() => _playerUserDataListeners.Remove(listener)); + } + public void OnAuthorizationSuccess(string playerId, string playerName, string payload) { foreach (var listener in _authorizationListeners) @@ -280,5 +306,17 @@ namespace Ragon.Client foreach (var listListener in _roomListListeners) listListener.OnRoomListUpdate(roomInfos); } + + public void OnRoomUserData() + { + foreach (var userDataListener in _roomUserDataListeners) + userDataListener.OnUserDataUpdated(_client); + } + + public void OnPlayerUserData(RagonPlayer player) + { + foreach(var playerUserDataListener in _playerUserDataListeners) + playerUserDataListener.OnPlayerUserDataUpdated(_client, player); + } } } \ No newline at end of file diff --git a/Ragon.Client/Sources/RagonPlayer.cs b/Ragon.Client/Sources/RagonPlayer.cs index c81ce99..3bfa375 100644 --- a/Ragon.Client/Sources/RagonPlayer.cs +++ b/Ragon.Client/Sources/RagonPlayer.cs @@ -1,5 +1,5 @@ /* - * Copyright 2023 Eduard Kargin + * Copyright 2023-2024 Eduard Kargin * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ namespace Ragon.Client public ushort PeerId { get; set; } public bool IsRoomOwner { get; set; } public bool IsLocal { get; set; } + public IUserData UserData { get; private set; } public RagonPlayer(ushort peerId, string playerId, string name, bool isRoomOwner, bool isLocal) { @@ -32,6 +33,7 @@ namespace Ragon.Client IsLocal = isLocal; Name = name; Id = playerId; - } + UserData = isLocal ? new RagonUserData() : new RagonUserDataReadOnly(); + } } } \ No newline at end of file diff --git a/Ragon.Client/Sources/RagonPlayerCache.cs b/Ragon.Client/Sources/RagonPlayerCache.cs index d1cdcbd..c3d3f8a 100644 --- a/Ragon.Client/Sources/RagonPlayerCache.cs +++ b/Ragon.Client/Sources/RagonPlayerCache.cs @@ -1,5 +1,5 @@ /* - * Copyright 2023 Eduard Kargin + * Copyright 2023-2024 Eduard Kargin * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Ragon.Client/Sources/RagonRoom.cs b/Ragon.Client/Sources/RagonRoom.cs index bf8d7c9..43ca668 100644 --- a/Ragon.Client/Sources/RagonRoom.cs +++ b/Ragon.Client/Sources/RagonRoom.cs @@ -1,5 +1,5 @@ /* - * Copyright 2023 Eduard Kargin + * Copyright 2023-2024 Eduard Kargin * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ using Ragon.Protocol; namespace Ragon.Client { - public class RagonRoom: IDisposable + public class RagonRoom : IDisposable { private class EventSubscription : IDisposable { @@ -46,14 +46,15 @@ namespace Ragon.Client _callback = null!; } } - + private delegate void OnEventDelegate(RagonPlayer player, RagonBuffer serializer); - private RagonClient _client; - private RagonScene _scene; - private RagonEntityCache _entityCache; - private RagonPlayerCache _playerCache; - private RoomParameters _parameters; + private readonly RagonClient _client; + private readonly RagonScene _scene; + private readonly RagonEntityCache _entityCache; + private readonly RagonPlayerCache _playerCache; + private readonly RoomParameters _parameters; + private readonly RagonUserData _userData; public string Id => _parameters.RoomId; public int MinPlayers => _parameters.Min; @@ -63,10 +64,15 @@ namespace Ragon.Client public IReadOnlyList Players => _playerCache.Players; public RagonPlayer Local => _playerCache.Local; public RagonPlayer Owner => _playerCache.Owner; + public RagonUserData UserData => _userData; private readonly Dictionary _events = new Dictionary(); - private readonly Dictionary>> _localListeners = new Dictionary>>(); - private readonly Dictionary>> _listeners = new Dictionary>>(); + + private readonly Dictionary>> _localListeners = + new Dictionary>>(); + + private readonly Dictionary>> _listeners = + new Dictionary>>(); public RagonRoom(RagonClient client, RagonEntityCache entityCache, @@ -79,6 +85,7 @@ namespace Ragon.Client _entityCache = entityCache; _playerCache = playerCache; _scene = scene; + _userData = new RagonUserData(); } internal void Cleanup() @@ -92,7 +99,7 @@ namespace Ragon.Client _scene.Update(sceneName); } - internal void Event(ushort eventCode, RagonPlayer caller, RagonBuffer buffer) + internal void HandleEvent(ushort eventCode, RagonPlayer caller, RagonBuffer buffer) { if (_events.TryGetValue(eventCode, out var evnt)) evnt?.Invoke(caller, buffer); @@ -100,18 +107,23 @@ namespace Ragon.Client RagonLog.Warn($"Handler event on entity {Id} with eventCode {eventCode} not defined"); } + internal void HandleUserData(RagonBuffer buffer) + { + _userData.Read(buffer); + } + public IDisposable OnEvent(Action callback) where TEvent : IRagonEvent, new() { var t = new TEvent(); var eventCode = _client.Event.GetEventCode(t); var action = (RagonPlayer player, IRagonEvent eventData) => callback.Invoke(player, (TEvent)eventData); - + if (!_listeners.TryGetValue(eventCode, out var callbacks)) { callbacks = new List>(); _listeners.Add(eventCode, callbacks); } - + if (!_localListeners.TryGetValue(eventCode, out var localCallbacks)) { localCallbacks = new List>(); @@ -138,8 +150,12 @@ namespace Ragon.Client public void LoadScene(string sceneName) => _scene.Load(sceneName); public void SceneLoaded() => _scene.SceneLoaded(); - public void ReplicateEvent(TEvent evnt, RagonTarget target, RagonReplicationMode mode) where TEvent : IRagonEvent, new() => _scene.ReplicateEvent(evnt, target, mode); - public void ReplicateEvent(TEvent evnt, RagonPlayer target, RagonReplicationMode mode) where TEvent : IRagonEvent, new() => _scene.ReplicateEvent(evnt, target, mode); + public void ReplicateEvent(TEvent evnt, RagonTarget target, RagonReplicationMode mode) + where TEvent : IRagonEvent, new() => _scene.ReplicateEvent(evnt, target, mode); + + public void ReplicateEvent(TEvent evnt, RagonPlayer target, RagonReplicationMode mode) + where TEvent : IRagonEvent, new() => _scene.ReplicateEvent(evnt, target, mode); + public void ReplicateData(byte[] data, bool reliable = false) => _scene.ReplicateData(data, reliable); public void CreateEntity(RagonEntity entity) => CreateEntity(entity, null); diff --git a/Ragon.Client/Sources/RagonUserData.cs b/Ragon.Client/Sources/RagonUserData.cs new file mode 100644 index 0000000..d51ae49 --- /dev/null +++ b/Ragon.Client/Sources/RagonUserData.cs @@ -0,0 +1,70 @@ +/* + * Copyright 2024 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Ragon.Protocol; + +namespace Ragon.Client +{ + public class RagonUserData: IUserData + { + public byte[] this[string key] + { + get => _properties[key]; + set + { + _properties[key] = value; + + _dirty = true; + } + } + public bool Dirty => _dirty; + + private bool _dirty = false; + private readonly Dictionary _properties = new(); + + public RagonUserData() + { + } + + public void Read(RagonBuffer buffer) + { + _properties.Clear(); + + var len = buffer.ReadUShort(); + for (int i = 0; i < len; i++) + { + var key = buffer.ReadString(); + var valueSize = buffer.ReadUShort(); + var value = buffer.ReadBytes(valueSize); + + _properties[key] = value; + } + } + + public void Write(RagonBuffer buffer) + { + buffer.WriteUShort((ushort)_properties.Count); + foreach (var property in _properties) + { + buffer.WriteString(property.Key); + buffer.WriteUShort((ushort) property.Value.Length); + buffer.WriteBytes(property.Value); + } + + _dirty = false; + } + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/RagonUserDataReadOnly.cs b/Ragon.Client/Sources/RagonUserDataReadOnly.cs new file mode 100644 index 0000000..110aa64 --- /dev/null +++ b/Ragon.Client/Sources/RagonUserDataReadOnly.cs @@ -0,0 +1,65 @@ +/* + * Copyright 2024 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Ragon.Protocol; + +namespace Ragon.Client; + +public class RagonUserDataReadOnly : IUserData +{ + public byte[] this[string key] + { + get => _properties[key]; + set { } + } + + public bool Dirty => _dirty; + + private bool _dirty = false; + private readonly Dictionary _properties = new(); + + public RagonUserDataReadOnly() + { + } + + public void Write(RagonBuffer buffer) + { + buffer.WriteUShort((ushort)_properties.Count); + foreach (var property in _properties) + { + buffer.WriteString(property.Key); + buffer.WriteUShort((ushort)property.Value.Length); + buffer.WriteBytes(property.Value); + } + + _dirty = false; + } + + public void Read(RagonBuffer buffer) + { + _properties.Clear(); + + var len = buffer.ReadUShort(); + for (int i = 0; i < len; i++) + { + var key = buffer.ReadString(); + var valueSize = buffer.ReadUShort(); + var value = buffer.ReadBytes(valueSize); + + _properties[key] = value; + } + } +} \ No newline at end of file diff --git a/Ragon.Protocol/Sources/RagonBuffer.cs b/Ragon.Protocol/Sources/RagonBuffer.cs index 6b3e431..40932f1 100644 --- a/Ragon.Protocol/Sources/RagonBuffer.cs +++ b/Ragon.Protocol/Sources/RagonBuffer.cs @@ -282,6 +282,7 @@ namespace Ragon.Protocol _write += numBits; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public uint Read(int numBits = 16) diff --git a/Ragon.Protocol/Sources/RagonOperation.cs b/Ragon.Protocol/Sources/RagonOperation.cs index b7d6787..f5a9960 100644 --- a/Ragon.Protocol/Sources/RagonOperation.cs +++ b/Ragon.Protocol/Sources/RagonOperation.cs @@ -45,5 +45,7 @@ namespace Ragon.Protocol TRANSFER_ENTITY_OWNERSHIP = 24, TIMESTAMP_SYNCHRONIZATION = 25, ROOM_LIST_UPDATED = 26, + PLAYER_DATA_UPDATED = 27, + ROOM_DATA_UPDATED = 28, } } \ No newline at end of file diff --git a/Ragon.Relay/relay.config.json b/Ragon.Relay/relay.config.json index 25fbb5a..55c4b5e 100644 --- a/Ragon.Relay/relay.config.json +++ b/Ragon.Relay/relay.config.json @@ -10,6 +10,7 @@ "limitPlayersPerRoom": 20, "limitRooms": 200, "limitBufferedEvents": 50, + "limitUserData": 1024, "webHooks": { "room-created": "http://127.0.0.1:3000/service/create-room", diff --git a/Ragon.Server/Sources/Data/RagonData.cs b/Ragon.Server/Sources/Data/RagonData.cs new file mode 100644 index 0000000..c81a350 --- /dev/null +++ b/Ragon.Server/Sources/Data/RagonData.cs @@ -0,0 +1,23 @@ +using Ragon.Protocol; + +namespace Ragon.Server.Data; + +public class RagonData +{ + private byte[] _data; + public bool IsDirty { get; set; } + public byte[] Data + { + get => _data; + set + { + _data = value; + IsDirty = true; + } + } + + public RagonData(byte[] data) + { + _data = data; + } +} \ No newline at end of file diff --git a/Ragon.Server/Sources/Handler/AuthorizationOperation.cs b/Ragon.Server/Sources/Handler/AuthorizationOperation.cs index 6ed25bb..4bd37f3 100644 --- a/Ragon.Server/Sources/Handler/AuthorizationOperation.cs +++ b/Ragon.Server/Sources/Handler/AuthorizationOperation.cs @@ -29,16 +29,19 @@ public sealed class AuthorizationOperation: BaseOperation private readonly RagonWebHookPlugin _webhook; private readonly RagonContextObserver _observer; private readonly RagonBuffer _writer; + private readonly RagonServerConfiguration _configuration; public AuthorizationOperation( RagonBuffer reader, RagonBuffer writer, RagonWebHookPlugin webhook, - RagonContextObserver observer): base(reader, writer) + RagonContextObserver observer, + RagonServerConfiguration configuration): base(reader, writer) { _webhook = webhook; _observer = observer; _writer = writer; + _configuration = configuration; } public override void Handle(RagonContext context, NetworkChannel channel) @@ -55,7 +58,7 @@ public sealed class AuthorizationOperation: BaseOperation return; } - var configuration = context.Configuration; + var configuration = _configuration; var key = Reader.ReadString(); var name = Reader.ReadString(); var payload = Reader.ReadString(); diff --git a/Ragon.Server/Sources/Handler/EntityCreateOperation.cs b/Ragon.Server/Sources/Handler/EntityCreateOperation.cs index 546f5d8..7d5bf36 100644 --- a/Ragon.Server/Sources/Handler/EntityCreateOperation.cs +++ b/Ragon.Server/Sources/Handler/EntityCreateOperation.cs @@ -44,7 +44,7 @@ public sealed class EntityCreateOperation : BaseOperation Authority = eventAuthority, AttachId = attachId, StaticId = 0, - BufferedEvents = context.Configuration.LimitBufferedEvents, + BufferedEvents = context.LimitBufferedEvents, }; var entity = new RagonEntity(entityParameters); diff --git a/Ragon.Server/Sources/Handler/PlayerUserDataOperation.cs b/Ragon.Server/Sources/Handler/PlayerUserDataOperation.cs new file mode 100644 index 0000000..cc05955 --- /dev/null +++ b/Ragon.Server/Sources/Handler/PlayerUserDataOperation.cs @@ -0,0 +1,40 @@ +using NLog; +using Ragon.Protocol; +using Ragon.Server.IO; +using Ragon.Server.Lobby; + +namespace Ragon.Server.Handler +{ + public class PlayerUserDataOperation : BaseOperation + { + private readonly ILogger _logger = LogManager.GetCurrentClassLogger(); + private readonly int _userDataLimit; + + public PlayerUserDataOperation( + RagonBuffer reader, + RagonBuffer writer, + int userDataLimit + ) : base(reader, writer) + { + _userDataLimit = userDataLimit; + } + + public override void Handle(RagonContext context, NetworkChannel channel) + { + if (context.ConnectionStatus == ConnectionStatus.Unauthorized) + { + _logger.Warn($"Player {context.Connection.Id} not authorized for this request"); + return; + } + + var playerUserData = Reader.ReadBytes(Reader.Capacity); + if (playerUserData.Length > _userDataLimit) + { + _logger.Warn($"Player {context.Connection.Id} exceeded user data limit"); + return; + } + + context.UserData.Data = playerUserData; + } + } +} \ No newline at end of file diff --git a/Ragon.Server/Sources/Handler/RoomUserDataOperation.cs b/Ragon.Server/Sources/Handler/RoomUserDataOperation.cs new file mode 100644 index 0000000..ccffd20 --- /dev/null +++ b/Ragon.Server/Sources/Handler/RoomUserDataOperation.cs @@ -0,0 +1,57 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using NLog; +using Ragon.Protocol; +using Ragon.Server.IO; +using Ragon.Server.Lobby; + +namespace Ragon.Server.Handler; + +public sealed class RoomUserDataOperation : BaseOperation +{ + private readonly ILogger _logger = LogManager.GetCurrentClassLogger(); + private readonly int _userDataLimit; + + public RoomUserDataOperation( + RagonBuffer reader, + RagonBuffer writer, + int userDataLimit + ) : base(reader, writer) + { + _userDataLimit = userDataLimit; + } + + public override void Handle(RagonContext context, NetworkChannel channel) + { + if (context.ConnectionStatus == ConnectionStatus.Unauthorized) + { + _logger.Warn($"Player {context.Connection.Id} not authorized for this request"); + return; + } + + var roomUserData = Reader.ReadBytes(Reader.Capacity); + if (roomUserData.Length > _userDataLimit) + { + _logger.Warn("Room user data is too big"); + return; + } + + var room = context.Room; + if (room != null) + room.UserData.Data = roomUserData; + } +} \ No newline at end of file diff --git a/Ragon.Server/Sources/Handler/SceneLoadedOperation.cs b/Ragon.Server/Sources/Handler/SceneLoadedOperation.cs index dc0edc9..c81fbd4 100644 --- a/Ragon.Server/Sources/Handler/SceneLoadedOperation.cs +++ b/Ragon.Server/Sources/Handler/SceneLoadedOperation.cs @@ -62,7 +62,7 @@ public sealed class SceneLoadedOperation : BaseOperation Authority = eventAuthority, AttachId = 0, StaticId = staticId, - BufferedEvents = context.Configuration.LimitBufferedEvents, + BufferedEvents = context.LimitBufferedEvents, }; var entity = new RagonEntity(entityParameters); diff --git a/Ragon.Server/Sources/Lobby/RagonLobbyPlayer.cs b/Ragon.Server/Sources/Lobby/RagonLobbyPlayer.cs index 69377eb..60aea4b 100644 --- a/Ragon.Server/Sources/Lobby/RagonLobbyPlayer.cs +++ b/Ragon.Server/Sources/Lobby/RagonLobbyPlayer.cs @@ -14,6 +14,7 @@ * limitations under the License. */ +using Ragon.Server.Data; using Ragon.Server.IO; namespace Ragon.Server.Lobby; @@ -36,7 +37,7 @@ public class RagonLobbyPlayer { Id = id; Name = name; - Payload = payload; Connection = connection; + Payload = payload; } } \ No newline at end of file diff --git a/Ragon.Server/Sources/Plugin/Web/RagonWebHookPlugin.cs b/Ragon.Server/Sources/Plugin/Web/RagonWebHookPlugin.cs index 37048a6..d6c88fc 100644 --- a/Ragon.Server/Sources/Plugin/Web/RagonWebHookPlugin.cs +++ b/Ragon.Server/Sources/Plugin/Web/RagonWebHookPlugin.cs @@ -16,6 +16,7 @@ using System.Net; using System.Net.Http.Json; +using System.Text; using Newtonsoft.Json; using Ragon.Protocol; using Ragon.Server.Handler; diff --git a/Ragon.Server/Sources/RagonContext.cs b/Ragon.Server/Sources/RagonContext.cs index 3cf9e22..26a60c3 100644 --- a/Ragon.Server/Sources/RagonContext.cs +++ b/Ragon.Server/Sources/RagonContext.cs @@ -14,6 +14,7 @@ * limitations under the License. */ +using Ragon.Server.Data; using Ragon.Server.IO; using Ragon.Server.Lobby; using Ragon.Server.Time; @@ -26,43 +27,42 @@ public class RagonContext public ConnectionStatus ConnectionStatus { get; set; } public INetworkConnection Connection { get; } public IExecutor Executor { get; private set; } - public RagonServerConfiguration Configuration { get; private set; } + public int LimitBufferedEvents { get; private set; } public IRagonLobby Lobby { get; private set; } public RagonLobbyPlayer? LobbyPlayer { get; private set; } - public RagonRoom? Room { get; private set; } public RagonRoomPlayer? RoomPlayer { get; private set; } - + public RagonData UserData { get; private set; } public RagonScheduler Scheduler { get; private set; } public RagonContext( - INetworkConnection connection, - RagonServerConfiguration configuration, - IExecutor executor, - IRagonLobby lobby, - RagonScheduler scheduler) + INetworkConnection connection, + IExecutor executor, + IRagonLobby lobby, + RagonScheduler scheduler, + int limitBufferedEvents) { ConnectionStatus = ConnectionStatus.Unauthorized; - Configuration = configuration; + LimitBufferedEvents = limitBufferedEvents; Connection = connection; Executor = executor; Lobby = lobby; Scheduler = scheduler; + UserData = new RagonData(Array.Empty()); } internal void SetPlayer(RagonLobbyPlayer player) { LobbyPlayer = player; } - + internal void SetRoom(RagonRoom room, RagonRoomPlayer player) { Room?.DetachPlayer(RoomPlayer); - + Room = room; RoomPlayer = player; - + Room.AttachPlayer(RoomPlayer); } - } \ No newline at end of file diff --git a/Ragon.Server/Sources/RagonServer.cs b/Ragon.Server/Sources/RagonServer.cs index db57d26..aaeda90 100644 --- a/Ragon.Server/Sources/RagonServer.cs +++ b/Ragon.Server/Sources/RagonServer.cs @@ -74,11 +74,13 @@ public class RagonServer : IRagonServer, INetworkListener var contextObserver = new RagonContextObserver(_contextsByPlayerId); _scheduler.Run(new RagonActionTimer(SendRoomList, 1.0f)); + _scheduler.Run(new RagonActionTimer(SendPlayerUserData, 0.2f)); + _scheduler.Run(new RagonActionTimer(SendRoomUserData, 0.2f)); _serverPlugin.OnAttached(this); _handlers = new BaseOperation[byte.MaxValue]; - _handlers[(byte)RagonOperation.AUTHORIZE] = new AuthorizationOperation(_reader, _writer, _webhooks, contextObserver); + _handlers[(byte)RagonOperation.AUTHORIZE] = new AuthorizationOperation(_reader, _writer, _webhooks, contextObserver, configuration); _handlers[(byte)RagonOperation.JOIN_OR_CREATE_ROOM] = new RoomJoinOrCreateOperation(_reader, _writer, plugin, _webhooks); _handlers[(byte)RagonOperation.CREATE_ROOM] = new RoomCreateOperation(_reader, _writer, plugin, _webhooks); _handlers[(byte)RagonOperation.JOIN_ROOM] = new RoomJoinOperation(_reader, _writer, _webhooks); @@ -94,6 +96,8 @@ public class RagonServer : IRagonServer, INetworkListener _handlers[(byte)RagonOperation.TIMESTAMP_SYNCHRONIZATION] = new TimestampSyncOperation(_reader, _writer); _handlers[(byte)RagonOperation.REPLICATE_ROOM_EVENT] = new RoomEventOperation(_reader, _writer); _handlers[(byte)RagonOperation.REPLICATE_RAW_DATA] = new RoomDataOperation(_reader, _writer); + _handlers[(byte)RagonOperation.ROOM_DATA_UPDATED] = new RoomUserDataOperation(_reader, _writer, _configuration.LimitUserData); + _handlers[(byte)RagonOperation.PLAYER_DATA_UPDATED] = new PlayerUserDataOperation(_reader, _writer, _configuration.LimitUserData); _logger.Trace($"Server Tick Rate: {_configuration.ServerTickRate}"); } @@ -150,7 +154,7 @@ public class RagonServer : IRagonServer, INetworkListener public void OnConnected(INetworkConnection connection) { - var context = new RagonContext(connection, _configuration, _executor, _lobby, _scheduler); + var context = new RagonContext(connection, _executor, _lobby, _scheduler, _configuration.LimitBufferedEvents); _logger.Trace($"Connected: {connection.Id}"); _contextsByConnection.Add(connection.Id, context); @@ -244,6 +248,43 @@ public class RagonServer : IRagonServer, INetworkListener } } + public void SendPlayerUserData() + { + foreach (var (_, value) in _contextsByPlayerId) + { + if (value.UserData.IsDirty) + { + _writer.Clear(); + _writer.WriteOperation(RagonOperation.PLAYER_DATA_UPDATED); + _writer.WriteUShort(value.Connection.Id); + _writer.WriteBytes(value.UserData.Data); + + var sendData = _writer.ToArray(); + _server.Broadcast(sendData, NetworkChannel.RELIABLE); + + value.UserData.IsDirty = false; + } + } + } + + public void SendRoomUserData() + { + foreach (var room in _lobby.Rooms) + { + if (room.UserData.IsDirty) + { + _writer.Clear(); + _writer.WriteOperation(RagonOperation.ROOM_DATA_UPDATED); + _writer.WriteBytes(room.UserData.Data); + + var sendData = _writer.ToArray(); + _server.Broadcast(sendData, NetworkChannel.RELIABLE); + + room.UserData.IsDirty = false; + } + } + } + public BaseOperation ResolveHandler(RagonOperation operation) { return _handlers[(byte)operation]; diff --git a/Ragon.Server/Sources/RagonServerConfiguration.cs b/Ragon.Server/Sources/RagonServerConfiguration.cs index 4cd3dda..3e1e3d2 100644 --- a/Ragon.Server/Sources/RagonServerConfiguration.cs +++ b/Ragon.Server/Sources/RagonServerConfiguration.cs @@ -39,6 +39,7 @@ public struct RagonServerConfiguration public int LimitPlayersPerRoom; public int LimitRooms; public int LimitBufferedEvents; + public int LimitUserData; public Dictionary WebHooks; private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); diff --git a/Ragon.Server/Sources/Room/IRagonRoom.cs b/Ragon.Server/Sources/Room/IRagonRoom.cs index de2aacb..9f42df9 100644 --- a/Ragon.Server/Sources/Room/IRagonRoom.cs +++ b/Ragon.Server/Sources/Room/IRagonRoom.cs @@ -14,6 +14,7 @@ * limitations under the License. */ +using Ragon.Server.Data; using Ragon.Server.Entity; using Ragon.Server.IO; @@ -26,6 +27,7 @@ public interface IRagonRoom public int PlayerMin { get; } public int PlayerMax { get; } public int PlayerCount { get; } + public RagonData UserData { get; } RagonRoomPlayer GetPlayerByConnection(INetworkConnection connection); RagonRoomPlayer GetPlayerById(string id); diff --git a/Ragon.Server/Sources/Room/RagonRoom.cs b/Ragon.Server/Sources/Room/RagonRoom.cs index 8edfb2c..1ab0399 100644 --- a/Ragon.Server/Sources/Room/RagonRoom.cs +++ b/Ragon.Server/Sources/Room/RagonRoom.cs @@ -15,6 +15,7 @@ */ using Ragon.Protocol; +using Ragon.Server.Data; using Ragon.Server.Entity; using Ragon.Server.IO; using Ragon.Server.Plugin; @@ -30,6 +31,7 @@ public class RagonRoom : IRagonRoom, IRagonAction public int PlayerMin { get; private set; } public int PlayerCount => WaitPlayersList.Count; + public RagonData UserData { get; set; } public RagonRoomPlayer Owner { get; private set; } public RagonBuffer Writer { get; } public IRoomPlugin Plugin { get; private set; } @@ -66,6 +68,7 @@ public class RagonRoom : IRagonRoom, IRagonAction _entitiesDirtySet = new HashSet(); + UserData = new RagonData(Array.Empty()); Writer = new RagonBuffer(); } diff --git a/Ragon.Server/Sources/Room/RagonRoomPlayer.cs b/Ragon.Server/Sources/Room/RagonRoomPlayer.cs index b40637a..054d4c9 100644 --- a/Ragon.Server/Sources/Room/RagonRoomPlayer.cs +++ b/Ragon.Server/Sources/Room/RagonRoomPlayer.cs @@ -14,6 +14,7 @@ * limitations under the License. */ +using Ragon.Server.Data; using Ragon.Server.Entity; using Ragon.Server.IO;