Initial commit: Brass & Sigil monorepo
Self-hosted Minecraft modpack distribution + administration system.
- launcher/ Avalonia 12 desktop client; single-file win-x64 publish.
Microsoft auth via XboxAuthNet, manifest+SHA-1 mod sync,
portable install path, sidecar settings.
- server/ brass-sigil-server daemon (.NET 8, linux-x64). Wraps the
MC subprocess, embedded Kestrel admin panel with cookie
auth + rate limiting, RCON bridge, scheduled backups,
BlueMap CLI integration with player markers + skin proxy,
friend-side whitelist request flow, world wipe with seed
selection (keep current / random / custom).
- pack/ pack.lock.json (Modrinth + manual CurseForge entries),
data-only tweak source under tweaks/, build outputs in
overrides/ (gitignored).
- scripts/ Build-Pack / Build-Tweaks / Update-Pack / Check-Updates
plus Deploy-Brass.ps1 unified one-shot deploy with
version-bump pre-flight and daemon-state detection.
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
||||
namespace BrassAndSigil.Server.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal Minecraft RCON client (Source RCON protocol).
|
||||
/// Used for sending console commands and reading "list" output for player counts.
|
||||
/// </summary>
|
||||
public sealed class RconClient : IDisposable
|
||||
{
|
||||
private const int SERVERDATA_AUTH = 3;
|
||||
private const int SERVERDATA_EXECCOMMAND = 2;
|
||||
private const int SERVERDATA_RESPONSE_VALUE = 0;
|
||||
|
||||
private TcpClient? _tcp;
|
||||
private NetworkStream? _stream;
|
||||
private int _nextRequestId = 1;
|
||||
|
||||
public bool Connected => _tcp?.Connected ?? false;
|
||||
|
||||
public async Task ConnectAsync(string host, int port, string password, CancellationToken ct = default)
|
||||
{
|
||||
_tcp = new TcpClient();
|
||||
await _tcp.ConnectAsync(host, port, ct);
|
||||
_stream = _tcp.GetStream();
|
||||
|
||||
var authId = NextId();
|
||||
await SendPacketAsync(authId, SERVERDATA_AUTH, password, ct);
|
||||
|
||||
// Read auth response. Server sends an empty response value first, then the auth result.
|
||||
var (id1, _, _) = await ReadPacketAsync(ct);
|
||||
if (id1 == -1) throw new InvalidOperationException("RCON auth failed (bad password)");
|
||||
// Auth ok if id matches what we sent
|
||||
}
|
||||
|
||||
public async Task<string> SendCommandAsync(string command, CancellationToken ct = default)
|
||||
{
|
||||
if (_stream == null) throw new InvalidOperationException("Not connected");
|
||||
var id = NextId();
|
||||
await SendPacketAsync(id, SERVERDATA_EXECCOMMAND, command, ct);
|
||||
var (_, _, body) = await ReadPacketAsync(ct);
|
||||
return body;
|
||||
}
|
||||
|
||||
private int NextId() => Interlocked.Increment(ref _nextRequestId);
|
||||
|
||||
private async Task SendPacketAsync(int id, int type, string body, CancellationToken ct)
|
||||
{
|
||||
var bodyBytes = Encoding.UTF8.GetBytes(body);
|
||||
var packetSize = 4 + 4 + bodyBytes.Length + 2; // id + type + body + 2 null bytes
|
||||
var buffer = new byte[4 + packetSize];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(0, 4), packetSize);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(4, 4), id);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(8, 4), type);
|
||||
bodyBytes.CopyTo(buffer.AsSpan(12));
|
||||
// last two bytes already 0 by default
|
||||
await _stream!.WriteAsync(buffer, ct);
|
||||
}
|
||||
|
||||
private async Task<(int Id, int Type, string Body)> ReadPacketAsync(CancellationToken ct)
|
||||
{
|
||||
var sizeBuf = new byte[4];
|
||||
await ReadExactAsync(sizeBuf, ct);
|
||||
var size = BinaryPrimitives.ReadInt32LittleEndian(sizeBuf);
|
||||
if (size < 10 || size > 1024 * 1024) throw new InvalidOperationException($"Bad RCON packet size {size}");
|
||||
|
||||
var pkt = new byte[size];
|
||||
await ReadExactAsync(pkt, ct);
|
||||
var id = BinaryPrimitives.ReadInt32LittleEndian(pkt.AsSpan(0, 4));
|
||||
var type = BinaryPrimitives.ReadInt32LittleEndian(pkt.AsSpan(4, 4));
|
||||
var body = Encoding.UTF8.GetString(pkt, 8, size - 10); // strip 2 trailing nulls
|
||||
return (id, type, body);
|
||||
}
|
||||
|
||||
private async Task ReadExactAsync(byte[] buffer, CancellationToken ct)
|
||||
{
|
||||
var read = 0;
|
||||
while (read < buffer.Length)
|
||||
{
|
||||
var n = await _stream!.ReadAsync(buffer.AsMemory(read), ct);
|
||||
if (n == 0) throw new EndOfStreamException();
|
||||
read += n;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_stream?.Dispose();
|
||||
_tcp?.Dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user