mirror of
https://github.com/Stone-Red-Code/RemSox.git
synced 2026-09-06 23:41:33 +02:00
Reorganize project structure
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
using Org.BouncyCastle.Crypto.Engines;
|
||||
using Org.BouncyCastle.Crypto.Modes;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace RemSox.Shared.Cryptography;
|
||||
|
||||
public static class AesGcmCrypto
|
||||
{
|
||||
private const int NonceSize = 12;
|
||||
private const int TagSize = 128;
|
||||
|
||||
public static byte[] Encrypt(byte[] plainData, byte[] key)
|
||||
{
|
||||
byte[] nonce = new byte[NonceSize];
|
||||
RandomNumberGenerator.Fill(nonce);
|
||||
|
||||
GcmBlockCipher cipher = new GcmBlockCipher(new AesEngine());
|
||||
cipher.Init(true, new AeadParameters(new KeyParameter(key), TagSize, nonce));
|
||||
|
||||
byte[] output = new byte[cipher.GetOutputSize(plainData.Length)];
|
||||
|
||||
int len = cipher.ProcessBytes(plainData, 0, plainData.Length, output, 0);
|
||||
_ = cipher.DoFinal(output, len);
|
||||
|
||||
byte[] result = new byte[nonce.Length + output.Length];
|
||||
|
||||
Buffer.BlockCopy(nonce, 0, result, 0, nonce.Length);
|
||||
Buffer.BlockCopy(output, 0, result, nonce.Length, output.Length);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static string Encrypt(string text, byte[] key)
|
||||
{
|
||||
byte[] data = Encoding.UTF8.GetBytes(text);
|
||||
return Convert.ToBase64String(Encrypt(data, key));
|
||||
}
|
||||
|
||||
public static byte[] Decrypt(byte[] encryptedData, byte[] key)
|
||||
{
|
||||
byte[] nonce = new byte[NonceSize];
|
||||
Buffer.BlockCopy(encryptedData, 0, nonce, 0, NonceSize);
|
||||
|
||||
int cipherLength = encryptedData.Length - NonceSize;
|
||||
byte[] cipherBytes = new byte[cipherLength];
|
||||
|
||||
Buffer.BlockCopy(encryptedData, NonceSize, cipherBytes, 0, cipherLength);
|
||||
|
||||
GcmBlockCipher cipher = new GcmBlockCipher(new AesEngine());
|
||||
cipher.Init(false, new AeadParameters(new KeyParameter(key), TagSize, nonce));
|
||||
|
||||
byte[] plain = new byte[cipher.GetOutputSize(cipherBytes.Length)];
|
||||
|
||||
int len = cipher.ProcessBytes(cipherBytes, 0, cipherBytes.Length, plain, 0);
|
||||
_ = cipher.DoFinal(plain, len);
|
||||
|
||||
return plain;
|
||||
}
|
||||
|
||||
public static string Decrypt(string cipherText, byte[] key)
|
||||
{
|
||||
byte[] data = Convert.FromBase64String(cipherText);
|
||||
byte[] plain = Decrypt(data, key);
|
||||
return Encoding.UTF8.GetString(plain);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Org.BouncyCastle.Crypto;
|
||||
using Org.BouncyCastle.Crypto.Digests;
|
||||
using Org.BouncyCastle.Crypto.Generators;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
|
||||
namespace RemSox.Shared.Cryptography;
|
||||
|
||||
public static class Pkcs5S2PasswordKeyDeriver
|
||||
{
|
||||
private const int KeySize = 32; // 256-bit key
|
||||
private const int Iterations = 150_000; // PBKDF2 cost factor
|
||||
|
||||
public static byte[] DeriveKey(string password, byte[] salt)
|
||||
{
|
||||
Pkcs5S2ParametersGenerator generator = new Pkcs5S2ParametersGenerator(new Sha256Digest());
|
||||
|
||||
generator.Init(
|
||||
PbeParametersGenerator.Pkcs5PasswordToBytes(password.ToCharArray()),
|
||||
salt,
|
||||
Iterations
|
||||
);
|
||||
|
||||
return ((KeyParameter)generator.GenerateDerivedMacParameters(KeySize * 8))
|
||||
.GetKey();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using RemSox.Shared.Cryptography;
|
||||
|
||||
namespace RemSox.Shared.Networking;
|
||||
|
||||
public class AesPacketCrypto(byte[] key) : IPacketCrypto
|
||||
{
|
||||
public byte[] Decrypt(byte[] data)
|
||||
{
|
||||
return AesGcmCrypto.Decrypt(data, key);
|
||||
}
|
||||
|
||||
public byte[] Encrypt(byte[] data)
|
||||
{
|
||||
return AesGcmCrypto.Encrypt(data, key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace RemSox.Shared.Networking;
|
||||
|
||||
public interface IPacketCrypto
|
||||
{
|
||||
byte[] Encrypt(byte[] data);
|
||||
|
||||
byte[] Decrypt(byte[] data);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace RemSox.Shared.Networking;
|
||||
|
||||
/// <summary>
|
||||
/// Simple wrapper around TcpClient to hold connection-scoped resources safely.
|
||||
/// </summary>
|
||||
public class TcpConnection(TcpClient client) : IDisposable
|
||||
{
|
||||
public TcpClient Client { get; } = client;
|
||||
public NetworkStream Stream { get; } = client.GetStream();
|
||||
public object SendLock { get; } = new();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stream.Dispose();
|
||||
Client.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace RemSox.Shared.Networking;
|
||||
|
||||
public class TcpMessage
|
||||
{
|
||||
public string Type { get; set; } = string.Empty;
|
||||
public string RequestId { get; set; } = string.Empty;
|
||||
public byte[] Payload { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
||||
namespace RemSox.Shared.Networking;
|
||||
|
||||
public abstract class TcpRpcBase(IPacketCrypto? crypto = null)
|
||||
{
|
||||
protected readonly IPacketCrypto? crypto = crypto;
|
||||
protected readonly Dictionary<string, Func<TcpConnection, TcpMessage, Task>> handlers = [];
|
||||
protected readonly ConcurrentDictionary<string, TaskCompletionSource<byte[]>> pendingRequests = new();
|
||||
|
||||
// Security Guard: Prevent OOM/DoS via oversized length headers (Default: 32MB)
|
||||
protected const int MaxMessageSize = 32 * 1024 * 1024;
|
||||
|
||||
public void ListenTo(string type, Func<TcpConnection, byte[], Task> handler)
|
||||
{
|
||||
handlers[type] = async (conn, msg) => await handler(conn, msg.Payload);
|
||||
}
|
||||
|
||||
public void ListenTo(string type, Func<byte[], Task> handler)
|
||||
{
|
||||
handlers[type] = async (conn, msg) => await handler(msg.Payload);
|
||||
}
|
||||
|
||||
public void RespondTo(string type, Func<byte[], Task<byte[]>> handler)
|
||||
{
|
||||
handlers[type] = async (conn, msg) =>
|
||||
{
|
||||
byte[] res = await handler(msg.Payload);
|
||||
SendRaw(conn, new TcpMessage
|
||||
{
|
||||
Type = type,
|
||||
RequestId = msg.RequestId,
|
||||
Payload = res
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
protected void SendRaw(TcpConnection conn, TcpMessage msg)
|
||||
{
|
||||
byte[] rawData = SerializeMessage(msg);
|
||||
|
||||
if (crypto is not null)
|
||||
{
|
||||
rawData = crypto.Encrypt(rawData);
|
||||
}
|
||||
|
||||
byte[] packet = new byte[4 + rawData.Length];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(packet, rawData.Length);
|
||||
Array.Copy(rawData, 0, packet, 4, rawData.Length);
|
||||
|
||||
lock (conn.SendLock)
|
||||
{
|
||||
conn.Stream.Write(packet, 0, packet.Length);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReadExact(NetworkStream stream, byte[] buffer, int offset, int count)
|
||||
{
|
||||
int totalRead = 0;
|
||||
while (totalRead < count)
|
||||
{
|
||||
int read = stream.Read(buffer, offset + totalRead, count - totalRead);
|
||||
if (read == 0)
|
||||
{
|
||||
Thread.Sleep(10);
|
||||
continue;
|
||||
}
|
||||
totalRead += read;
|
||||
}
|
||||
}
|
||||
|
||||
protected void HandleConnection(TcpConnection conn, CancellationToken token = default)
|
||||
{
|
||||
NetworkStream stream = conn.Stream;
|
||||
byte[] lengthBytes = new byte[4];
|
||||
|
||||
try
|
||||
{
|
||||
while (!token.IsCancellationRequested && conn.Client.Connected)
|
||||
{
|
||||
Console.WriteLine("[TcpRpc] Waiting for incoming packet...");
|
||||
|
||||
ReadExact(stream, lengthBytes, 0, 4);
|
||||
int length = BinaryPrimitives.ReadInt32LittleEndian(lengthBytes);
|
||||
|
||||
if (length is <= 0 or > MaxMessageSize)
|
||||
{
|
||||
Console.WriteLine($"[TcpRpc] Invalid packet length: {length}");
|
||||
Thread.Sleep(10);
|
||||
continue;
|
||||
}
|
||||
|
||||
Console.WriteLine($"[TcpRpc] Incoming packet length: {length} bytes");
|
||||
|
||||
byte[] buffer = new byte[length];
|
||||
ReadExact(stream, buffer, 0, length);
|
||||
|
||||
Console.WriteLine($"[TcpRpc] Incoming packet payload: {BitConverter.ToString(buffer)}");
|
||||
|
||||
if (crypto is not null)
|
||||
{
|
||||
buffer = crypto.Decrypt(buffer);
|
||||
}
|
||||
|
||||
Console.WriteLine($"[TcpRpc] Decrypted packet payload: {BitConverter.ToString(buffer)}");
|
||||
|
||||
TcpMessage? msg = DeserializeMessage(buffer);
|
||||
if (msg is null)
|
||||
{
|
||||
Console.WriteLine("[TcpRpc] Failed to deserialize incoming message.");
|
||||
continue;
|
||||
}
|
||||
|
||||
Console.WriteLine($"[TcpRpc] Incoming message type: {msg.Type}, requestId: {msg.RequestId}, payload length: {msg.Payload.Length} bytes");
|
||||
|
||||
if (pendingRequests.TryGetValue(msg.RequestId, out TaskCompletionSource<byte[]>? tcs))
|
||||
{
|
||||
_ = tcs.TrySetResult(msg.Payload);
|
||||
_ = pendingRequests.TryRemove(msg.RequestId, out _);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (handlers.TryGetValue(msg.Type, out Func<TcpConnection, TcpMessage, Task>? handler))
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await handler(conn, msg);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}, token);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
OnConnectionClosed(conn);
|
||||
conn.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] SerializeMessage(TcpMessage msg)
|
||||
{
|
||||
byte[] typeBytes = Encoding.UTF8.GetBytes(msg.Type);
|
||||
byte[] requestIdBytes = Encoding.UTF8.GetBytes(msg.RequestId);
|
||||
|
||||
byte[] data = new byte[4 + typeBytes.Length + 4 + requestIdBytes.Length + 4 + msg.Payload.Length];
|
||||
int offset = 0;
|
||||
|
||||
WriteInt32(data, ref offset, typeBytes.Length);
|
||||
typeBytes.CopyTo(data, offset);
|
||||
offset += typeBytes.Length;
|
||||
|
||||
WriteInt32(data, ref offset, requestIdBytes.Length);
|
||||
requestIdBytes.CopyTo(data, offset);
|
||||
offset += requestIdBytes.Length;
|
||||
|
||||
WriteInt32(data, ref offset, msg.Payload.Length);
|
||||
msg.Payload.CopyTo(data, offset);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private static TcpMessage? DeserializeMessage(byte[] data)
|
||||
{
|
||||
int offset = 0;
|
||||
if (offset + 4 > data.Length)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
int typeLen = ReadInt32(data, ref offset);
|
||||
if (offset + typeLen > data.Length)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string type = Encoding.UTF8.GetString(data, offset, typeLen);
|
||||
offset += typeLen;
|
||||
|
||||
if (offset + 4 > data.Length)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
int requestIdLen = ReadInt32(data, ref offset);
|
||||
if (offset + requestIdLen > data.Length)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string requestId = Encoding.UTF8.GetString(data, offset, requestIdLen);
|
||||
offset += requestIdLen;
|
||||
|
||||
if (offset + 4 > data.Length)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
int payloadLen = ReadInt32(data, ref offset);
|
||||
if (offset + payloadLen > data.Length)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] payload = new byte[payloadLen];
|
||||
Array.Copy(data, offset, payload, 0, payloadLen);
|
||||
|
||||
return new TcpMessage { Type = type, RequestId = requestId, Payload = payload };
|
||||
}
|
||||
|
||||
private static void WriteInt32(byte[] data, ref int offset, int value)
|
||||
{
|
||||
data[offset++] = (byte)(value & 0xFF);
|
||||
data[offset++] = (byte)((value >> 8) & 0xFF);
|
||||
data[offset++] = (byte)((value >> 16) & 0xFF);
|
||||
data[offset++] = (byte)((value >> 24) & 0xFF);
|
||||
}
|
||||
|
||||
private static int ReadInt32(byte[] data, ref int offset)
|
||||
{
|
||||
int val = data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) | (data[offset + 3] << 24);
|
||||
offset += 4;
|
||||
return val;
|
||||
}
|
||||
|
||||
protected abstract void OnConnectionClosed(TcpConnection conn);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace RemSox.Shared.Networking;
|
||||
|
||||
public class TcpRpcClient(IPacketCrypto? crypto = null) : TcpRpcBase(crypto)
|
||||
{
|
||||
private TcpConnection? connection;
|
||||
|
||||
public void Connect(string host, int port)
|
||||
{
|
||||
TcpClient client = new();
|
||||
client.Connect(host, port);
|
||||
|
||||
connection = new TcpConnection(client);
|
||||
|
||||
_ = Task.Run(() => HandleConnection(connection));
|
||||
}
|
||||
|
||||
public async Task<byte[]> RequestAsync(string type, byte[] request, TimeSpan timeout = default)
|
||||
{
|
||||
if (connection is null)
|
||||
{
|
||||
throw new InvalidOperationException("Client not connected.");
|
||||
}
|
||||
|
||||
if (timeout == default)
|
||||
{
|
||||
timeout = TimeSpan.FromSeconds(30);
|
||||
}
|
||||
|
||||
string requestId = Guid.NewGuid().ToString();
|
||||
TaskCompletionSource<byte[]> tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
pendingRequests[requestId] = tcs;
|
||||
|
||||
try
|
||||
{
|
||||
SendRaw(connection, new TcpMessage
|
||||
{
|
||||
Type = type,
|
||||
RequestId = requestId,
|
||||
Payload = request
|
||||
});
|
||||
|
||||
using CancellationTokenSource timeoutCts = new(timeout);
|
||||
using (timeoutCts.Token.Register(() => tcs.TrySetCanceled()))
|
||||
{
|
||||
return await tcs.Task;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_ = pendingRequests.TryRemove(requestId, out _);
|
||||
}
|
||||
}
|
||||
|
||||
public void Send(string type, byte[] data)
|
||||
{
|
||||
if (connection is null)
|
||||
{
|
||||
throw new InvalidOperationException("Client not connected.");
|
||||
}
|
||||
|
||||
SendRaw(connection, new TcpMessage
|
||||
{
|
||||
Type = type,
|
||||
RequestId = Guid.NewGuid().ToString(),
|
||||
Payload = data
|
||||
});
|
||||
}
|
||||
|
||||
protected override void OnConnectionClosed(TcpConnection conn)
|
||||
{
|
||||
if (connection == conn)
|
||||
{
|
||||
connection = null;
|
||||
}
|
||||
|
||||
foreach (TaskCompletionSource<byte[]> req in pendingRequests.Values)
|
||||
{
|
||||
_ = req.TrySetException(new SocketException((int)SocketError.ConnectionReset));
|
||||
}
|
||||
pendingRequests.Clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace RemSox.Shared.Networking;
|
||||
|
||||
public class TcpRpcServer(IPacketCrypto? crypto = null) : TcpRpcBase(crypto)
|
||||
{
|
||||
private readonly ConcurrentDictionary<TcpConnection, byte> connections = new();
|
||||
private readonly ConcurrentDictionary<string, byte> activeEndpoints = new();
|
||||
private TcpListener? listener;
|
||||
private CancellationTokenSource? cts;
|
||||
|
||||
public void StartAsync(int port, CancellationToken token = default)
|
||||
{
|
||||
cts = CancellationTokenSource.CreateLinkedTokenSource(token);
|
||||
|
||||
listener = new TcpListener(IPAddress.Any, port);
|
||||
listener.Start();
|
||||
|
||||
try
|
||||
{
|
||||
while (!cts.IsCancellationRequested)
|
||||
{
|
||||
TcpClient client = listener.AcceptTcpClient();
|
||||
string ep = client.Client.RemoteEndPoint?.ToString() ?? "";
|
||||
|
||||
if (!activeEndpoints.TryAdd(ep, 0))
|
||||
{
|
||||
client.Close();
|
||||
Thread.Sleep(10);
|
||||
continue;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Client connected: {ep}");
|
||||
TcpConnection conn = new(client);
|
||||
_ = connections.TryAdd(conn, 0);
|
||||
|
||||
_ = Task.Run(() => HandleConnection(conn, cts.Token));
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
cts?.Cancel();
|
||||
listener?.Stop();
|
||||
|
||||
foreach (TcpConnection c in connections.Keys)
|
||||
{
|
||||
c.Dispose();
|
||||
}
|
||||
|
||||
connections.Clear();
|
||||
}
|
||||
|
||||
public void SendRawToAll(string type, byte[] payload)
|
||||
{
|
||||
foreach (TcpConnection conn in connections.Keys)
|
||||
{
|
||||
SendRaw(conn, new TcpMessage
|
||||
{
|
||||
Type = type,
|
||||
RequestId = Guid.NewGuid().ToString(),
|
||||
Payload = payload
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnConnectionClosed(TcpConnection conn)
|
||||
{
|
||||
_ = connections.TryRemove(conn, out _);
|
||||
|
||||
string ep = conn.Client.Client.RemoteEndPoint?.ToString() ?? "";
|
||||
_ = activeEndpoints.TryRemove(ep, out _);
|
||||
|
||||
Console.WriteLine($"Client disconnected: {ep}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace RemSox.Shared.UI.GUI.Rendering;
|
||||
|
||||
public interface IRenderSource
|
||||
{
|
||||
void Render(IEnumerable<RenderCommand> commands);
|
||||
void Composite();
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
using System.Drawing;
|
||||
using System.Text;
|
||||
|
||||
namespace RemSox.Shared.UI.GUI.Rendering;
|
||||
|
||||
public class RenderCommand
|
||||
{
|
||||
public required int WindowId { get; set; }
|
||||
public required int ElementId { get; set; }
|
||||
public required RenderCommandType Type { get; set; }
|
||||
public Point Position { get; set; }
|
||||
public Dictionary<string, object?> Properties { get; set; } = [];
|
||||
|
||||
public byte[] ToBytes()
|
||||
{
|
||||
using MemoryStream ms = new();
|
||||
Write(ms);
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
public void Write(Stream s)
|
||||
{
|
||||
s.WriteByte((byte)Type);
|
||||
WriteVarint(s, WindowId);
|
||||
WriteVarint(s, ElementId);
|
||||
|
||||
switch (Type)
|
||||
{
|
||||
case RenderCommandType.CreateWindow:
|
||||
WriteInt16(s, Position.X);
|
||||
WriteInt16(s, Position.Y);
|
||||
Size size = GetProp("Size", new Size(160, 120));
|
||||
WriteUInt16(s, size.Width);
|
||||
WriteUInt16(s, size.Height);
|
||||
WriteVarint(s, GetProp("ZIndex", 0));
|
||||
break;
|
||||
|
||||
case RenderCommandType.DestroyWindow:
|
||||
break;
|
||||
|
||||
case RenderCommandType.MoveWindow:
|
||||
WriteInt16(s, Position.X);
|
||||
WriteInt16(s, Position.Y);
|
||||
break;
|
||||
|
||||
case RenderCommandType.DrawFilledRect:
|
||||
case RenderCommandType.DrawRectBorder:
|
||||
WriteInt16(s, Position.X);
|
||||
WriteInt16(s, Position.Y);
|
||||
Size rs = GetProp("Size", new Size(10, 10));
|
||||
WriteUInt16(s, rs.Width);
|
||||
WriteUInt16(s, rs.Height);
|
||||
WriteColor(s, GetProp("Color", Color.White));
|
||||
break;
|
||||
|
||||
case RenderCommandType.DrawFilledCircle:
|
||||
case RenderCommandType.DrawCircle:
|
||||
WriteInt16(s, Position.X);
|
||||
WriteInt16(s, Position.Y);
|
||||
WriteUInt16(s, GetProp("Radius", 10));
|
||||
WriteColor(s, GetProp("Color", Color.White));
|
||||
break;
|
||||
|
||||
case RenderCommandType.DrawPoint:
|
||||
WriteInt16(s, Position.X);
|
||||
WriteInt16(s, Position.Y);
|
||||
WriteColor(s, GetProp("Color", Color.White));
|
||||
break;
|
||||
|
||||
case RenderCommandType.DrawText:
|
||||
WriteInt16(s, Position.X);
|
||||
WriteInt16(s, Position.Y);
|
||||
WriteColor(s, GetProp("Color", Color.White));
|
||||
s.WriteByte((byte)GetProp("FontSize", 12));
|
||||
string text = GetProp("Content", string.Empty);
|
||||
byte[] utf8 = Encoding.UTF8.GetBytes(text);
|
||||
WriteVarint(s, utf8.Length);
|
||||
s.Write(utf8, 0, utf8.Length);
|
||||
WriteVarint(s, GetProp("MaxWidth", int.MaxValue));
|
||||
break;
|
||||
|
||||
case RenderCommandType.DrawLine:
|
||||
WriteInt16(s, Position.X);
|
||||
WriteInt16(s, Position.Y);
|
||||
Point end = GetProp("EndPosition", Position);
|
||||
WriteInt16(s, end.X);
|
||||
WriteInt16(s, end.Y);
|
||||
WriteColor(s, GetProp("Color", Color.White));
|
||||
break;
|
||||
|
||||
case RenderCommandType.RemovePrimitives:
|
||||
break;
|
||||
|
||||
case RenderCommandType.SetCursor:
|
||||
WriteInt16(s, Position.X);
|
||||
WriteInt16(s, Position.Y);
|
||||
break;
|
||||
|
||||
case RenderCommandType.ScreenInfo:
|
||||
WriteUInt16(s, GetProp("Width", 0));
|
||||
WriteUInt16(s, GetProp("Height", 0));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static RenderCommand FromBytes(byte[] data)
|
||||
{
|
||||
int offset = 0;
|
||||
RenderCommandType type = (RenderCommandType)data[offset++];
|
||||
int windowId = ReadVarint(data, ref offset);
|
||||
int elementId = ReadVarint(data, ref offset);
|
||||
Point pos = default;
|
||||
Dictionary<string, object?> props = [];
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case RenderCommandType.CreateWindow:
|
||||
pos = new Point(ReadInt16(data, ref offset), ReadInt16(data, ref offset));
|
||||
props["Size"] = new Size(ReadUInt16(data, ref offset), ReadUInt16(data, ref offset));
|
||||
props["ZIndex"] = ReadVarint(data, ref offset);
|
||||
break;
|
||||
|
||||
case RenderCommandType.DestroyWindow:
|
||||
break;
|
||||
|
||||
case RenderCommandType.MoveWindow:
|
||||
pos = new Point(ReadInt16(data, ref offset), ReadInt16(data, ref offset));
|
||||
break;
|
||||
|
||||
case RenderCommandType.DrawFilledRect:
|
||||
case RenderCommandType.DrawRectBorder:
|
||||
pos = new Point(ReadInt16(data, ref offset), ReadInt16(data, ref offset));
|
||||
props["Size"] = new Size(ReadUInt16(data, ref offset), ReadUInt16(data, ref offset));
|
||||
props["Color"] = ReadColor(data, ref offset);
|
||||
break;
|
||||
|
||||
case RenderCommandType.DrawFilledCircle:
|
||||
case RenderCommandType.DrawCircle:
|
||||
pos = new Point(ReadInt16(data, ref offset), ReadInt16(data, ref offset));
|
||||
props["Radius"] = ReadUInt16(data, ref offset);
|
||||
props["Color"] = ReadColor(data, ref offset);
|
||||
break;
|
||||
|
||||
case RenderCommandType.DrawPoint:
|
||||
pos = new Point(ReadInt16(data, ref offset), ReadInt16(data, ref offset));
|
||||
props["Color"] = ReadColor(data, ref offset);
|
||||
break;
|
||||
|
||||
case RenderCommandType.DrawText:
|
||||
pos = new Point(ReadInt16(data, ref offset), ReadInt16(data, ref offset));
|
||||
props["Color"] = ReadColor(data, ref offset);
|
||||
props["FontSize"] = data[offset++];
|
||||
int textLen = ReadVarint(data, ref offset);
|
||||
props["Content"] = Encoding.UTF8.GetString(data, offset, textLen);
|
||||
offset += textLen;
|
||||
props["MaxWidth"] = ReadVarint(data, ref offset);
|
||||
break;
|
||||
|
||||
case RenderCommandType.DrawLine:
|
||||
pos = new Point(ReadInt16(data, ref offset), ReadInt16(data, ref offset));
|
||||
props["EndPosition"] = new Point(ReadInt16(data, ref offset), ReadInt16(data, ref offset));
|
||||
props["Color"] = ReadColor(data, ref offset);
|
||||
break;
|
||||
|
||||
case RenderCommandType.RemovePrimitives:
|
||||
break;
|
||||
|
||||
case RenderCommandType.SetCursor:
|
||||
pos = new Point(ReadInt16(data, ref offset), ReadInt16(data, ref offset));
|
||||
break;
|
||||
|
||||
case RenderCommandType.ScreenInfo:
|
||||
props["Width"] = ReadUInt16(data, ref offset);
|
||||
props["Height"] = ReadUInt16(data, ref offset);
|
||||
break;
|
||||
}
|
||||
|
||||
return new RenderCommand
|
||||
{
|
||||
Type = type,
|
||||
WindowId = windowId,
|
||||
ElementId = elementId,
|
||||
Position = pos,
|
||||
Properties = props,
|
||||
};
|
||||
}
|
||||
|
||||
// --- Serialization helpers ---
|
||||
|
||||
private T GetProp<T>(string key, T fallback)
|
||||
{
|
||||
return Properties.TryGetValue(key, out object? raw) && raw is T val ? val : fallback;
|
||||
}
|
||||
|
||||
private static void WriteVarint(Stream s, int value)
|
||||
{
|
||||
uint v = (uint)value;
|
||||
while (v >= 0x80)
|
||||
{
|
||||
s.WriteByte((byte)(v | 0x80));
|
||||
v >>= 7;
|
||||
}
|
||||
s.WriteByte((byte)v);
|
||||
}
|
||||
|
||||
private static void WriteInt16(Stream s, int value)
|
||||
{
|
||||
s.WriteByte((byte)(value & 0xFF));
|
||||
s.WriteByte((byte)((value >> 8) & 0xFF));
|
||||
}
|
||||
|
||||
private static void WriteUInt16(Stream s, int value)
|
||||
{
|
||||
s.WriteByte((byte)(value & 0xFF));
|
||||
s.WriteByte((byte)((value >> 8) & 0xFF));
|
||||
}
|
||||
|
||||
private static void WriteColor(Stream s, Color c)
|
||||
{
|
||||
s.WriteByte(c.R);
|
||||
s.WriteByte(c.G);
|
||||
s.WriteByte(c.B);
|
||||
}
|
||||
|
||||
private static int ReadVarint(byte[] data, ref int offset)
|
||||
{
|
||||
uint result = 0;
|
||||
int shift = 0;
|
||||
while (true)
|
||||
{
|
||||
byte b = data[offset++];
|
||||
result |= (uint)(b & 0x7F) << shift;
|
||||
if ((b & 0x80) == 0)
|
||||
{
|
||||
return (int)result;
|
||||
}
|
||||
|
||||
shift += 7;
|
||||
}
|
||||
}
|
||||
|
||||
private static int ReadInt16(byte[] data, ref int offset)
|
||||
{
|
||||
int lo = data[offset++];
|
||||
int hi = data[offset++];
|
||||
return (short)(lo | (hi << 8));
|
||||
}
|
||||
|
||||
private static int ReadUInt16(byte[] data, ref int offset)
|
||||
{
|
||||
int lo = data[offset++];
|
||||
int hi = data[offset++];
|
||||
return lo | (hi << 8);
|
||||
}
|
||||
|
||||
private static Color ReadColor(byte[] data, ref int offset)
|
||||
{
|
||||
byte r = data[offset++];
|
||||
byte g = data[offset++];
|
||||
byte b = data[offset++];
|
||||
return Color.FromArgb(r, g, b);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
namespace RemSox.Shared.UI.GUI.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// <para>Render command opcodes, grouped by category:</para>
|
||||
/// <para>
|
||||
/// 0x01–0x0F System/setup<br/>
|
||||
/// 0x10–0x1F Window lifecycle<br/>
|
||||
/// 0x20–0x2F Primitives lifecycle<br/>
|
||||
/// 0x30–0x3F Primitives draw<br/>
|
||||
/// 0x40+ Future expansion
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public enum RenderCommandType : byte
|
||||
{
|
||||
// System / setup (0x01–0x0F)
|
||||
ScreenInfo = 0x01,
|
||||
SetCursor = 0x02,
|
||||
|
||||
// Window lifecycle (0x10–0x1F)
|
||||
CreateWindow = 0x10,
|
||||
DestroyWindow = 0x11,
|
||||
MoveWindow = 0x12,
|
||||
|
||||
// Primitives lifecycle (0x20–0x2F)
|
||||
RemovePrimitives = 0x20,
|
||||
|
||||
// Primitives draw (0x30–0x3F)
|
||||
DrawFilledRect = 0x30,
|
||||
DrawRectBorder = 0x31,
|
||||
DrawFilledCircle = 0x32,
|
||||
DrawCircle = 0x33,
|
||||
DrawText = 0x34,
|
||||
DrawLine = 0x35,
|
||||
DrawPoint = 0x36,
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
namespace RemSox.Shared.UI;
|
||||
|
||||
public record MouseEvent(MouseEventType Type, int X = 0, int Y = 0, MouseButton Button = MouseButton.None, int Delta = 0)
|
||||
{
|
||||
public static MouseEvent Move(int x, int y)
|
||||
{
|
||||
return new(MouseEventType.Move, x, y);
|
||||
}
|
||||
|
||||
public static MouseEvent ButtonDown(int x, int y, MouseButton button)
|
||||
{
|
||||
return new(MouseEventType.ButtonDown, x, y, button);
|
||||
}
|
||||
|
||||
public static MouseEvent ButtonUp(int x, int y, MouseButton button)
|
||||
{
|
||||
return new(MouseEventType.ButtonUp, x, y, button);
|
||||
}
|
||||
|
||||
public static MouseEvent Wheel(int x, int y, int delta)
|
||||
{
|
||||
return new(MouseEventType.Wheel, x, y, Delta: delta);
|
||||
}
|
||||
}
|
||||
|
||||
public enum MouseEventType
|
||||
{
|
||||
Move,
|
||||
ButtonDown,
|
||||
ButtonUp,
|
||||
Wheel
|
||||
}
|
||||
public enum MouseButton
|
||||
{
|
||||
Left,
|
||||
Right,
|
||||
Middle,
|
||||
None
|
||||
}
|
||||
Reference in New Issue
Block a user