using System; using FishNet.Managing; using GameKit.Dependencies.Utilities; namespace FishNet.Serializing { /// /// Special reader/writer buffer struct that can be used in Fishnet RPCs or Broadcasts, as arguments or part of structs /// /// Use cases: /// - replacement for stream sort of /// - instead of always allocating some arrays T[] and sending that over RPCs/Broadcast, you can use SubStream /// - you can pass SubStream into objects via reference 'ref', and those objects write/read state, useful for dynamic length reconcile (items, inventory, buffs, etc...) /// - sending data inside OnServerSpawn to clients via TargetRPC /// - instead of writting custom serializers for big struct, you can use SubStream inside RPCs/Broadcasts /// /// Pros: /// - reading is zero copy, reads directly from FishNet buffers /// - everything is pooled /// - ease of use /// - SubStream can also be left uninitialized (default) /// - Can work safely with multiple receivers in Broadcasts, as long as you read data in the same order /// Cons: /// - no reading over length protection, you have to know how much data you are reading, due to buffer being red can be larger than substreams buffer /// - writing buffers are also pooled, but there is a copy (since you write into it, then what is written is copied into fishnet internal buffer, but it's byte copy (fast) /// - have to use Dispose() to return buffers to pool, or it may result in memory leak /// - reading in multiple receiver methods (for same client) in Broadcasts, you have extra deserialization processing per each method /// - might be unsafe to use this to send from clients (undefined data length), but so is sending T[] or List from clients /// - not to be used for IReplicateData/input structs, because underlying reading buffer may be changed where as IReplicateData structs are stored internally in replay buffer (substream buffer is not) /// /// Note: /// - If you write/read custom structs ONLY via SubStream, automatic serializer will not pick those up. Mark those custom structs with [FishNet.CodeGenerating.IncludeSerialization]. /// Codegen detects only custom structs that are used in RPC/Broadcast methods, not in SubStream. /// /// public struct SubStream : IResettable { /// /// Is Substream initialized (can be read from or written to) /// public bool Initialized { get; private set; } /// /// Returns Length of substream data /// public int Length { get { if (_writer != null) return _writer.Length; if (_reader != null) return _reader.Length; return UNINITIALIZED_LENGTH; } } /// /// Returns remaining bytes to read from substream /// public int Remaining => (_reader != null) ? _reader.Remaining : UNINITIALIZED_LENGTH; /// /// Returns NetworkManager that Substream was initialized with /// public NetworkManager NetworkManager { get { if (_writer != null) return _writer.NetworkManager; if (_reader != null) return _reader.NetworkManager; return null; } } private PooledReader _reader; private int _startPosition; private PooledWriter _writer; private bool _disposed; /// /// Length to use when SubStream is not initialized. /// public const int UNINITIALIZED_LENGTH = -1; /// /// Creates SubStream for writing, use this before sending into RPC or Broadcast /// /// Need to include network manager for handling of networked IDs /// Minimum expected length of data, that will be written /// Returns writer of SubStream public static SubStream StartWriting(NetworkManager manager, out PooledWriter writer, int minimumLength = 0) { if (minimumLength == 0) writer = WriterPool.Retrieve(manager); else writer = WriterPool.Retrieve(manager, minimumLength); SubStream stream = new() { _writer = writer, Initialized = true, }; return stream; } /// /// Starts reading from substream via Reader class. Do not forget do Dispose() after reading /// /// Reader to read data from /// Returns true, if SubStream is initialized else false public bool StartReading(out Reader reader) { if (Initialized) { // reset reader, in case we are reading in multiple broadcasts delegates/events _reader.Position = _startPosition; reader = _reader; return true; } reader = null; return false; } public static SubStream CreateFromReader(Reader originalReader, int subStreamLength) { if (subStreamLength < 0) { NetworkManagerExtensions.LogError("SubStream length cannot be less than 0"); return default; } byte[] originalReaderBuffer = originalReader.GetBuffer(); // inherits reading buffer directly from fishnet reader ArraySegment arraySegment = new(originalReaderBuffer, originalReader.Position, subStreamLength); PooledReader newReader = ReaderPool.Retrieve(arraySegment, originalReader.NetworkManager); // advance original reader by length of substream data originalReader.Skip(subStreamLength); return new() { _startPosition = newReader.Position, _reader = newReader, _writer = null, _disposed = false, Initialized = true, }; } /// /// Resets reader to start position, so you can read data again from start of substream. /// /// public void ResetReaderToStartPosition() { if (_reader != null) _reader.Position = _startPosition; else NetworkManager.LogError("SubStream was not initialized as reader!"); } /// /// Used internally to get writer of SubStream /// /// internal PooledWriter GetWriter() { if (!Initialized) NetworkManager.LogError("SubStream was not initialized, it has to be initialized properly either localy or remotely!"); else if (_writer == null) NetworkManager.LogError($"GetWriter() requires SubStream to be initialized as writer! You have to create SubStream with {nameof(StartWriting)}()!"); return _writer; } internal PooledReader GetReader() { if (!Initialized) NetworkManager.LogError("SubStream was not initialized, it has to be initialized properly either localy or remotely!"); if (_reader == null) NetworkManager.LogError($"GetReader() requires SubStream to be initialized as reader!"); return _reader; } /// /// Returns uninitialized SubStream. Can send safely over network, but cannot be read from (StartReading will return false). /// You can also use 'var stream = default;' instead. /// /// Empty SubStream internal static SubStream GetUninitialized() { return new() { Initialized = false, }; } /// /// Do not forget to call this after: /// - you stopped writing to Substream AND already sent it via RPCs/Broadcasts /// - you stoped reading from it inside RPCs/Broadcast receive event /// - if you use it in Reconcile method, you have dispose SubStream inside Dispose() of IReconcileData struct /// public void ResetState() { if (!_disposed) // dispose reader only once { _disposed = true; if (_reader != null) { _reader.Store(); _reader = null; } } if (_writer != null) { if (_writer.Length < WriterPool.LENGTH_BRACKET) // 1000 is LENGTH_BRACKET _writer.Store(); else _writer.StoreLength(); _writer = null; } } public void InitializeState() { } } }