Minecraft-Console-Client/.skills/mcc-chatbot-authoring/references/pattern-cookbook.md
2026-03-21 17:34:34 +01:00

8.5 KiB

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.

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.

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.

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.

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:

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.

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.

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:

//using MinecraftClient.Inventory

For built-in bots, add:

using MinecraftClient.Inventory;

Always guard inventory logic first:

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.

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.

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.

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.

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.