using System.Runtime.InteropServices; using System.Text.Json; namespace Sandbox.Network; internal class StringTable { /// /// The maximum size of a snapshot entry in megabytes. /// private const int MaxSnapshotEntrySize = 8; public readonly Dictionary Entries = new(); public string Name { get; } public bool Compressed { get; } public bool HasChanged => changed.Count > 0 || removed.Count > 0; /// /// Called when a table entry is changed or added from the network /// public Action OnChangeOrAdd; /// /// The table has been fully replaced. Anything or everything could have changed. /// public Action OnSnapshot; /// /// Called when a table entry is removed by the network /// public Action OnRemoved; /// /// Called after a network update of the table is complete. Not called after snapshots! /// public Action PostNetworkUpdate; readonly HashSet changed = new(); readonly HashSet removed = new(); public StringTable( string name, bool compressed ) { Name = name; Compressed = compressed; } internal Entry Remove( string name ) { if ( !Entries.Remove( name, out var e ) ) return default; Log.Trace( $"Removed Key {name}" ); removed.Add( name ); changed.Remove( name ); return e; } internal void Reset() { Log.Trace( "Table Reset" ); Entries.Clear(); removed.Clear(); changed.Clear(); } internal Entry Set( string name, T assemblyBytes ) where T : unmanaged { var size = Marshal.SizeOf(); var arr = new byte[size]; MemoryMarshal.Write( arr, assemblyBytes ); return Set( name, arr ); } internal Entry Set( string name, byte[] assemblyBytes ) { if ( Entries.TryGetValue( name, out var entry ) ) { entry.Data = assemblyBytes; Log.Trace( $"Updated {name} [{assemblyBytes.Length}]" ); } else { entry = new Entry { Name = name, Data = assemblyBytes }; Entries[name] = entry; Log.Trace( $"Added {name} [{assemblyBytes.Length}]" ); } changed.Add( name ); removed.Remove( name ); return entry; } public class Entry { public string Name { get; set; } public byte[] Data { get; set; } /// /// Read an unmanaged struct from the data /// internal T Read() where T : unmanaged { var bytes = ByteStream.CreateReader( Data ); return bytes.Read(); } internal string ReadAsString() { return System.Text.Encoding.UTF8.GetString( Data ); } internal T ReadJson() { var json = ReadAsString(); if ( string.IsNullOrWhiteSpace( json ) ) return default; return JsonSerializer.Deserialize( json ); } } internal void SendSnapshot( Connection source ) { var bs = ByteStream.Create( 1024 ); bs.Write( InternalMessageType.TableSnapshot ); bs.Write( Name ); BuildSnapshotMessage( ref bs ); source.SendRawMessage( bs ); bs.Dispose(); } internal void ReadSnapshot( ByteStream bs ) { while ( true ) { var key = bs.Read(); if ( key == null ) break; var data = bs.ReadArraySpan( 1024 * 1024 * MaxSnapshotEntrySize ); Set( key, data.ToArray() ); } OnSnapshot?.Invoke(); } internal void BuildSnapshotMessage( ref ByteStream bs ) { foreach ( var entry in Entries ) { bs.Write( entry.Value.Name ); bs.WriteArray( entry.Value.Data ); } bs.Write( (string)null ); } internal void BuildUpdateMessage( ref ByteStream bs ) { bs.Write( removed.Count ); bs.Write( changed.Count ); foreach ( var entry in removed ) { bs.Write( entry ); } foreach ( var entry in changed ) { var e = Entries[entry]; bs.Write( e.Name ); bs.Write( e.Data.Length ); bs.Write( e.Data ); } } internal void ReadUpdate( ByteStream bs ) { var removed = bs.Read(); var changed = bs.Read(); for ( var i = 0; i < removed; i++ ) { var e = Remove( bs.Read() ); if ( e is not null ) { OnRemoved?.Invoke( e ); } } for ( var i = 0; i < changed; i++ ) { var key = bs.Read(); var len = bs.Read(); if ( len == 0 ) { var e = Set( key, [] ); if ( e is not null ) { OnChangeOrAdd?.Invoke( e ); } } else { using var data = bs.ReadByteStream( len ); var e = Set( key, data.ToArray() ); if ( e is not null ) { OnChangeOrAdd?.Invoke( e ); } } } PostNetworkUpdate?.Invoke(); } internal void ClearChanges() { changed.Clear(); removed.Clear(); } internal Entry SetJson( string key, T value ) { var json = JsonSerializer.Serialize( value ); return SetString( key, json ); } internal Entry SetString( string key, string value ) { return Set( key, System.Text.Encoding.UTF8.GetBytes( value ) ); } }