Files
GoakeFlax/Source/Game/GameMode/NetworkManager_Demo.cs
2025-03-28 15:24:44 +02:00

278 lines
8.5 KiB
C#

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<NetworkEvent> buffer;
private static IEnumerator<NetworkEvent> 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);
World.InitServer();
}
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<NetworkEvent>();
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<byte> bytes = stackalloc byte[Unsafe.SizeOf<DemoNetworkEventHeader>()];
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<byte> messageBytes = new Span<byte>(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<NetworkEvent>();
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<DemoNetworkEventHeader>();
Span<byte> 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<DemoNetworkEventHeader>(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<byte> messageBufferSpan = new Span<byte>(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();
}
}