mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
330 lines
8.5 KiB
Markdown
330 lines
8.5 KiB
Markdown
# 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.
|