diff --git a/.gitignore b/.gitignore index d50b7434..b00da3f6 100644 --- a/.gitignore +++ b/.gitignore @@ -8,9 +8,15 @@ /Other/ /.vs/ SessionCache.ini -!/.github /packages +# OS-generated files +.DS_Store +Thumbs.db +desktop.ini +ehthumbs.db +._* + ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## @@ -386,9 +392,6 @@ FodyWeavers.xsd !.vscode/extensions.json *.code-workspace -# Cursor files -!.cursor/ - # Local History for Visual Studio Code .history/ @@ -427,6 +430,3 @@ MinecraftOfficial/ /mcc_input.txt /MinecraftClient.ini /MinecraftClient.backup.ini - -# Other -.* diff --git a/.skills/mcc-chatbot-authoring/SKILL.md b/.skills/mcc-chatbot-authoring/SKILL.md new file mode 100644 index 00000000..3934c268 --- /dev/null +++ b/.skills/mcc-chatbot-authoring/SKILL.md @@ -0,0 +1,107 @@ +--- +name: mcc-chatbot-authoring +description: Create, modify, repair, and wire Minecraft Console Client ChatBots and standalone `/script` bots. Use this whenever the user wants an MCC bot, C# script bot, chat or event handlers, periodic automation, movement logic, inventory logic, plugin-channel handling, or asks to fix or port an existing bot; default to standalone `//MCCScript` bots unless the user explicitly asks for a built-in MCC bot or repo wiring. +--- + +# MCC ChatBot Authoring + +Implement MCC chat bots against the bundled MCC authoring reference. Do not invent methods, lifecycle hooks, or registration steps. + +Always read: +- `references/authoring-reference.md` + +Load only as needed: +- `references/pattern-cookbook.md` for concrete standalone examples +- `assets/script-chatbot-template.cs` for the default standalone `/script` path +- `assets/builtin-chatbot-template.cs` only when the user explicitly requests a built-in bot + +If the current workspace contains an MCC checkout, verify final names and signatures against local sources before editing. The skill should still work without those files. +If there is no MCC checkout available, rely on the bundled reference and cookbook as the full source of truth for authoring patterns. + +## Choose the bot type first + +1. Default to a standalone script bot loaded with `/script`. +2. Only choose a built-in bot when the user explicitly asks for a compiled MCC bot, repo wiring, automatic config loading, or changes under the built-in bot system. +3. If the prompt is ambiguous, infer the likely target from commands, requested output files, or phrasing, state the assumption briefly, and proceed. +4. If a user says only "make a bot", do not create a built-in bot. + +## Source priority + +When the local MCC checkout is available, prefer these sources in this order: +1. `MinecraftClient/Scripting/ChatBot.cs` and current files under `MinecraftClient/ChatBots/` +2. the bundled `references/authoring-reference.md` +3. the bundled `references/pattern-cookbook.md` +4. older `MinecraftClient/config/` sample bots only for ideas, not as the default scaffold + +If an older sample conflicts with the current built-in bots, follow the current built-in bots. +If the local checkout is not available, do not block on missing repo files. Use the bundled references directly. + +## Hard rules + +- Only use lifecycle hooks and helpers documented in the bundled reference or verified in the target codebase. +- Do not send chat from `Initialize()`. Use `AfterGameJoined()` once the session can send messages. +- Prefer the current Brigadier command-registration pattern for built-in bots. Do not introduce `ChatBotCommand` unless the surrounding code already uses it. +- For message parsing, normalize with `GetVerbatim(text)` before `IsChatMessage(...)` or `IsPrivateMessage(...)`. +- Clean up everything you register or start: commands, plugin channels, threads, timers, and movement locks. +- If a built-in bot or long-running automation controls movement, follow a movement-lock pattern and release it on every stop path. Do not add `BotMovementLock` to a simple standalone `/script` bot unless the prompt or surrounding code explicitly needs shared movement coordination. +- For built-in bots, follow the host codebase's localization and config-comment conventions instead of scattering hardcoded user-facing text. +- For new code, prefer `Initialize()` over constructors for prerequisite checks and unload decisions. +- In this repo, built-in bot wiring usually means edits in `MinecraftClient/Settings.cs` and `MinecraftClient/McClient.cs` in addition to the bot class. +- For repair tasks, preserve the existing bot type and file layout unless the user explicitly asks for a conversion or restructure. + +## Standalone script bots + +Use the exact MCC metadata format from the bundled reference. +This is the default path for new work. + +The script should usually: +- keep `Initialize()` for cheap setup only +- use `GetText(...)`, `AfterGameJoined()`, and other event hooks for live behavior +- log with `LogToConsole(...)` +- send server chat or commands with `SendText(...)` +- use `PerformInternalCommand(...)` only for MCC internal commands +- add `//using MinecraftClient.Inventory` in metadata when the script uses inventory types explicitly +- reuse the standalone snippets in `references/pattern-cookbook.md` before inventing new scaffolding +- keep load instructions explicit, usually `/script FileName.cs` + +## Built-in bots + +Built-in bots usually need three pieces: +- the bot class itself +- config wiring in the chat-bot config model +- bot registration in the load flow + +If the codebase exposes commands, follow the built-in command and unload pattern from the bundled reference. If it exposes new settings or status text, follow the codebase's localization and config-comment patterns. + +When working in this checkout, built-in bot delivery usually needs: +- a new file under `MinecraftClient/ChatBots/` +- a config property inside `Settings.ChatBotConfigHealper.ChatBotConfig` +- a `BotLoad(new YourBot())` line inside `McClient.RegisterBots(...)` +- literal code snippets or patch hunks for the `Settings.cs` property and the `McClient.cs` registration line, not only prose notes + +## Repair flow + +When the user asks to fix or debug a bot: +- identify whether it is standalone or built-in and keep that shape unless told otherwise +- remove the broken pattern first, then preserve the intended behavior +- check especially for these regressions: `SendText(...)` in `Initialize()`, raw formatted chat parsing, inventory snapshot mutation, missing command unregister, missing plugin-channel unregister, and unreleased movement locks +- reuse the local repo's modern pattern instead of patching around a legacy helper when the helper is no longer current + +## Delivery checklist + +Before finishing, verify: +- the class inherits `ChatBot` +- the chosen overrides exist in the MCC ChatBot API +- standalone script metadata is exact if this is a `/script` bot +- built-in bots are fully wired into config and registration if needed +- all command registrations, background work, and movement locks are released +- files and namespaces match the surrounding codebase + +## Output + +When you implement or modify a bot: +- state whether it is a standalone script bot or built-in bot +- list the files you changed +- mention any required config keys or the MCC command used to load it +- when built-in wiring is involved, show the exact inserted code lines or patch hunks for `Settings.cs` and `McClient.cs` +- call out assumptions briefly if the user did not specify bot type or trigger behavior diff --git a/.skills/mcc-chatbot-authoring/assets/builtin-chatbot-template.cs b/.skills/mcc-chatbot-authoring/assets/builtin-chatbot-template.cs new file mode 100644 index 00000000..086c89e5 --- /dev/null +++ b/.skills/mcc-chatbot-authoring/assets/builtin-chatbot-template.cs @@ -0,0 +1,57 @@ +// Use this template only when the user explicitly requests a built-in MCC bot. + +using MinecraftClient.Scripting; +using Tomlet.Attributes; + +namespace MinecraftClient.ChatBots +{ + public class ExampleBot : ChatBot + { + private const string BotName = "ExampleBot"; + + public static Configs Config = new(); + + [TomlDoNotInlineObject] + public class Configs + { + public bool Enabled = false; + + public void OnSettingUpdate() + { + } + } + + public override void Initialize() + { + LogToConsole(BotName, "Initialized."); + } + + public override void AfterGameJoined() + { + } + + public override void GetText(string text) + { + text = GetVerbatim(text); + + string message = ""; + string username = ""; + + if (IsPrivateMessage(text, ref message, ref username)) + { + } + else if (IsChatMessage(text, ref message, ref username)) + { + } + } + + public override void OnUnload() + { + } + + public override bool OnDisconnect(DisconnectReason reason, string message) + { + return false; + } + } +} diff --git a/.skills/mcc-chatbot-authoring/assets/script-chatbot-template.cs b/.skills/mcc-chatbot-authoring/assets/script-chatbot-template.cs new file mode 100644 index 00000000..72be0cae --- /dev/null +++ b/.skills/mcc-chatbot-authoring/assets/script-chatbot-template.cs @@ -0,0 +1,37 @@ +//MCCScript 1.0 + +MCC.LoadBot(new ExampleScriptBot()); + +//MCCScript Extensions + +public class ExampleScriptBot : ChatBot +{ + public override void Initialize() + { + LogToConsole("ExampleScriptBot initialized."); + } + + public override void AfterGameJoined() + { + // Safe place for startup chat or commands. + } + + public override void GetText(string text) + { + text = GetVerbatim(text); + + string message = ""; + string username = ""; + + if (IsPrivateMessage(text, ref message, ref username)) + { + LogToConsole("PM from " + username + ": " + message); + return; + } + + if (IsChatMessage(text, ref message, ref username)) + { + LogToConsole("Chat from " + username + ": " + message); + } + } +} diff --git a/.skills/mcc-chatbot-authoring/references/authoring-reference.md b/.skills/mcc-chatbot-authoring/references/authoring-reference.md new file mode 100644 index 00000000..e51495ee --- /dev/null +++ b/.skills/mcc-chatbot-authoring/references/authoring-reference.md @@ -0,0 +1,492 @@ +# MCC ChatBot Reference + +Self-contained authoring notes for Minecraft Console Client chat bots. + +## Bot types + +MCC supports two common authoring paths: +- standalone script bots loaded at runtime with `/script` +- built-in bots compiled into the MCC codebase + +Default to a standalone `/script` bot unless the user explicitly asks for a built-in bot or repo wiring. + +## Embedded current patterns + +This skill is intended to work even without an MCC checkout. The patterns below capture the important behavior that would otherwise be borrowed from current repo examples. + +If the local repo is available, you can verify against files such as `TestBot.cs`, `RemoteControl.cs`, `FollowPlayer.cs`, `ItemsCollector.cs`, and `Farmer.cs`. If it is not available, use the embedded patterns here directly. + +### Minimal chat parsing pattern + +Use this as the baseline for public/private chat handling: + +```csharp +public override void GetText(string text) +{ + string message = ""; + string sender = ""; + text = GetVerbatim(text); + + if (IsPrivateMessage(text, ref message, ref sender)) + { + LogToConsole("PM from " + sender + ": " + message); + } + else if (IsChatMessage(text, ref message, ref sender)) + { + LogToConsole("Chat from " + sender + ": " + message); + } +} +``` + +What matters: +- normalize first with `GetVerbatim(text)` +- handle PMs before public chat if both matter +- keep simple chat bots deterministic and small + +### Owner-gated PM control pattern + +Use this when a bot owner should be able to whisper MCC internal commands: + +```csharp +public override void GetText(string text) +{ + text = GetVerbatim(text).Trim(); + string command = ""; + string sender = ""; + + if (IsPrivateMessage(text, ref command, ref sender) + && Settings.Config.Main.Advanced.BotOwners.Contains(sender.ToLowerInvariant())) + { + CmdResult result = new(); + PerformInternalCommand(command, ref result); + SendPrivateMessage(sender, result.ToString()); + } +} +``` + +What matters: +- `PerformInternalCommand(...)` is for MCC commands, not server chat commands +- owner gating should use `Settings.Config.Main.Advanced.BotOwners` +- if `CmdResult` is used in a standalone script, add `//using MinecraftClient.CommandHandler` + +### Periodic work pattern + +Use `Update()` plus a counter or timestamp for simple repeated work: + +```csharp +private int count = 0; + +public override void Update() +{ + count++; + if (count < Settings.DoubleToTick(60)) + return; + + count = 0; + SendText("/list"); +} +``` + +What matters: +- avoid a worker thread for simple periodic loops +- avoid `Thread.Sleep(...)` inside `Update()` +- if sending chat, do it from a join-safe path like `Update()` or `AfterGameJoined()`, not `Initialize()` + +### Built-in Brigadier command pattern + +Use this for built-in command bots: + +```csharp +public override void Initialize() +{ + McClient.dispatcher.Register(l => l.Literal("help") + .Then(l => l.Literal(CommandName) + .Executes(r => OnCommandHelp(r.Source, string.Empty)) + ) + ); + + McClient.dispatcher.Register(l => l.Literal(CommandName) + .Then(l => l.Literal("stop") + .Executes(r => OnCommandStop(r.Source))) + .Then(l => l.Literal("_help") + .Executes(r => OnCommandHelp(r.Source, string.Empty)) + .Redirect(McClient.dispatcher.GetRoot().GetChild("help").GetChild(CommandName))) + ); +} + +public override void OnUnload() +{ + McClient.dispatcher.Unregister(CommandName); + McClient.dispatcher.GetRoot().GetChild("help").RemoveChild(CommandName); +} +``` + +What matters: +- register commands in `Initialize()` +- unregister the command tree in `OnUnload()` +- remove the help child you added in `OnUnload()` +- prefer this over legacy command wrappers for new built-in work + +### Built-in config and wiring pattern + +Use this as the default built-in shape: + +```csharp +public class ExampleBot : ChatBot +{ + public static Configs Config = new(); + + [TomlDoNotInlineObject] + public class Configs + { + public bool Enabled = false; + + public void OnSettingUpdate() + { + } + } +} +``` + +Typical host wiring shape: + +```csharp +[TomlPrecedingComment("$ChatBot.ExampleBot$")] +public ChatBots.ExampleBot.Configs ExampleBot +{ + get { return ChatBots.ExampleBot.Config; } + set { ChatBots.ExampleBot.Config = value; ChatBots.ExampleBot.Config.OnSettingUpdate(); } +} +``` + +```csharp +if (Config.ChatBot.ExampleBot.Enabled) { BotLoad(new ExampleBot()); } +``` + +What matters: +- built-in configurable bots default to `Enabled = false` +- `OnSettingUpdate()` is the place to normalize config values +- built-in delivery is incomplete without both config wiring and load registration + +### Movement gating pattern + +Use this shape when a built-in bot owns movement: + +```csharp +public override void Initialize() +{ + if (!GetEntityHandlingEnabled()) + { + LogToConsole("Entity handling is required."); + UnloadBot(); + return; + } + + if (!GetTerrainEnabled()) + { + LogToConsole("Terrain handling is required."); + UnloadBot(); + return; + } +} +``` + +```csharp +var movementLock = BotMovementLock.Instance; +if (movementLock is { IsLocked: true }) + return; + +movementLock?.Lock("Example Bot"); +``` + +```csharp +public override void OnUnload() +{ + BotMovementLock.Instance?.UnLock("Example Bot"); +} +``` + +What matters: +- guard terrain and entity handling before movement logic +- built-in movement bots should use `BotMovementLock` +- release the lock on every stop path, including unload and disconnect-sensitive flows + +### Dropped-item collector pattern + +Use this as the standalone item-search baseline: + +```csharp +private DateTime nextScan = DateTime.MinValue; + +public override void Update() +{ + var now = DateTime.UtcNow; + if (now < nextScan || ClientIsMoving()) + return; + + nextScan = now.AddSeconds(1); + + var here = GetCurrentLocation(); + var target = GetEntities().Values + .Where(entity => entity.Type == EntityType.Item && entity.Location.Distance(here) <= 15) + .OrderBy(entity => entity.Location.Distance(here)) + .FirstOrDefault(); + + if (target != null) + MoveToLocation(target.Location); +} +``` + +What matters: +- simple standalone collectors do not need a worker thread +- simple standalone collectors also do not need `BotMovementLock` by default +- `GetEntities()` plus distance ordering is the core search pattern + +### Inventory selection pattern + +Use this as the default hotbar-switch pattern: + +```csharp +private bool TrySwitchToItem(ItemType itemType) +{ + var inventory = GetPlayerInventory(); + + var hotbarSlots = inventory.SearchItem(itemType) + .Where(slot => slot >= 36 && slot <= 44) + .ToArray(); + + if (hotbarSlots.Length == 0) + return false; + + ChangeSlot((short)(hotbarSlots[0] - 36)); + return true; +} +``` + +What matters: +- guard with `GetInventoryEnabled()` +- search inventory snapshots, but mutate real server state with helpers like `ChangeSlot(...)` +- do not treat local `Container.Items` mutation as real inventory manipulation + +Use the older config examples only for ideas, not as primary scaffolding. + +## Standalone script format + +A standalone script bot has two parts in this order: +1. metadata block +2. one or more C# classes, with the main bot class inheriting `ChatBot` + +Required metadata rules: +- line 1 must be exactly `//MCCScript 1.0` +- metadata must include `MCC.LoadBot(new BotClassName());` +- metadata ends with `//MCCScript Extensions` +- optional metadata directives use `//using Namespace` and `//dll SomeLibrary.dll` +- do not insert a space after `//` in metadata directives + +Typical runtime flow: +- place the script file beside MCC +- connect to a server +- load it with `/script YourBotFile.cs` + +### Namespace linking for inventory code + +If a standalone script uses inventory-specific types such as `Container`, `ItemType`, `WindowActionType`, or `ItemMovingHelper`, add this metadata import: + +```csharp +//using MinecraftClient.Inventory +``` + +For built-in bots, use a normal C# import: + +```csharp +using MinecraftClient.Inventory; +``` + +## Lifecycle summary + +Common lifecycle hooks: +- `Initialize()` + called once when the bot loads; use it for cheap setup only +- `AfterGameJoined()` + called after the server has been joined successfully, and again after reconnecting; use it when chat can be sent +- `Update()` + called roughly every 100 ms +- `OnUnload()` + called when the bot unloads; release resources here +- `OnDisconnect(DisconnectReason reason, string message)` + called on disconnect; stop background work and clean up reconnect-sensitive state here + +Important rule: +- do not send chat from `Initialize()`; use `AfterGameJoined()` instead +- prefer `Initialize()` over constructors for environment checks and resource setup + +## Common event hooks + +Useful event hooks include: +- `GetText(string text)` +- `GetText(string text, string? json)` +- `OnPlayerJoin(Guid uuid, string name)` +- `OnPlayerLeave(Guid uuid, string? name)` +- `OnEntitySpawn(Entity entity)` +- `OnEntityDespawn(Entity entity)` +- `OnEntityMove(Entity entity)` +- `OnHealthUpdate(float health, int food)` +- `OnMapData(...)` +- `OnInventoryUpdate(int inventoryId)` +- `OnPluginMessage(string channel, byte[] data)` +- `OnNetworkPacket(int packetID, List packetData, bool isLogin, bool isInbound)` + +Only override hooks that actually exist in the target MCC ChatBot API. + +## Common helpers + +Text and messaging helpers: +- `GetVerbatim(text)` strips Minecraft formatting codes +- `IsChatMessage(text, ref message, ref sender)` parses public chat +- `IsPrivateMessage(text, ref message, ref sender)` parses private chat +- `IsValidName(username)` validates a Minecraft username +- `SendText(text)` sends chat or server commands +- `SendPrivateMessage(player, message)` sends a private message +- `PerformInternalCommand(command, ...)` runs an internal MCC command, not a server command +- `LogToConsole(text)` writes a bot-prefixed console message + +Lifecycle and threading helpers: +- `InvokeOnMainThread(...)` +- `ScheduleOnMainThread(...)` +- `ReconnectToTheServer(...)` +- `UnloadBot()` +- `BotLoad(chatBot)` +- `RunScript(filename, ...)` + +World and player-state helpers: +- `GetWorld()` +- `GetEntities()` +- `GetCurrentLocation()` +- `ClientIsMoving()` +- `GetOnlinePlayers()` +- `GetOnlinePlayersWithUUID()` +- `GetServerTPS()` +- `GetProtocolVersion()` + +Movement and inventory helpers: +- `MoveToLocation(...)` +- `LookAtLocation(...)` +- `GetInventoryEnabled()` +- `GetPlayerInventory()` +- `GetInventories()` +- `GetItemMovingHelper(...)` +- `WindowAction(...)` +- `ChangeSlot(...)` +- `GetCurrentSlot()` +- `UseItemInHand()` +- `UseItemInLeftHand()` +- `CloseInventory(...)` +- `DigBlock(...)` +- `InteractEntity(...)` + +## Inventory notes + +Inventory handling is optional in MCC. Check `GetInventoryEnabled()` before relying on inventory state or mutation. + +Important behavior: +- `GetPlayerInventory()` returns a snapshot copy of the player's inventory +- `GetInventories()` returns current container snapshots +- writing to those `Container` objects locally does not update the server +- to actually change inventory state, use `ChangeSlot(...)`, `WindowAction(...)`, `GetItemMovingHelper(...)`, `UseItemInHand()`, or related helpers + +Useful practical facts: +- hotbar selection uses `ChangeSlot(0..8)` +- hotbar slots are commonly `36..44` in inventory slot numbering +- the offhand slot is commonly `45` +- `Container.SearchItem(...)` is the normal way to locate items by type + +Good inventory workflow: +1. guard with `GetInventoryEnabled()` +2. read the current container using `GetPlayerInventory()` +3. locate slots with `SearchItem(...)` or `Items` +4. mutate server state using `ChangeSlot(...)`, `WindowAction(...)`, or `ItemMovingHelper` +5. if needed, react to `OnInventoryUpdate(...)`, `OnInventoryOpen(...)`, or `OnInventoryClose(...)` + +Plugins and channels: +- `RegisterPluginChannel(channel)` +- `UnregisterPluginChannel(channel)` +- `SendPluginChannelMessage(channel, data, ...)` + +## Built-in bot pattern + +A built-in bot usually follows this shape: +- a class that inherits `ChatBot` +- an optional static `Config` field +- a nested `[TomlDoNotInlineObject]` `Configs` class for settings +- an `Enabled = false` setting by default +- `OnSettingUpdate()` to normalize or validate config values + +If the bot is configurable, the host codebase usually also needs: +- config wiring in the chat-bot config model +- load registration so enabled bots are instantiated automatically + +In this MCC checkout, the usual built-in wiring points are: +- `MinecraftClient/Settings.cs` inside `Settings.ChatBotConfigHealper.ChatBotConfig` +- `MinecraftClient/McClient.cs` inside `RegisterBots(...)` + +Match the surrounding `[TomlPrecedingComment(...)]`, property-forwarding, and `BotLoad(new YourBot())` style instead of inventing a different config path. +When presenting built-in wiring, prefer literal code snippets or patch hunks for those two edits so the wiring can be checked directly. + +If the bot adds user-facing settings or messages, follow the host codebase's localization and config-comment conventions instead of scattering hardcoded strings. + +## Command pattern + +For standalone script bots, prefer chat or PM handling in `GetText(...)` unless the user explicitly asks for built-in command registration. + +For built-in commands, prefer the current Brigadier dispatcher pattern: +- register commands in `Initialize()` +- add a help entry if the bot exposes commands +- unregister the command tree in `OnUnload()` +- remove any help child added during registration in `OnUnload()` + +Avoid using legacy command wrappers if the current codebase uses direct dispatcher registration. +In this checkout, treat direct `McClient.dispatcher.Register(...)` usage in current built-in bots as the source of truth. + +## Concurrency and cleanup + +If the bot starts background work: +- stop it in `OnUnload()` +- stop it in `OnDisconnect(...)` +- consider resetting state in `AfterGameJoined()` after relog +- prefer `Update()` plus counters or timestamps over unmanaged threads when the task is simple periodic work + +If the bot controls movement: +- use a movement-lock discipline +- release the lock on every stop path +- avoid fighting other movement bots +- `BotMovementLock` is mainly for built-in bots or shared long-running automation; a simple standalone script that just calls `MoveToLocation(...)` does not need it by default + +When interacting with client state from background logic, use the main-thread helpers when required by the codebase. + +## Practical defaults + +For simple chat bots: +- normalize text with `GetVerbatim(text)` +- inspect private chat first if the bot listens for whispers +- then inspect public chat +- keep response logic small and deterministic + +For long-running automation bots: +- guard prerequisites early, such as entity handling or terrain support +- fail fast with a clear log message if prerequisites are missing +- release all ongoing work cleanly on unload and disconnect + +## Common pitfalls + +- Incorrect metadata line 1 will break standalone script loading. +- Missing `MCC.LoadBot(new BotClassName())` will prevent standalone script registration. +- Sending chat in `Initialize()` is too early. +- Doing prerequisite checks or unloading from the constructor is harder to reason about than using `Initialize()`. +- Parsing raw formatted text without `GetVerbatim()` causes brittle chat matching. +- Inventing methods not present in the MCC ChatBot API leads to dead code. +- Built-in bot work is incomplete if config or registration wiring is missing. +- Command bots are incomplete if they register commands but do not unregister them. +- `RegisterChatBotCommand(...)` comes from older samples and is not a reliable current pattern for this checkout. +- `ChatBotCommand` exists, but the current built-in bots use Brigadier directly; do not prefer `ChatBotCommand` for new work. +- Blocking `Thread.Sleep(...)` inside `Update()` is a bad default. Prefer timers, counters, or timestamp-based scheduling. +- Mutating the `Container` returned by `GetPlayerInventory()` does not change the server. Use inventory actions instead. diff --git a/.skills/mcc-chatbot-authoring/references/pattern-cookbook.md b/.skills/mcc-chatbot-authoring/references/pattern-cookbook.md new file mode 100644 index 00000000..4d17c85c --- /dev/null +++ b/.skills/mcc-chatbot-authoring/references/pattern-cookbook.md @@ -0,0 +1,330 @@ +# MCC Pattern Cookbook + +Concrete patterns for standalone MCC `/script` bots. Use these before inventing new scaffolding. + +## Periodic task without threads + +Use `Update()` plus a timestamp or counter. This comes from the old `sample-script-with-task.cs` example and still holds up well. + +```csharp +public class PeriodicTaskBot : ChatBot +{ + private DateTime nextRun = DateTime.MinValue; + + public override void Update() + { + var now = DateTime.UtcNow; + if (now < nextRun) + return; + + nextRun = now.AddSeconds(30); + LogDebugToConsole("Running periodic task"); + SendText("/ping"); + } +} +``` + +Why this pattern is good: +- stays on MCC's normal tick flow +- avoids background threads for simple periodic work +- keeps the bot responsive to unload and disconnect + +## Chat and PM handling + +This combines the useful parts of `TestBot`, `sample-script-pm-forwarder.cs`, and `RemoteControl.cs`. + +```csharp +public override void GetText(string text) +{ + text = GetVerbatim(text); + + string message = ""; + string sender = ""; + + if (IsPrivateMessage(text, ref message, ref sender)) + { + LogToConsole("PM from " + sender + ": " + message); + return; + } + + if (IsChatMessage(text, ref message, ref sender)) + { + LogToConsole("Chat from " + sender + ": " + message); + } +} +``` + +Owner-gated internal command handling: + +Add `//using MinecraftClient.CommandHandler` in the script metadata if you use `CmdResult`. + +```csharp +public override void GetText(string text) +{ + text = GetVerbatim(text).Trim(); + + string command = ""; + string sender = ""; + + if (IsPrivateMessage(text, ref command, ref sender) + && Settings.Config.Main.Advanced.BotOwners.Contains(sender.ToLowerInvariant())) + { + CmdResult result = new(); + PerformInternalCommand(command, ref result); + SendPrivateMessage(sender, result.ToString()); + } +} +``` + +## Movement with prerequisite checks + +Modern movement code should copy the guard style from current built-in bots, not the older constructor-heavy scripts. + +```csharp +public override void Initialize() +{ + if (!GetEntityHandlingEnabled() || !GetTerrainEnabled()) + { + LogToConsole("Entity handling and terrain handling are required."); + UnloadBot(); + } +} +``` + +Simple "look at nearest player" logic adapted from `AutoLook.cs`: + +```csharp +private Entity? trackedPlayer = null; + +public override void OnEntitySpawn(Entity entity) +{ + TryTrack(entity); +} + +public override void OnEntityDespawn(Entity entity) +{ + if (trackedPlayer != null && entity.ID == trackedPlayer.ID) + trackedPlayer = null; +} + +public override void OnEntityMove(Entity entity) +{ + if (!TryTrack(entity)) + return; + + LookAtLocation(entity.Location); +} + +private bool TryTrack(Entity entity) +{ + if (entity.Type != EntityType.Player) + return false; + + if (trackedPlayer == null) + { + trackedPlayer = entity; + return true; + } + + if (GetCurrentLocation().Distance(entity.Location) < GetCurrentLocation().Distance(trackedPlayer.Location)) + trackedPlayer = entity; + + return trackedPlayer.ID == entity.ID; +} +``` + +## Search for dropped items and move to them + +This is the safest pattern to preserve from `ItemsCollector.cs` for standalone scripts. + +```csharp +public class NearbyItemsBot : ChatBot +{ + private DateTime nextScan = DateTime.MinValue; + + public override void Initialize() + { + if (!GetEntityHandlingEnabled() || !GetTerrainEnabled()) + { + LogToConsole("Entity handling and terrain handling are required."); + UnloadBot(); + } + } + + public override void Update() + { + var now = DateTime.UtcNow; + if (now < nextScan || ClientIsMoving()) + return; + + nextScan = now.AddSeconds(1); + + var here = GetCurrentLocation(); + var target = GetEntities().Values + .Where(entity => entity.Type == EntityType.Item && entity.Location.Distance(here) <= 15) + .OrderBy(entity => entity.Location.Distance(here)) + .FirstOrDefault(); + + if (target != null) + MoveToLocation(target.Location); + } +} +``` + +Why this version is better than older farming scripts: +- no unmanaged worker thread +- no busy wait loop around movement +- uses the current `GetEntities()` pattern + +## Search for blocks or crops in the world + +The old sugar cane and mining scripts still contain a useful search idea: use `GetWorld().FindBlock(...)`, then filter and sort. + +```csharp +var targets = GetWorld() + .FindBlock(GetCurrentLocation(), Material.SugarCane, 16) + .Where(block => + GetWorld().GetBlock(new Location(block.X, block.Y - 1, block.Z)).Type == Material.SugarCane) + .OrderBy(block => block.Distance(GetCurrentLocation())) + .ToList(); +``` + +Use this as a search primitive. Then decide separately how to move, dig, or harvest. + +## Inventory access and manipulation + +If a standalone script uses inventory types directly, add this import in the metadata block: + +```csharp +//using MinecraftClient.Inventory +``` + +For built-in bots, add: + +```csharp +using MinecraftClient.Inventory; +``` + +Always guard inventory logic first: + +```csharp +public override void Initialize() +{ + if (!GetInventoryEnabled()) + { + LogToConsole("Inventory handling is required."); + UnloadBot(); + } +} +``` + +Important rule: +- `GetPlayerInventory()` returns a snapshot copy, so editing its `Items` dictionary does not change the server +- actual changes must go through `ChangeSlot(...)`, `WindowAction(...)`, `GetItemMovingHelper(...)`, `UseItemInHand()`, and related helpers + +### Search inventory for an item + +This combines the useful current logic from `Farmer.cs` and `AutoEat.cs`. + +```csharp +private bool TrySwitchToItem(ItemType itemType) +{ + var inventory = GetPlayerInventory(); + + if (inventory.Items.TryGetValue(GetCurrentSlot() - 36, out var held) && held.Type == itemType) + return true; + + var hotbarSlots = inventory.SearchItem(itemType) + .Where(slot => slot >= 36 && slot <= 44) + .ToArray(); + + if (hotbarSlots.Length == 0) + return false; + + ChangeSlot((short)(hotbarSlots[0] - 36)); + return true; +} +``` + +Use this for simple hotbar selection. For deeper inventory reshuffling, built-in bots usually need more helper logic. + +### Move an item into the hotbar + +Use this when the item exists in inventory but is not already on the hotbar. + +```csharp +private bool TryMoveItemToHotbar(ItemType itemType, short targetHotbarSlot = 0) +{ + var inventory = GetPlayerInventory(); + var matches = inventory.SearchItem(itemType); + + if (matches.Length == 0) + return false; + + var targetInventorySlot = 36 + targetHotbarSlot; + + if (matches[0] >= 36 && matches[0] <= 44) + { + ChangeSlot((short)(matches[0] - 36)); + return true; + } + + var movingHelper = GetItemMovingHelper(inventory); + movingHelper.Swap(matches[0], targetInventorySlot); + ChangeSlot(targetHotbarSlot); + return true; +} +``` + +Why this pattern is good: +- it reads the current snapshot first +- it does not pretend local `Container` edits affect the server +- it uses the item-moving helper for real inventory manipulation + +### Drop or click items with window actions + +Use `WindowAction(...)` when the bot needs direct inventory clicks or dropping behavior. + +```csharp +private void DropAllOfType(ItemType itemType) +{ + var inventory = GetPlayerInventory(); + + foreach (int slot in inventory.SearchItem(itemType)) + WindowAction(0, slot, WindowActionType.DropItemStack); +} +``` + +Use this pattern carefully: +- verify the correct inventory ID first +- prefer reacting to `OnInventoryUpdate(...)` for larger inventory workflows +- for crafting or chest workflows, use `GetInventories()` and `CloseInventory(...)` as needed + +## Built-in command bot pattern + +Only use this when the user explicitly asks for a built-in bot. + +```csharp +public override void Initialize() +{ + McClient.dispatcher.Register(l => l.Literal("help") + .Then(l => l.Literal(CommandName) + .Executes(r => OnCommandHelp(r.Source, string.Empty)) + ) + ); + + McClient.dispatcher.Register(l => l.Literal(CommandName) + .Then(l => l.Literal("_help") + .Executes(r => OnCommandHelp(r.Source, string.Empty)) + .Redirect(McClient.dispatcher.GetRoot().GetChild("help").GetChild(CommandName))) + ); +} + +public override void OnUnload() +{ + McClient.dispatcher.Unregister(CommandName); + McClient.dispatcher.GetRoot().GetChild("help").RemoveChild(CommandName); +} +``` + +Use a built-in bot only when the user explicitly asks for compiled MCC behavior or repo wiring.