mirror of
https://github.com/Sanae6/SmoOnlineServer.git
synced 2024-11-22 03:05:16 +00:00
Implement console commands and settings (flip list broken)
This commit is contained in:
parent
78af568603
commit
821301e756
8 changed files with 220 additions and 68 deletions
26
Server/CommandHandler.cs
Normal file
26
Server/CommandHandler.cs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
namespace Server;
|
||||||
|
|
||||||
|
public static class CommandHandler {
|
||||||
|
public delegate string Handler(string[] args);
|
||||||
|
|
||||||
|
public static Dictionary<string, Handler> Handlers = new Dictionary<string, Handler>();
|
||||||
|
|
||||||
|
static CommandHandler() {
|
||||||
|
RegisterCommand("help", _ => $"Valid commands: {string.Join(", ", Handlers.Keys)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RegisterCommand(string name, Handler handler) {
|
||||||
|
Handlers[name] = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetResult(string input) {
|
||||||
|
try {
|
||||||
|
string[] args = input.Split(' ');
|
||||||
|
if (args.Length == 0) return "No command entered, see help command for valid commands";
|
||||||
|
string commandName = args[0];
|
||||||
|
return Handlers.TryGetValue(commandName, out Handler? handler) ? handler(args[1..]) : $"Invalid command {args[0]}, see help command for valid commands";
|
||||||
|
} catch (Exception e) {
|
||||||
|
return $"An error occured while trying to process your command: {e}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
Server/FlipOptions.cs
Normal file
7
Server/FlipOptions.cs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
namespace Server;
|
||||||
|
|
||||||
|
public enum FlipOptions {
|
||||||
|
Both,
|
||||||
|
Self,
|
||||||
|
Others
|
||||||
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using Server;
|
using Server;
|
||||||
|
using Shared;
|
||||||
using Shared.Packet.Packets;
|
using Shared.Packet.Packets;
|
||||||
|
using Tomlyn;
|
||||||
using Timer = System.Timers.Timer;
|
using Timer = System.Timers.Timer;
|
||||||
|
|
||||||
Server.Server server = new Server.Server();
|
Server.Server server = new Server.Server();
|
||||||
|
@ -21,23 +23,17 @@ async Task ClientSyncShineBag(Client client) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async void SyncShineBag() {
|
async void SyncShineBag() {
|
||||||
await Parallel.ForEachAsync(server.Clients, async (client, _) => {
|
await Parallel.ForEachAsync(server.Clients, async (client, _) => { await ClientSyncShineBag(client); });
|
||||||
await ClientSyncShineBag(client);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer timer = new Timer(120000);
|
Timer timer = new Timer(120000);
|
||||||
timer.AutoReset = true;
|
timer.AutoReset = true;
|
||||||
timer.Enabled = true;
|
timer.Enabled = true;
|
||||||
timer.Elapsed += (_, _) => {
|
timer.Elapsed += (_, _) => { SyncShineBag(); };
|
||||||
SyncShineBag();
|
|
||||||
};
|
|
||||||
timer.Start();
|
timer.Start();
|
||||||
bool piss = false;
|
bool flipEnabled = Settings.Instance.Flip.EnabledOnStart;
|
||||||
|
|
||||||
Guid lycel = Guid.Parse("d5feae62-2e71-1000-88fd-597ea147ae88");
|
float MarioSize(bool is2d) => is2d ? 180 : 160;
|
||||||
// Guid lycel = Guid.Parse("5e1f9db4-1c27-1000-a421-4701972e443e");
|
|
||||||
Guid test = Guid.Parse("00000001-0000-0000-0000-000000000000");
|
|
||||||
|
|
||||||
server.PacketHandler = (c, p) => {
|
server.PacketHandler = (c, p) => {
|
||||||
switch (p) {
|
switch (p) {
|
||||||
|
@ -55,18 +51,19 @@ server.PacketHandler = (c, p) => {
|
||||||
SyncShineBag();
|
SyncShineBag();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case PlayerPacket playerPacket when c.Id == lycel && c.Id != test && piss: {
|
case PlayerPacket playerPacket when flipEnabled && Settings.Instance.Flip.Pov is FlipOptions.Both or FlipOptions.Others && Settings.Instance.Flip.Players.Contains(c.Id): {
|
||||||
playerPacket.Position += Vector3.UnitY * 160;
|
playerPacket.Position += Vector3.UnitY * MarioSize(playerPacket.Is2d);
|
||||||
playerPacket.Rotation *= Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationX(MathF.PI)) * Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationY(MathF.PI));
|
playerPacket.Rotation *= Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationX(MathF.PI)) * Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationY(MathF.PI));
|
||||||
server.Broadcast(playerPacket, c);
|
server.Broadcast(playerPacket, c);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
case PlayerPacket playerPacket when c.Id != lycel && piss: {
|
case PlayerPacket playerPacket when flipEnabled && Settings.Instance.Flip.Pov is FlipOptions.Both or FlipOptions.Self && !Settings.Instance.Flip.Players.Contains(c.Id): {
|
||||||
server.BroadcastReplace(playerPacket, c, (from, to, sp) => {
|
server.BroadcastReplace(playerPacket, c, (from, to, sp) => {
|
||||||
if (to.Id == lycel) {
|
if (Settings.Instance.Flip.Players.Contains(to.Id)) {
|
||||||
sp.Position += Vector3.UnitY * 160;
|
sp.Position += Vector3.UnitY * MarioSize(playerPacket.Is2d);
|
||||||
sp.Rotation *= Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationX(MathF.PI)) * Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationY(MathF.PI));
|
sp.Rotation *= Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationX(MathF.PI)) * Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationY(MathF.PI));
|
||||||
}
|
}
|
||||||
|
|
||||||
to.Send(sp, from);
|
to.Send(sp, from);
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
|
@ -76,12 +73,71 @@ server.PacketHandler = (c, p) => {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
Task.Run(() => {
|
CommandHandler.RegisterCommand("flip", args => {
|
||||||
while (true) {
|
const string optionUsage = "Valid options: list, add <user id>, remove <user id>, set <true/false>, pov <both/self/others>";
|
||||||
Console.ReadLine();
|
if (args.Length < 1)
|
||||||
piss = !piss;
|
return optionUsage;
|
||||||
server.Logger.Warn($"Lycel flipped to {piss}");
|
switch (args[0]) {
|
||||||
|
case "list" when args.Length == 1:
|
||||||
|
return "User ids: " + string.Join(", ", Settings.Instance.Flip.Players.ToList());
|
||||||
|
case "add" when args.Length == 2: {
|
||||||
|
if (Guid.TryParse(args[1], out Guid result)) {
|
||||||
|
Settings.Instance.Flip.Players.Add(result);
|
||||||
|
Settings.SaveSettings();
|
||||||
|
return $"Added {result} to flipped players";
|
||||||
|
} else
|
||||||
|
return $"Invalid user id {args[1]}";
|
||||||
|
}
|
||||||
|
case "remove" when args.Length == 2: {
|
||||||
|
if (Guid.TryParse(args[1], out Guid result)) {
|
||||||
|
string output = Settings.Instance.Flip.Players.Remove(result) ? $"Removed {result} to flipped players" : $"User {result} wasn't in the flipped players list";
|
||||||
|
Settings.SaveSettings();
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"Invalid user id {args[1]}";
|
||||||
|
}
|
||||||
|
case "set" when args.Length == 2: {
|
||||||
|
if (bool.TryParse(args[1], out bool result)) {
|
||||||
|
flipEnabled = result;
|
||||||
|
return result ? "Enabled player flipping for session" : "Disabled player flipping for session";
|
||||||
|
}
|
||||||
|
|
||||||
|
return optionUsage;
|
||||||
|
}
|
||||||
|
case "pov" when args.Length == 2: {
|
||||||
|
if (Enum.TryParse(args[1], true, out FlipOptions result)) {
|
||||||
|
Settings.Instance.Flip.Pov = result;
|
||||||
|
Settings.SaveSettings();
|
||||||
|
return $"Point of view set to {result}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return optionUsage;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return optionUsage;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.Listen(1027);
|
CommandHandler.RegisterCommand("shine", (args) => {
|
||||||
|
const string optionUsage = "Valid options: list";
|
||||||
|
if (args.Length < 1)
|
||||||
|
return optionUsage;
|
||||||
|
switch (args[0]) {
|
||||||
|
case "list" when args.Length == 1:
|
||||||
|
return $"Shines: {string.Join(", ", shineBag)}";
|
||||||
|
default:
|
||||||
|
return optionUsage;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Task.Run(() => {
|
||||||
|
Logger logger = new Logger("Console");
|
||||||
|
logger.Info("Run help command for valid commands.");
|
||||||
|
while (true) {
|
||||||
|
string? text = Console.ReadLine();
|
||||||
|
if (text != null) logger.Info(CommandHandler.GetResult(text));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.Listen();
|
|
@ -15,13 +15,13 @@ public class Server {
|
||||||
public Func<Client, IPacket, bool>? PacketHandler = null!;
|
public Func<Client, IPacket, bool>? PacketHandler = null!;
|
||||||
public event Action<Client, ConnectPacket> ClientJoined = null!;
|
public event Action<Client, ConnectPacket> ClientJoined = null!;
|
||||||
|
|
||||||
public async Task Listen(ushort port) {
|
public async Task Listen() {
|
||||||
Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||||
serverSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
serverSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
||||||
serverSocket.Bind(new IPEndPoint(IPAddress.Any, port));
|
serverSocket.Bind(new IPEndPoint(IPAddress.Parse(Settings.Instance.Server.Address), Settings.Instance.Server.Port));
|
||||||
serverSocket.Listen();
|
serverSocket.Listen();
|
||||||
|
|
||||||
Logger.Info($"Listening on port {port}");
|
Logger.Info($"Listening on {serverSocket.LocalEndPoint}");
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
Socket socket = await serverSocket.AcceptAsync();
|
Socket socket = await serverSocket.AcceptAsync();
|
||||||
|
|
65
Server/Settings.cs
Normal file
65
Server/Settings.cs
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
using System.Net;
|
||||||
|
using Shared;
|
||||||
|
using Tomlyn;
|
||||||
|
using Tomlyn.Model;
|
||||||
|
using Tomlyn.Syntax;
|
||||||
|
|
||||||
|
namespace Server;
|
||||||
|
|
||||||
|
public class Settings {
|
||||||
|
public static Settings Instance = new Settings();
|
||||||
|
private static readonly Logger Logger = new Logger("Settings");
|
||||||
|
static Settings() {
|
||||||
|
LoadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void LoadSettings() {
|
||||||
|
if (File.Exists("settings.toml")) {
|
||||||
|
string text = File.ReadAllText("settings.toml");
|
||||||
|
if (Toml.TryToModel(text, out Settings? settings, out DiagnosticsBag? bag, options: new TomlModelOptions() {
|
||||||
|
ConvertTo = (value, _) => {
|
||||||
|
if (value is string str && Guid.TryParse(str, out Guid result))
|
||||||
|
return result;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
Logger.Info("Loaded settings from settings.toml");
|
||||||
|
else
|
||||||
|
Logger.Warn($"Failed to load settings.toml: {bag}");
|
||||||
|
if (settings != null) Instance = settings;
|
||||||
|
} else {
|
||||||
|
SaveSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SaveSettings(Settings? settings = null) {
|
||||||
|
try {
|
||||||
|
File.WriteAllText("settings.toml", Toml.FromModel(settings ?? Instance!, new TomlModelOptions {
|
||||||
|
ConvertTo = (x, _) => {
|
||||||
|
if (x is Guid guid)
|
||||||
|
return guid.ToString();
|
||||||
|
|
||||||
|
return null!;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
Logger.Info("Saved settings to settings.toml");
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.Error($"Failed to save settings.toml {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerTable Server { get; set; } = new ServerTable();
|
||||||
|
public FlipTable Flip { get; set; } = new FlipTable();
|
||||||
|
|
||||||
|
public class ServerTable {
|
||||||
|
public string Address { get; set; } = IPAddress.Any.ToString();
|
||||||
|
public ushort Port { get; set; } = 1027;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FlipTable {
|
||||||
|
public List<Guid> Players { get; set; } = new List<Guid>();
|
||||||
|
public bool EnabledOnStart { get; set; } = true;
|
||||||
|
public FlipOptions Pov { get; set; } = FlipOptions.Both;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +0,0 @@
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Shared.Packet.Packets;
|
|
||||||
|
|
||||||
public struct LogPacket : IPacket {
|
|
||||||
public string Text;
|
|
||||||
public void Serialize(Span<byte> data) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Deserialize(Span<byte> data) {
|
|
||||||
Text = Encoding.UTF8.GetString(data[..]).TrimNullTerm();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -6,7 +6,7 @@ namespace Shared.Packet.Packets;
|
||||||
|
|
||||||
[Packet(PacketType.Player)]
|
[Packet(PacketType.Player)]
|
||||||
public struct PlayerPacket : IPacket {
|
public struct PlayerPacket : IPacket {
|
||||||
public const int NameSize = 0x30;
|
public const int NameSize = 0x20;
|
||||||
|
|
||||||
public Vector3 Position;
|
public Vector3 Position;
|
||||||
public Quaternion Rotation;
|
public Quaternion Rotation;
|
||||||
|
@ -20,14 +20,17 @@ public struct PlayerPacket : IPacket {
|
||||||
public bool IsIt;
|
public bool IsIt;
|
||||||
public int ScenarioNum;
|
public int ScenarioNum;
|
||||||
|
|
||||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = NameSize)]
|
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x40)]
|
||||||
public string Stage;
|
public string Stage = "";
|
||||||
|
|
||||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = NameSize)]
|
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = NameSize)]
|
||||||
public string Act;
|
public string Act = "";
|
||||||
|
|
||||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = NameSize)]
|
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = NameSize)]
|
||||||
public string SubAct;
|
public string SubAct = "";
|
||||||
|
|
||||||
|
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = NameSize)]
|
||||||
|
public string Hack = "";
|
||||||
|
|
||||||
public void Serialize(Span<byte> data) {
|
public void Serialize(Span<byte> data) {
|
||||||
int offset = 0;
|
int offset = 0;
|
||||||
|
@ -43,11 +46,13 @@ public struct PlayerPacket : IPacket {
|
||||||
MemoryMarshal.Write(data[offset++..], ref IsIt);
|
MemoryMarshal.Write(data[offset++..], ref IsIt);
|
||||||
MemoryMarshal.Write(data[offset..], ref ScenarioNum);
|
MemoryMarshal.Write(data[offset..], ref ScenarioNum);
|
||||||
offset += 5;
|
offset += 5;
|
||||||
Encoding.UTF8.GetBytes(Stage).CopyTo(data[offset..(offset + NameSize)]);
|
Encoding.UTF8.GetBytes(Stage).CopyTo(data[offset..(offset + 0x40)]);
|
||||||
offset += NameSize;
|
offset += 0x40;
|
||||||
Encoding.UTF8.GetBytes(Act).CopyTo(data[offset..(offset + NameSize)]);
|
Encoding.UTF8.GetBytes(Act).CopyTo(data[offset..(offset + NameSize)]);
|
||||||
offset += NameSize;
|
offset += NameSize;
|
||||||
Encoding.UTF8.GetBytes(SubAct).CopyTo(data[offset..]);
|
Encoding.UTF8.GetBytes(SubAct).CopyTo(data[offset..(offset + NameSize)]);
|
||||||
|
offset += NameSize;
|
||||||
|
Encoding.UTF8.GetBytes(Hack).CopyTo(data[offset..]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Deserialize(Span<byte> data) {
|
public void Deserialize(Span<byte> data) {
|
||||||
|
@ -64,10 +69,12 @@ public struct PlayerPacket : IPacket {
|
||||||
// offset++; // padding
|
// offset++; // padding
|
||||||
ScenarioNum = MemoryMarshal.Read<int>(data[offset..]);
|
ScenarioNum = MemoryMarshal.Read<int>(data[offset..]);
|
||||||
offset += 5;
|
offset += 5;
|
||||||
Stage = Encoding.UTF8.GetString(data[offset..(offset + NameSize)]).TrimEnd('\0');
|
Stage = Encoding.UTF8.GetString(data[offset..(offset + 0x40)]).TrimEnd('\0');
|
||||||
offset += NameSize;
|
offset += 0x40;
|
||||||
Act = Encoding.UTF8.GetString(data[offset..(offset + NameSize)]).TrimEnd('\0');
|
Act = Encoding.UTF8.GetString(data[offset..(offset + NameSize)]).TrimEnd('\0');
|
||||||
offset += NameSize;
|
offset += NameSize;
|
||||||
SubAct = Encoding.UTF8.GetString(data[offset..(offset + NameSize)]).TrimEnd('\0');
|
SubAct = Encoding.UTF8.GetString(data[offset..(offset + NameSize)]).TrimEnd('\0');
|
||||||
|
offset += NameSize;
|
||||||
|
SubAct = Encoding.UTF8.GetString(data[offset..(offset + NameSize)]).TrimEnd('\0');
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -17,13 +17,14 @@ Vector3 basePoint = Vector3.Zero;
|
||||||
|
|
||||||
PacketType[] reboundPackets = {
|
PacketType[] reboundPackets = {
|
||||||
PacketType.Player,
|
PacketType.Player,
|
||||||
// PacketType.Cap,
|
PacketType.Cap,
|
||||||
PacketType.Capture,
|
PacketType.Capture,
|
||||||
PacketType.Costume,
|
PacketType.Costume,
|
||||||
PacketType.Tag,
|
PacketType.Tag,
|
||||||
PacketType.Shine
|
PacketType.Shine
|
||||||
};
|
};
|
||||||
|
|
||||||
|
string lastCapture = "";
|
||||||
async Task S() {
|
async Task S() {
|
||||||
IMemoryOwner<byte> owner = MemoryPool<byte>.Shared.Rent(Constants.MaxPacketSize);
|
IMemoryOwner<byte> owner = MemoryPool<byte>.Shared.Rent(Constants.MaxPacketSize);
|
||||||
while (true) {
|
while (true) {
|
||||||
|
@ -31,12 +32,16 @@ async Task S() {
|
||||||
PacketHeader header = MemoryMarshal.Read<PacketHeader>(owner.Memory.Span);
|
PacketHeader header = MemoryMarshal.Read<PacketHeader>(owner.Memory.Span);
|
||||||
PacketType type = header.Type;
|
PacketType type = header.Type;
|
||||||
if (header.Id != otherId) continue;
|
if (header.Id != otherId) continue;
|
||||||
// if (type is PacketType.Player) {
|
if (type is PacketType.Player) {
|
||||||
// CapPacket cap = new CapPacket();
|
// CapPacket cap = new CapPacket();
|
||||||
// PlayerPacket playerPacket = new PlayerPacket();
|
PlayerPacket playerPacket = new PlayerPacket();
|
||||||
// playerPacket.Deserialize(owner.Memory.Span[Constants.HeaderSize..]);
|
playerPacket.Deserialize(owner.Memory.Span[Constants.HeaderSize..]);
|
||||||
|
logger.Info(playerPacket.Hack);
|
||||||
|
if (playerPacket.Hack != lastCapture) {
|
||||||
|
logger.Info($"Changed to hack: {lastCapture = playerPacket.Hack}");
|
||||||
|
}
|
||||||
// cap.Position = playerPacket.Position + Vector3.UnitY * 500f;
|
// cap.Position = playerPacket.Position + Vector3.UnitY * 500f;
|
||||||
// // cap.Rotation = Quaternion.CreateFromYawPitchRoll(0,0,0);
|
// cap.Rotation = Quaternion.CreateFromYawPitchRoll(0,0,0);
|
||||||
// cap.CapAnim = "StayR";
|
// cap.CapAnim = "StayR";
|
||||||
// playerPacket.Position = new Vector3(1000000f);
|
// playerPacket.Position = new Vector3(1000000f);
|
||||||
// playerPacket.ThrowingCap = true;
|
// playerPacket.ThrowingCap = true;
|
||||||
|
@ -49,7 +54,7 @@ async Task S() {
|
||||||
// cap.Serialize(owner.Memory.Span[Constants.HeaderSize..]);
|
// cap.Serialize(owner.Memory.Span[Constants.HeaderSize..]);
|
||||||
// await stream.WriteAsync(owner.Memory[..Constants.MaxPacketSize]);
|
// await stream.WriteAsync(owner.Memory[..Constants.MaxPacketSize]);
|
||||||
// continue;
|
// continue;
|
||||||
// }
|
}
|
||||||
if (reboundPackets.All(x => x != type)) continue;
|
if (reboundPackets.All(x => x != type)) continue;
|
||||||
header.Id = ownId;
|
header.Id = ownId;
|
||||||
MemoryMarshal.Write(owner.Memory.Span, ref header);
|
MemoryMarshal.Write(owner.Memory.Span, ref header);
|
||||||
|
|
Loading…
Reference in a new issue