diff --git a/.skills/csharp-best-practices/SKILL.md b/.skills/csharp-best-practices/SKILL.md new file mode 100644 index 00000000..1d99bf30 --- /dev/null +++ b/.skills/csharp-best-practices/SKILL.md @@ -0,0 +1,797 @@ +--- +name: csharp-best-practices +description: > + C# 12 / .NET 8 coding conventions, idiomatic patterns, and performance best practices + for the Minecraft Console Client codebase. Use when writing, reviewing, or modifying C# code. +version: 0.3.0 +--- + +# C# 12 / .NET 8 Best Practices + +Target: **.NET 8**, **C# 12**, nullable enabled. +Sources: [MS C# Conventions](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions) · [.NET Runtime Style](https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/coding-style.md) · [C# 12 Docs](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-12) · [.NET 8 Perf](https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-8/) + +## Naming + +| Element | Style | Example | +|---|---|---| +| Type, method, property, const, enum member | PascalCase | `PacketHandler`, `MaxRetries`, `GameMode.Survival` | +| Interface | `I` + PascalCase | `IChatBot` | +| Private instance field | `_camelCase` | `_handler` | +| Private static field | `s_camelCase` | `s_defaultTimeout` | +| Thread-static field | `t_camelCase` | `t_cachedBuffer` | +| Local, parameter | camelCase | `packetId` | +| Type parameter | `T` + PascalCase | `TResult` | +| Namespace | PascalCase | `MinecraftClient.Protocol` | +| Async methods | Suffix `Async` | `ConnectAsync()`, `ReadPacketAsync()` | + +```csharp +// CORRECT: naming conventions +private readonly Dictionary _entities = new(); +private static readonly TimeSpan s_reconnectDelay = TimeSpan.FromSeconds(5); +public int PacketCount { get; private set; } +public async Task ConnectAsync(CancellationToken ct) { } +``` + +```csharp +// WRONG: naming violations +private Dictionary entities = new(); // missing _ +private static TimeSpan reconnectDelay; // missing s_ +public int packet_count { get; set; } // snake_case +public async Task Connect(CancellationToken ct) { } // missing Async suffix +``` + +## C# 12 Features + +### Primary Constructors + +Use for simple parameter capture. Parameters are `camelCase`, mutable — assign to `readonly` fields when immutability matters. + +```csharp +// CORRECT: primary constructor captures dependencies +public class ChatLogger(string logFilePath, bool appendMode) : ChatBot +{ + private readonly StreamWriter _writer = new(logFilePath, appendMode); + public override void GetText(string text) => _writer.WriteLine(text); +} +``` + +```csharp +// WRONG: verbose constructor boilerplate for simple capture +public class ChatLogger : ChatBot +{ + private readonly StreamWriter _writer; + public ChatLogger(string logFilePath, bool appendMode) + { + _writer = new StreamWriter(logFilePath, appendMode); + } + public override void GetText(string text) => _writer.WriteLine(text); +} +``` + +### Collection Expressions + +Use `[...]` and `..` spread for arrays, lists, spans. + +```csharp +// CORRECT: collection expressions (C# 12) +int[] ids = [1, 2, 3]; +List names = ["Steve", "Alex"]; +ReadOnlySpan header = [0xFE, 0x01]; // no heap alloc +int[] combined = [..firstArray, ..secondArray, 42]; +IReadOnlyList empty = []; +``` + +```csharp +// WRONG: verbose initialization +int[] ids = new int[] { 1, 2, 3 }; +var names = new List { "Steve", "Alex" }; +ReadOnlySpan header = new byte[] { 0xFE, 0x01 }; // allocates +var combined = firstArray.Concat(secondArray).Append(42).ToArray(); +``` + +### Type Aliases + +```csharp +// CORRECT: alias complex types for readability +using Coordinate = (int X, int Y, int Z); +using PacketMap = System.Collections.Generic.Dictionary>; +``` + +### Default Lambda Parameters + +```csharp +// CORRECT: C# 12 +var greet = (string name, string prefix = "Player") => $"{prefix} {name}"; +``` + +## Modern Syntax (C# 10–12) + +### File-Scoped Namespaces + +```csharp +// CORRECT: file-scoped namespace — one per file, less nesting +namespace MinecraftClient.ChatBots; + +public class MyBot : ChatBot { } +``` + +```csharp +// WRONG: block-scoped namespace adds unnecessary nesting +namespace MinecraftClient.ChatBots +{ + public class MyBot : ChatBot { } +} +``` + +### Target-Typed `new` + +Use when the type is obvious from the left-hand side. + +```csharp +// CORRECT: target-typed new +private readonly Dictionary _scores = new(); +List entities = new(capacity: 256); +``` + +```csharp +// WRONG: redundant type name +private readonly Dictionary _scores = new Dictionary(); +``` + +### Pattern Matching + +Prefer patterns over type-casting chains and complex boolean logic. + +```csharp +// CORRECT: is-pattern with declaration and property patterns +if (entity is Player { Health: > 0 } player) + SendMessage($"{player.Name} is alive"); +``` + +```csharp +// WRONG: manual cast and multi-step check +if (entity is Player) +{ + var player = (Player)entity; + if (player.Health > 0) + SendMessage($"{player.Name} is alive"); +} +``` + +```csharp +// CORRECT: switch expression +public string GetStatusLabel(GameMode mode) => mode switch +{ + GameMode.Survival => "Survival", + GameMode.Creative => "Creative", + GameMode.Adventure => "Adventure", + GameMode.Spectator => "Spectator", + _ => throw new ArgumentOutOfRangeException(nameof(mode)) +}; +``` + +```csharp +// WRONG: switch statement with returns +public string GetStatusLabel(GameMode mode) +{ + switch (mode) + { + case GameMode.Survival: return "Survival"; + case GameMode.Creative: return "Creative"; + default: throw new ArgumentOutOfRangeException(nameof(mode)); + } +} +``` + +```csharp +// CORRECT: property patterns for compound conditions +if (response is { StatusCode: >= 200 and < 300, Content.Length: > 0 }) + ProcessResponse(response); +``` + +```csharp +// WRONG: multiple chained conditions +if (response != null && response.StatusCode >= 200 + && response.StatusCode < 300 && response.Content != null + && response.Content.Length > 0) + ProcessResponse(response); +``` + +```csharp +// CORRECT: relational, logical, and list patterns +if (health is > 0 and <= 6) LogToConsole("Low health!"); +if (args is [var command, var target, ..]) ProcessCommand(command, target); +``` + +### Raw String Literals + +Use for JSON, regex, multi-line strings. + +```csharp +// CORRECT: raw string literal +string json = """ + { "username": "Steve", "action": "connect" } + """; +string pattern = """<\w+>"""; +``` + +```csharp +// WRONG: escaped quotes +string json = "{ \"username\": \"Steve\", \"action\": \"connect\" }"; +``` + +### Records + +Use `record` for immutable data carriers and DTOs. Use `record struct` for small value types. + +```csharp +// CORRECT: record for data carrier +public record PlayerInfo(string Name, Guid Uuid, GameMode Mode); +public record struct ChunkCoord(int X, int Z); +var updated = info with { Mode = GameMode.Creative }; +``` + +```csharp +// WRONG: full class for a simple data carrier +public class PlayerInfo +{ + public string Name { get; set; } = string.Empty; + public Guid Uuid { get; set; } + public GameMode Mode { get; set; } +} +``` + +```csharp +// CORRECT: compact constructor for record validation +public record OrderItem(string ProductId, int Quantity, decimal UnitPrice) +{ + public OrderItem + { + ArgumentException.ThrowIfNullOrWhiteSpace(ProductId); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(Quantity); + ArgumentOutOfRangeException.ThrowIfNegative(UnitPrice); + } +} +``` + +### Required Members + +```csharp +// CORRECT: required + init enforces initialization without constructor boilerplate +public class ServerConfig +{ + public required string Host { get; init; } + public required int Port { get; init; } + public string? Password { get; init; } +} +var config = new ServerConfig { Host = "mc.example.com", Port = 25565 }; +``` + +## Nullable Reference Types + +Project has nullable enabled. Follow these rules: + +```csharp +// CORRECT: guard at API boundaries with .NET 8 throw helpers +public void Connect(string host, IProtocolHandler handler) +{ + ArgumentNullException.ThrowIfNull(handler); + ArgumentException.ThrowIfNullOrEmpty(host); +} +``` + +```csharp +// WRONG: manual null checks +if (handler == null) throw new ArgumentNullException(nameof(handler)); +if (string.IsNullOrWhiteSpace(host)) + throw new ArgumentException("Host is required", nameof(host)); +``` + +```csharp +// CORRECT: 'is not null' pattern +if (currentPlayer is not null) + currentPlayer.Update(); +``` + +```csharp +// WRONG: comparison operator for null check +if (currentPlayer != null) + currentPlayer.Update(); +``` + +```csharp +// CORRECT: explicit nullable handling +public Entity? FindEntity(int id) +{ + return _entities.TryGetValue(id, out var entity) ? entity : null; +} + +// CORRECT: null-coalescing / null-conditional +string name = player?.CustomName ?? player?.Name ?? "Unknown"; + +// CORRECT: null-forgiving only when proven safe (after ThrowIfNull or equivalent) +string val = GetRequiredValue()!; + +// CORRECT: annotate return values +[return: MaybeNull] +public T Find(Predicate match) { } + +[MemberNotNull(nameof(_connection))] +private void EnsureConnected() { } +``` + +```csharp +// WRONG: hiding nullability with null-forgiving +public string GetName(Player? player) +{ + return player!.Name; // hides potential NullReferenceException +} +``` + +## Async / Await + +```csharp +// CORRECT: propagate CancellationToken through every async I/O call +public async Task FetchDataAsync(Uri uri, CancellationToken ct = default) +{ + using var response = await _httpClient.GetAsync(uri, ct); + return await response.Content.ReadAsStringAsync(ct); +} +``` + +```csharp +// WRONG: CancellationToken not passed downstream +public async Task FetchDataAsync(Uri uri) +{ + using var response = await _httpClient.GetAsync(uri, default); + return await response.Content.ReadAsStringAsync(default); +} +``` + +```csharp +// CORRECT: ValueTask when result is often available synchronously +public ValueTask GetCachedCountAsync() +{ + if (_cache.TryGetValue("count", out int count)) + return ValueTask.FromResult(count); + return new ValueTask(LoadCountFromDbAsync()); +} +``` + +```csharp +// WRONG: Task allocates unnecessarily when result is cached +public async Task GetCachedCountAsync() +{ + if (_cache.TryGetValue("count", out int count)) + return count; // allocates a Task + return await LoadCountFromDbAsync(); +} +``` + +```csharp +// CORRECT: async Task for async event handlers +public async Task HandleEventAsync(GameEvent e, CancellationToken ct) +{ + await notificationService.SendAsync(e.PlayerId, ct); +} +``` + +```csharp +// WRONG: async void — exceptions are unobservable, cannot be awaited +public async void HandleEvent(GameEvent e) +{ + await notificationService.SendAsync(e.PlayerId, default); +} +``` + +```csharp +// CORRECT: await the result +var packet = await reader.ReadPacketAsync(ct); +``` + +```csharp +// WRONG: .Result / .Wait() causes deadlocks +var packet = reader.ReadPacketAsync(ct).Result; +var packet2 = reader.ReadPacketAsync(ct).GetAwaiter().GetResult(); +``` + +```csharp +// CORRECT: ConfigureAwait(false) in library code +var data = await stream.ReadAsync(buffer, ct).ConfigureAwait(false); + +// CORRECT: IAsyncEnumerable for streaming +public async IAsyncEnumerable ReadChatStreamAsync( + [EnumeratorCancellation] CancellationToken ct = default) +{ + while (!ct.IsCancellationRequested) + yield return await _reader.ReadNextAsync(ct); +} + +// CORRECT: await using for async disposal +await using var conn = new McConnection(host, port); +``` + +## LINQ + +### Prefer Method Syntax for Most Operations + +```csharp +// CORRECT: method syntax for common operations +var onlinePlayers = players + .Where(p => p.IsOnline) + .OrderBy(p => p.Name) + .Select(p => new PlayerListItem(p.Id, p.Name)) + .ToList(); +``` + +```csharp +// AVOID: query syntax for simple operations +var onlinePlayers = ( + from p in players + where p.IsOnline + orderby p.Name + select new PlayerListItem(p.Id, p.Name) +).ToList(); +``` + +### Use Query Syntax for Joins + +```csharp +// CORRECT: query syntax makes joins readable +var results = + from entity in entities + join player in players on entity.OwnerId equals player.Id + where entity.Health > 0 + select new { entity.Name, player.Name }; +``` + +```csharp +// AVOID: method syntax for complex joins is hard to read +var results = entities + .Join(players, + e => e.OwnerId, + p => p.Id, + (e, p) => new { e, p }) + .Where(x => x.e.Health > 0) + .Select(x => new { x.e.Name, PlayerName = x.p.Name }); +``` + +### Materialize to Avoid Multiple Enumeration + +```csharp +// CORRECT: materialize once, iterate many times +var online = players.Where(p => p.IsOnline).ToList(); +Console.WriteLine(online.Count); +foreach (var p in online) { } +``` + +```csharp +// WRONG: enumerates the query twice +var filtered = players.Where(p => p.IsOnline); +Console.WriteLine(filtered.Count()); // first enumeration +foreach (var p in filtered) { } // second enumeration +``` + +### Use Any() Over Count() > 0 + +```csharp +// CORRECT: short-circuits on first match +if (entities.Any(e => e.IsHostile)) + TriggerAlert(); +``` + +```csharp +// WRONG: counts the entire collection +if (entities.Count(e => e.IsHostile) > 0) + TriggerAlert(); +``` + +### Prefer FirstOrDefault with Null Handling + +```csharp +// CORRECT: explicit null handling +var target = players.FirstOrDefault(p => p.Name == name) + ?? throw new InvalidOperationException($"Player '{name}' not found"); +``` + +### TryGetNonEnumeratedCount + +```csharp +// CORRECT: avoid full enumeration just to get count (.NET 6+) +if (source.TryGetNonEnumeratedCount(out int count)) + buffer = new Entity[count]; +``` + +### Avoid LINQ in Hot Paths + +```csharp +// CORRECT: manual loop with Span in performance-critical code +Span data = stackalloc byte[256]; +int found = 0; +for (int i = 0; i < data.Length; i++) + if (data[i] == target) found++; +``` + +```csharp +// AVOID: LINQ allocates enumerators and delegates on hot paths +int found = data.ToArray().Count(b => b == target); +``` + +## Performance (.NET 8) + +### Span\ / Memory\ + +```csharp +// CORRECT: zero-allocation slicing +ReadOnlySpan command = input.AsSpan()[1..]; // skip '/' + +// CORRECT: stack-allocated parsing +public static int ParseVarInt(ReadOnlySpan data, out int bytesRead) +{ + int result = 0; bytesRead = 0; byte cur; + do { cur = data[bytesRead]; result |= (cur & 0x7F) << (bytesRead * 7); bytesRead++; } + while ((cur & 0x80) != 0); + return result; +} +``` + +### FrozenDictionary / FrozenSet (.NET 8) + +Build once, read many — ~50% faster lookups than Dictionary. + +```csharp +// CORRECT: FrozenDictionary for read-heavy lookup tables (palettes, protocol maps) +using System.Collections.Frozen; +private static readonly FrozenDictionary s_blockNames = + new Dictionary { [0] = "air", [1] = "stone" }.ToFrozenDictionary(); +``` + +### SearchValues\ (.NET 8) + +Hardware-accelerated set search. + +```csharp +// CORRECT: precompute once, scan with SIMD +private static readonly SearchValues s_separators = SearchValues.Create(" \t\n\r,;"); +int idx = input.AsSpan().IndexOfAny(s_separators); +``` + +### CompositeFormat (.NET 8) + +Parse format string once, reuse. + +```csharp +// CORRECT: avoids re-parsing the format string each call +private static readonly CompositeFormat s_logFmt = CompositeFormat.Parse("[{0:HH:mm:ss}] {1}: {2}"); +string msg = string.Format(CultureInfo.InvariantCulture, s_logFmt, DateTime.Now, player, text); +``` + +### ArrayPool / stackalloc + +```csharp +// CORRECT: rent from pool for temporary buffers +byte[] buf = ArrayPool.Shared.Rent(4096); +try { int n = stream.Read(buf.AsSpan(0, 4096)); ProcessPacket(buf.AsSpan(0, n)); } +finally { ArrayPool.Shared.Return(buf); } + +// CORRECT: stackalloc for small, fixed-size buffers (< 512 bytes) +Span header = stackalloc byte[5]; +``` + +## String Handling + +```csharp +// CORRECT: explicit StringComparison — always +bool match = name.Equals("Steve", StringComparison.OrdinalIgnoreCase); +int idx = text.IndexOf("hello", StringComparison.Ordinal); +``` + +```csharp +// WRONG: allocates a lowered copy +bool match = name.ToLower() == "steve"; +``` + +```csharp +// CORRECT: string.Create for perf-critical formatting +string hex = string.Create(data.Length * 2, data, static (span, bytes) => +{ + for (int i = 0; i < bytes.Length; i++) + bytes[i].TryFormat(span[(i * 2)..], out _, "X2"); +}); + +// CORRECT: StringBuilder for loops +var sb = new StringBuilder(256); +foreach (var item in inventory) + sb.Append(item.Name).Append(" x").Append(item.Count).AppendLine(); +``` + +```csharp +// WRONG: O(n²) string concatenation in loop +string combined = ""; +foreach (var s in items) combined += s + ", "; +``` + +## Collections — Choosing the Right Type + +| Scenario | Type | Notes | +|---|---|---| +| General key-value | `Dictionary` | O(1) lookup | +| Build once, read many | `FrozenDictionary` | .NET 8; faster reads | +| Thread-safe | `ConcurrentDictionary` | Lock-free reads | +| Immutable snapshots | `ImmutableDictionary` | Persistent structure | +| Membership test | `HashSet` / `FrozenSet` | FrozenSet for static | +| Priority queue | `PriorityQueue` | .NET 6+ | +| Producer-consumer | `Channel` | Over `BlockingCollection` | +| Temp buffer | `ArrayPool` / `stackalloc` | Zero/low alloc | + +## Error Handling + +```csharp +// CORRECT: Try* pattern for expected failures +if (int.TryParse(input, out int value)) ProcessValue(value); +if (_registry.TryGetValue(packetId, out var handler)) handler.Invoke(data); +``` + +```csharp +// WRONG: using exceptions for control flow +try { return dict[key]; } +catch (KeyNotFoundException) { return null; } // use TryGetValue +``` + +```csharp +// CORRECT: exception filters (catch-when) +try { await ConnectAsync(ct); } +catch (SocketException ex) when (ex.SocketErrorCode == SocketError.ConnectionRefused) +{ + LogToConsole("Connection refused, retrying..."); +} +``` + +```csharp +// CORRECT: throw helpers (smaller IL, better inlining) +ArgumentNullException.ThrowIfNull(handler); +ArgumentOutOfRangeException.ThrowIfNegative(timeout); +ArgumentOutOfRangeException.ThrowIfGreaterThan(timeout, MaxTimeout); +ObjectDisposedException.ThrowIf(_disposed, this); +``` + +```csharp +// WRONG: generic exceptions +throw new Exception($"Entity {id} not found"); +``` + +```csharp +// CORRECT: specific, meaningful exception types +throw new EntityNotFoundException(id); +// or use null-coalescing with throw +return await FindEntityAsync(id, ct) + ?? throw new EntityNotFoundException(id); +``` + +```csharp +// AVOID: catching Exception without filtering +try { DoWork(); } +catch (Exception) { /* swallowed */ } +``` + +## Warning Suppression + +```csharp +// CORRECT: fix the warning by handling null properly +public string GetDisplayName(Player? player) +{ + return player?.DisplayName ?? "Unknown"; +} +``` + +```csharp +// WRONG: suppressing nullable warning with pragma +#pragma warning disable CS8602 +public string GetDisplayName(Player? player) +{ + return player.DisplayName; // NullReferenceException at runtime +} +#pragma warning restore CS8602 +``` + +```csharp +// WRONG: suppressing with attribute +[SuppressMessage("Usage", "CA1062:Validate arguments of public methods")] +public void Process(Packet packet) +{ + // missing null check +} +``` + +Project-wide `.editorconfig` is the only acceptable place for warning policy: + +```text +# .editorconfig - project-wide policy decisions only +dotnet_diagnostic.CA2007.severity = none +``` + +## Resource Management + +```csharp +// CORRECT: using declaration — disposed at end of scope +using var stream = new FileStream(path, FileMode.Open); +using var reader = new StreamReader(stream); + +// CORRECT: IAsyncDisposable +await using var conn = await CreateConnectionAsync(); +``` + +```csharp +// CORRECT: Dispose pattern +public class PacketReader : IDisposable +{ + private Stream? _stream; + private bool _disposed; + public void Dispose() + { + if (_disposed) return; + _stream?.Dispose(); _stream = null; _disposed = true; + } +} +``` + +## Security + +```csharp +// CORRECT: secure random for tokens +byte[] token = RandomNumberGenerator.GetBytes(32); + +// CORRECT: constant-time comparison for secrets +bool valid = CryptographicOperations.FixedTimeEquals(expected, actual); + +// CORRECT: validate external input +if (Uri.TryCreate(userInput, UriKind.Absolute, out var uri) + && uri.Scheme is "http" or "https") + await FetchAsync(uri); +``` + +```csharp +// WRONG: predictable random for security-sensitive values +var rng = new Random(); + +// WRONG: timing side-channel on secret comparison +bool eq = secret1.SequenceEqual(secret2); +``` + +## Miscellaneous Idioms + +```csharp +// CORRECT: var when type is obvious from RHS +var entities = new Dictionary(); +var timer = Stopwatch.StartNew(); + +// CORRECT: explicit type when var would be unclear +Stream responseStream = GetResponse(); +int count = items.Count; + +// CORRECT: expression-bodied members for one-liners +public override string ToString() => $"[{X}, {Y}, {Z}]"; +public bool IsAlive => Health > 0; + +// CORRECT: discards for unused values +_ = int.TryParse(s, out int result); +(_, int y, _) = GetCoordinates(); + +// CORRECT: nameof for resilient refactoring +throw new ArgumentException("Invalid value", nameof(packetId)); +LogToConsole($"{nameof(AutoEat)}: eating {item.Name}"); + +// CORRECT: static lambdas prevent accidental closure allocations +list.Sort(static (a, b) => a.Id.CompareTo(b.Id)); + +// CORRECT: index/range operators +var last = items[^1]; +var slice = data[3..^1]; + +// CORRECT: tuple deconstruction +var (x, y, z) = GetPosition(); + +// CORRECT: string interpolation with alignment and format specifiers +LogToConsole($"Health: {health,6:F1} | Hunger: {hunger,6:F1}"); +``` diff --git a/.skills/mcc-dev-workflow/SKILL.md b/.skills/mcc-dev-workflow/SKILL.md index bc721633..f0c3c5b9 100644 --- a/.skills/mcc-dev-workflow/SKILL.md +++ b/.skills/mcc-dev-workflow/SKILL.md @@ -1,3 +1,8 @@ +--- +name: mcc-development-workflow +description: Documentation of the typical development workflow for Minecraft Console Client (MCC), including project structure, build commands, and debugging steps. +--- + # MCC Development Workflow ## Project Overview diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..185fa96d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,95 @@ +# AGENTS.md + +## Project +- Minecraft Console Client (MCC) is a cross-platform text/TUI client for Minecraft Java Edition. +- Primary scope: connect to servers, send chat and commands, receive text, automate gameplay/admin tasks, and extend behavior through built-in bots or runtime C# scripts. +- Secondary scope: protocol/version adaptation tooling, docs site, legacy GUI wrapper, and debug tooling. + +## Build / Run +- Init submodules first: `git submodule update --init --recursive` +- Build: `dotnet build MinecraftClient.sln -c Release` +- Publish (matches CI shape): `dotnet publish MinecraftClient.sln -f net8.0 -r --self-contained=true -c Release -p:UseAppHost=true -p:IncludeNativeLibrariesForSelfExtract=true -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded` +- Run from source: `dotnet run --project MinecraftClient -- --help` +- Docs: `cd docs && npm install && npm run docs:dev` or `npm run docs:build` +- Docker: `cd Docker && docker build -t minecraft-console-client:latest .` +- Tests: no dedicated test project is present in the main solution. +- Current state: the solution builds after submodule init, but `dotnet build` emits many analyzer and NuGet vulnerability warnings; treat them as real. + +## Architecture +- `Program` bootstraps console I/O, TOML config, auth/session state, MC version selection, Forge detection, then creates `McClient`. +- `McClient` is the live session runtime: TCP client, selected protocol handler, Brigadier command dispatcher, loaded bots, world/inventory/entity state, queued chat, movement/pathing, reconnect flow. +- `Protocol/` is the network/auth boundary. `ProtocolHandler` maps Minecraft versions to protocol numbers and selects either `Protocol16Handler` (1.4.6-1.6.4) or `Protocol18Handler` (1.7.2+). +- `Scripting/ChatBot` is the extension boundary. Built-in bots and `/script` C# bots share the same event/tick API. +- Main runtime flow: console input -> internal Brigadier command or server chat; packets -> protocol handler -> `McClient` state update -> bot events; `OnUpdate()` (~10 Hz) drives bot ticks, delayed work, chat cooldowns, movement, and main-thread tasks. + +## Technology Stack +- Main app: C#, .NET 8, nullable enabled. +- Command system: `Brigadier.NET`. +- Config: TOML via `Samboy063.Tomlet`. +- Runtime scripting: Roslyn (`Microsoft.CodeAnalysis.CSharp`) with in-memory compilation. +- Networking/auth: custom Minecraft protocol handlers, DNS SRV lookup (`DnsClient`), Forge/session/profile-key support. +- Integrations: `DSharpPlus`, `Telegram.Bot`, `MessagePack`, `Magick.NET`, `Sentry`. +- Docs site: VuePress 2 (`docs/package.json`). +- Tooling: Docker, GitHub Actions, Python 3.10+ scripts under `tools/` for palette/version generation. +- Legacy UI: `MinecraftClientGUI` is a separate .NET Framework 4.0 WinForms wrapper, not the main runtime. + +## Version Support +Feature columns mean: +- Inventory: `/inventory` plus inventory/container bot APIs +- Movement: terrain handling, `/move`, and movement/pathing bots +- Entity: entity tracking and entity-driven bot events + +| Minecraft | Protocol path | Inventory | Movement | Entity | Notes | +| --- | --- | --- | --- | --- | --- | +| 1.4.6-1.6.4 | `Protocol16Handler` | No | No | No | Core login/chat only | +| 1.7.2-1.7.10 | `Protocol18Handler` | No | Yes | No | Pre-1.8 special case | +| 1.8-1.9.4 | `Protocol18Handler` | Partial / docs conflict | Yes | Yes | Runtime gates allow 1.8+, but docs still warn inventory is unsupported through 1.9 | +| 1.10-1.12.2 | `Protocol18Handler` | Yes | Yes | Yes | Pre-flattening palettes | +| 1.13-1.19.2 | `Protocol18Handler` | Yes | Yes | Yes | Flattened block/item/entity palettes | +| 1.19.3-1.20.4 | `Protocol18Handler` | Yes | Yes | Yes | Newer chat/signing and palette splits | +| 1.20.6-1.21.4 | `Protocol18Handler` | Yes | Yes | Yes | Registry-driven world/attribute handling | +| 1.21.5-1.21.8 | `Protocol18Handler` | Yes | Yes | Yes | 1.21.7/1.21.8 reuse 1.21.6 block/entity palettes in code | +| 1.21.9-1.21.10 | `Protocol18Handler` | Yes | Yes | Yes | Latest coded support; version tools prefer server data reports since 1.21.9 | + +Notes: +- Declared code range is `1.4.6` to `1.21.10`. +- Human docs are stale in places and sometimes stop at older ranges; prefer code when docs and code disagree. +- Movement/pathing limits called out in docs still apply: no swimming, no jumping, no knockback, slab support is partial. + +## Module Map +- `MinecraftClient/`: main `net8.0` runtime project. +- `MinecraftClient/Protocol/`: protocol selection, auth/session flows, packet I/O, Forge/profile-key support. +- `MinecraftClient/Mapping/`: world state, movement/pathfinding, block/entity/material palettes. +- `MinecraftClient/Inventory/`: containers, items, enchantments, inventory helpers, item palettes. +- `MinecraftClient/Commands/` and `MinecraftClient/CommandHandler/`: internal MCC commands plus Brigadier argument types/patches. +- `MinecraftClient/ChatBots/`: built-in automation bots, bridges, script scheduler, replay/map/item helpers. +- `MinecraftClient/Scripting/`: `ChatBot` API, runtime C# compilation, movement lock helpers. +- `MinecraftClient/config/`: sample scripts and example bots; excluded from compilation. +- `ConsoleInteractive/`: required git submodule for richer console input/output. +- `docs/`: VuePress documentation site. +- `tools/`: Python scripts for version adaptation and palette generation. +- `DebugTools/`: packet/proxy debugging utilities. +- `MinecraftClientGUI/`: legacy Windows GUI wrapper around the console app. + +## Engineering Guidance + +### DO +- Keep startup/config/auth logic in `Program` and connection runtime logic in `McClient` or `Protocol/*`. +- Update version support holistically: protocol constants, version mapping, packet palette, block palette, item palette, entity palette, metadata palette, and routing switches. +- Use `tools/` and authoritative server data reports when adapting to new Minecraft versions, especially 1.21.9+. +- Guard optional subsystems with `GetTerrainEnabled()`, `GetInventoryEnabled()`, and `GetEntityHandlingEnabled()` before using them. +- For built-in bots, wire all pieces together: bot class, `Settings.ChatBotConfigHealper`, and `McClient.RegisterBots()`. +- Keep `Initialize()` for setup/prereq checks and `AfterGameJoined()` for sending chat or commands. +- Normalize inbound chat with `GetVerbatim()` before `IsChatMessage()` / `IsPrivateMessage()`. +- Clean up commands, plugin channels, threads, timers, and movement locks in `OnUnload()`. +- Prefer nullable-aware code, pattern matching, `ArgumentNullException.ThrowIfNull`, `Try*` APIs for expected failures, and `InvokeOnMainThread()` for cross-thread state changes. +- Use modern C# only when it fits the current target: the repo builds as `net8.0` with default language version. + +### DON'T +- Don't update only `MCVer2ProtocolVersion()` or only one palette file when adding a new Minecraft version. +- Don't send chat in `Initialize()`. +- Don't mutate inventory snapshots and expect server-side effects; use handler APIs/window actions. +- Don't bypass Brigadier with ad hoc command parsing. +- Don't start background workers when `Update()` or delayed tasks are sufficient; if you must, stop them on unload/disconnect. +- Don't leave movement locks, plugin channels, or dispatcher registrations behind. +- Don't trust older docs over current code for supported versions or feature gates. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..23562a5f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +Read @AGENTS.md \ No newline at end of file