namespace Sandbox.Network; internal class ConnectionInfoManager { public readonly Dictionary All = new(); public StringTable StringTable { get; } = new( "ConnectionInfo", true ); public NetworkSystem NetworkSystem { get; } public ConnectionInfoManager( NetworkSystem networkSystem ) { NetworkSystem = networkSystem; StringTable.OnChangeOrAdd = OnTableEntryUpdated; StringTable.OnRemoved = OnTableEntryRemoved; StringTable.OnSnapshot = OnTableSnapshot; StringTable.PostNetworkUpdate = NetworkSystem.OnConnectionInfoUpdated; } public ConnectionInfo Add( Connection connection ) { var c = new ConnectionInfo( this ) { ConnectionId = connection.Id, State = connection.State }; All[connection.Id] = c; return c; } void OnTableEntryUpdated( StringTable.Entry entry ) { if ( !TryParseEntry( entry, out var id, out var key, out var isUserInfo ) ) return; if ( id == Guid.Empty || string.IsNullOrEmpty( key ) ) return; if ( !All.TryGetValue( id, out var c ) ) { c = All[id] = new( this ) { ConnectionId = id }; } c.FromStringTable( key, entry.Data, isUserInfo ); } void OnTableEntryRemoved( StringTable.Entry entry ) { if ( !TryParseEntry( entry, out var id, out _, out _ ) ) return; if ( id == Guid.Empty ) return; All.Remove( id ); } bool TryParseEntry( StringTable.Entry entry, out Guid id, out string key, out bool isUserInfo ) { id = Guid.Empty; key = string.Empty; isUserInfo = false; string[] split; // This is a UserInfo ConVar... if ( entry.Name.Contains( "#" ) ) { isUserInfo = true; split = entry.Name.Split( '#' ); id = Guid.Parse( split[0] ); key = string.Join( '#', split[1..] ); return true; } if ( !entry.Name.Contains( ":" ) ) return false; // This is regular data... split = entry.Name.Split( ':' ); id = Guid.Parse( split[0] ); key = string.Join( ':', split[1..] ); return true; } void OnTableSnapshot() { All.Clear(); foreach ( var e in StringTable.Entries.Values ) { OnTableEntryUpdated( e ); } NetworkSystem.OnConnectionInfoUpdated(); } /// /// Get info for this connection /// internal ConnectionInfo Get( Guid id ) { return All.GetValueOrDefault( id ); } internal void Remove( Guid id ) { if ( All.Remove( id, out var c ) ) { c.Dispose(); } var entries = StringTable.Entries.Keys .Where( c => c.StartsWith( $"{id}:" ) || c.StartsWith( $"{id}#" ) ) .ToArray(); foreach ( var key in entries ) { StringTable.Remove( key ); } } } /// /// Information about a connection. The difference between this and the actual connections is that /// this can be networked between clients, so all clients have the same information about each other. /// This is going to be required in p2p games, where players need to take over hosting from each other. /// In a game like Rust, with a dedicated server, this won't need to be networked to other clients. /// internal sealed class ConnectionInfo { private ConnectionInfoManager manager; private NetworkSystem NetworkSystem => manager.NetworkSystem; public int Ping { get; private set; } public Guid ConnectionId { get; internal set; } public string DisplayName { get; internal set; } public SteamId SteamId { get; internal set; } public DateTimeOffset ConnectionTime { get; internal set; } public bool CanRefreshObjects { get; internal set; } = true; public bool CanSpawnObjects { get; internal set; } = true; public SteamId PartyId { get; internal set; } internal Connection.ChannelState State { get; set; } internal ConnectionInfo( ConnectionInfoManager connectionInfoManager ) { manager = connectionInfoManager; ConnectionTime = DateTime.UtcNow; CanSpawnObjects = Sandbox.ProjectSettings.Networking.ClientsCanSpawnObjects; CanRefreshObjects = Sandbox.ProjectSettings.Networking.ClientsCanRefreshObjects; } /// /// Key values that come straight from the user. Can't be trusted, they could send anything. Used /// for things like preferences, avatar clothing etc. /// public Dictionary UserData { get; } = new(); internal void Update( UserInfo userInfo ) { DisplayName = userInfo.DisplayName; SteamId = userInfo.SteamId; PartyId = userInfo.PartyId; UserData.Clear(); foreach ( var p in userInfo.UserData ) { UserData.Add( p.Key, p.Value ); } UpdateStringTable(); } internal void UpdatePing( int ping ) { // This is the case for local mock info if ( manager is null ) return; // Only the host updates the string table if ( !NetworkSystem.IsHost ) return; UpdateStringTable( "ping", ping ); Ping = ping; } internal void UpdateStringTable() { // This is the case for local mock info if ( manager is null ) return; // Only the host updates the string table if ( !NetworkSystem.IsHost ) return; UpdateStringTable( "ping", Ping ); UpdateStringTable( "name", DisplayName ); UpdateStringTable( "state", State ); UpdateStringTable( "steamid", SteamId ); UpdateStringTable( "connect", ConnectionTime ); UpdateStringTable( "canSpawnObjects", CanSpawnObjects ); UpdateStringTable( "canRefreshObjects", CanRefreshObjects ); UpdateStringTable( "party", PartyRoom.Current?.Id ?? new( 0 ) ); foreach ( var (k, v) in UserData ) { UpdateStringTable( k, v, true ); } } void UpdateStringTable( string key, T value, bool isUserInfo = false ) { var stringTableKey = isUserInfo ? $"{ConnectionId}#{key}" : $"{ConnectionId}:{key}"; var data = NetworkSystem.TypeLibrary.ToBytes( value ); if ( manager.StringTable.Entries.TryGetValue( stringTableKey, out var entry ) ) { // The value is the same, no need to update it. if ( data.SequenceEqual( entry.Data ) ) return; } manager.StringTable.Set( stringTableKey, data ); } internal void FromStringTable( string key, byte[] data, bool isUserInfo ) { var value = NetworkSystem.TypeLibrary.FromBytes( data ); if ( isUserInfo ) { UserData[key] = (string)value; return; } switch ( key ) { case "name": DisplayName = (string)value; break; case "ping": Ping = (int)value; break; case "steamid": SteamId = (SteamId)value; break; case "connect": ConnectionTime = (DateTimeOffset)value; break; case "canSpawnObjects": CanSpawnObjects = (bool)value; break; case "canRefreshObjects": CanRefreshObjects = (bool)value; break; case "party": PartyId = (SteamId)value; break; case "state": State = (Connection.ChannelState)value; break; } } internal void Dispose() { manager = null; } internal void UpdateUserData( Dictionary userData ) { UserData.Clear(); // tony todo: Can this be one loop? foreach ( var (k, v) in userData ) { UserData.Add( k, v ); } foreach ( var (k, v) in userData ) { UpdateStringTable( k, v, true ); } } internal void SetUserData( string key, string value ) { UpdateStringTable( key, value, true ); } internal string GetUserData( string key ) { return UserData.GetValueOrDefault( key ); } /// /// When a user isn't connected to a server, or hosting a server, the /// client info table doesn't exist. So we provide info here for the /// local connection. /// static ConnectionInfo _localMock; internal static ConnectionInfo GetLocalMock() { _localMock ??= new( null ); _localMock.Update( UserInfo.Local ); return _localMock; } }