16 KiB
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:
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:
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
CmdResultis used in a standalone script, add//using MinecraftClient.CommandHandler
Periodic work pattern
Use Update() plus a counter or timestamp for simple repeated work:
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(...)insideUpdate() - if sending chat, do it from a join-safe path like
Update()orAfterGameJoined(), notInitialize()
Built-in Brigadier command pattern
Use this for built-in command bots:
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:
public class ExampleBot : ChatBot
{
public static Configs Config = new();
[TomlDoNotInlineObject]
public class Configs
{
public bool Enabled = false;
public void OnSettingUpdate()
{
}
}
}
Typical host wiring shape:
[TomlPrecedingComment("$ChatBot.ExampleBot$")]
public ChatBots.ExampleBot.Configs ExampleBot
{
get { return ChatBots.ExampleBot.Config; }
set { ChatBots.ExampleBot.Config = value; ChatBots.ExampleBot.Config.OnSettingUpdate(); }
}
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:
public override void Initialize()
{
if (!GetEntityHandlingEnabled())
{
LogToConsole("Entity handling is required.");
UnloadBot();
return;
}
if (!GetTerrainEnabled())
{
LogToConsole("Terrain handling is required.");
UnloadBot();
return;
}
}
var movementLock = BotMovementLock.Instance;
if (movementLock is { IsLocked: true })
return;
movementLock?.Lock("Example Bot");
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:
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
BotMovementLockby default GetEntities()plus distance ordering is the core search pattern
Inventory selection pattern
Use this as the default hotbar-switch pattern:
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.Itemsmutation 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:
- metadata block
- 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 Namespaceand//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:
//using MinecraftClient.Inventory
For built-in bots, use a normal C# import:
using MinecraftClient.Inventory;
Lifecycle summary
Common lifecycle hooks:
Initialize()called once when the bot loads; use it for cheap setup onlyAfterGameJoined()called after the server has been joined successfully, and again after reconnecting; use it when chat can be sentUpdate()called roughly every 100 msOnUnload()called when the bot unloads; release resources hereOnDisconnect(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(); useAfterGameJoined()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<byte> 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 codesIsChatMessage(text, ref message, ref sender)parses public chatIsPrivateMessage(text, ref message, ref sender)parses private chatIsValidName(username)validates a Minecraft usernameSendText(text)sends chat or server commandsSendPrivateMessage(player, message)sends a private messagePerformInternalCommand(command, ...)runs an internal MCC command, not a server commandLogToConsole(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 inventoryGetInventories()returns current container snapshots- writing to those
Containerobjects 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..44in inventory slot numbering - the offhand slot is commonly
45 Container.SearchItem(...)is the normal way to locate items by type
Good inventory workflow:
- guard with
GetInventoryEnabled() - read the current container using
GetPlayerInventory() - locate slots with
SearchItem(...)orItems - mutate server state using
ChangeSlot(...),WindowAction(...), orItemMovingHelper - if needed, react to
OnInventoryUpdate(...),OnInventoryOpen(...), orOnInventoryClose(...)
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
Configfield - a nested
[TomlDoNotInlineObject]Configsclass for settings - an
Enabled = falsesetting 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.csinsideSettings.ChatBotConfigHealper.ChatBotConfigMinecraftClient/McClient.csinsideRegisterBots(...)
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
BotMovementLockis mainly for built-in bots or shared long-running automation; a simple standalone script that just callsMoveToLocation(...)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.ChatBotCommandexists, but the current built-in bots use Brigadier directly; do not preferChatBotCommandfor new work.- Blocking
Thread.Sleep(...)insideUpdate()is a bad default. Prefer timers, counters, or timestamp-based scheduling. - Mutating the
Containerreturned byGetPlayerInventory()does not change the server. Use inventory actions instead.