using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using FlaxEngine; using FlaxEngine.Networking; using Console = Game.Console; using Object = FlaxEngine.Object; namespace Game; public static unsafe partial class NetworkManager { public const byte DemoVer = 2; private struct DemoNetworkEventHeader { public NetworkEventType EventType; public uint SenderId; public uint MessageId; public uint Length; } private static List buffer; private static IEnumerator bufferEnumerable; private static GZipStream demoStream; // record private static FileStream demoFileStream; public static bool IsRecording => demoStream != null; private static long flushedFrames = 0; public static bool IsDemoPlaying => bufferEnumerable != null; public static bool PlayDemo(string demoName) { { Cleanup(); clientWorldStateManager = new WorldStateManager(isServer: true); } if (!ReadDemo(demoName)) { Console.Print("Failed to read demo."); return false; } /*if (client == null) { Console.Print("Failed to create NetworkPeer."); return false; } if (!client.Connect()) { Console.Print("Failed to connect to the server."); return false; }*/ Scripting.FixedUpdate += OnDemoUpdate; //if (!listenServer) { Scripting.Exit += Cleanup; //Level.ActorSpawned += OnClientActorSpawned; } return true; } public static void RecordDemo(string demoPath) { if (IsRecording) StopRecording(); buffer = new List(); var demoFolder = Directory.GetParent(demoPath); if (!demoFolder.Exists) Directory.CreateDirectory(demoFolder.FullName); demoFileStream = File.Open(demoPath, FileMode.Create, FileAccess.Write); demoStream = new GZipStream(demoFileStream, CompressionMode.Compress); demoStream.WriteByte(DemoVer); } private static void RecordMessage(ref NetworkEvent networkEvent) { if (!IsRecording) return; NetworkEvent recordedEvent = networkEvent; recordedEvent.Message.Buffer = (byte*)NativeMemory.Alloc(recordedEvent.Message.Length); recordedEvent.Sender.ConnectionId = networkEvent.Sender.ConnectionId; NativeMemory.Copy(networkEvent.Message.Buffer, recordedEvent.Message.Buffer, recordedEvent.Message.Length); buffer.Add(recordedEvent); } public static void FlushDemo() { if (!IsRecording) return; Stopwatch sw = Stopwatch.StartNew(); Span bytes = stackalloc byte[Unsafe.SizeOf()]; foreach (ref NetworkEvent networkEvent in CollectionsMarshal.AsSpan(buffer)) { DemoNetworkEventHeader header = new DemoNetworkEventHeader() { EventType = networkEvent.EventType, SenderId = networkEvent.Sender.ConnectionId, Length = networkEvent.Message.Length, MessageId = networkEvent.Message.MessageId, }; MemoryMarshal.Write(bytes, header); demoStream.Write(bytes); Span messageBytes = new Span(networkEvent.Message.Buffer, (int)networkEvent.Message.Length); demoStream.Write(messageBytes); // TODO: free networkEvent buffer } sw.Stop(); flushedFrames += buffer.Count; buffer.Clear(); FlaxEngine.Debug.Write(LogType.Info, $"Wrote demo in {sw.Elapsed.TotalMilliseconds}ms, frames: {flushedFrames}"); } public static void StopRecording() { if (!IsRecording) return; FlushDemo(); demoStream.Close(); demoStream = null; demoFileStream.Close(); demoFileStream = null; } private static unsafe bool ReadDemo(string demoPath) { if (!File.Exists(demoPath)) return false; buffer = new List(); Stopwatch sw = Stopwatch.StartNew(); using FileStream fileStream = File.OpenRead(demoPath); using GZipStream stream = new GZipStream(fileStream, CompressionMode.Decompress); int ver = stream.ReadByte(); if (ver != DemoVer) { Console.Print($"Demo version mismatch, expected {DemoVer}, got {ver}"); stream.Close(); return false; } int headerSize = Unsafe.SizeOf(); Span headerBuffer = stackalloc byte[headerSize]; while (true) { { int bytesLeftInBuffer = headerSize; do { int readBytes = stream.Read(headerBuffer.Slice(headerSize - bytesLeftInBuffer, bytesLeftInBuffer)); if (readBytes == 0) break; bytesLeftInBuffer -= readBytes; } while (bytesLeftInBuffer > 0); if (bytesLeftInBuffer > 0) break; // EOF; } DemoNetworkEventHeader header = MemoryMarshal.Read(headerBuffer); //buffer.Add(new NetworkEvent()); //ref NetworkEvent networkEvent = ref buffer[buffer.Length]; // collectionmarshal NetworkEvent networkEvent = new NetworkEvent(); networkEvent.EventType = header.EventType; networkEvent.Message.Position = 0; networkEvent.Message.MessageId = header.MessageId; networkEvent.Message.Length = networkEvent.Message.BufferSize = header.Length; networkEvent.Sender.ConnectionId = header.SenderId; if (header.Length > 0) { networkEvent.Message.Buffer = (byte*)NativeMemory.Alloc(header.Length); Span messageBufferSpan = new Span(networkEvent.Message.Buffer, (int)networkEvent.Message.BufferSize); { int bytesLeftInBuffer = (int)header.Length; do { int readBytes = stream.Read(messageBufferSpan.Slice((int)header.Length - bytesLeftInBuffer, bytesLeftInBuffer)); if (readBytes == 0) break; bytesLeftInBuffer -= readBytes; } while (bytesLeftInBuffer > 0); if (bytesLeftInBuffer > 0) break; // EOF; } } buffer.Add(networkEvent); } sw.Stop(); bufferEnumerable = buffer.GetEnumerator(); Console.Print($"Demo parse time {sw.Elapsed.TotalMilliseconds}ms, frames: {buffer.Count} "); DemoEndFrame(); // advances to first frame return true; } private static void DemoEndFrame() { // TODO: check if the current state frame matches the current frame number before advancing /*asdf++; if (asdf < 8) return;*/ if (bufferEnumerable == null || !bufferEnumerable.MoveNext()) { if (buffer.Any()) { bufferEnumerable.Dispose(); bufferEnumerable = null; buffer.Clear(); Console.Print("Demo ended"); } return; } //var actorState = currentState.actor; //currentState.input = bufferEnumerable.Current; //frame++; //currentState.actor = actorState; } private static void OnDemoUpdate() { if (!IsDemoPlaying) return; using Utilities.ScopeProfiler _ = Utilities.ProfileScope("NetworkManager_OnDemoUpdate"); NetworkEvent demoEvent = bufferEnumerable.Current; // ref? // TODO: change/randomize Sender.ConnectionId? try { //IsLocalClient = server != null; //IsClient = true; OnClientReadMessage(ref demoEvent); } finally { //IsLocalClient = false; //IsClient = false; //TODO: recycle event? } DemoEndFrame(); } }