Refactored to share the new utilities between the MCP and the Chat Bot API

This commit is contained in:
Anon 2026-04-03 19:06:43 +02:00
parent a88ca3cb71
commit dfef24ad10
12 changed files with 2225 additions and 1027 deletions

View file

@ -215,6 +215,11 @@ namespace MinecraftClient.Scripting
this.localVars = localVars;
}
/// <summary>
/// Access the shared MCC gameplay API used by bots and the embedded MCP server.
/// </summary>
new public MccGameApi Game => base.Game;
/* == Wrappers for ChatBot API with public visibility and call limit to one per tick for safety == */
/// <summary>

View file

@ -48,6 +48,7 @@ namespace MinecraftClient.Scripting
private readonly List<string> registeredChatBotCommands = new();
private readonly Lock delayTasksLock = new();
private readonly List<TaskWithDelay> delayedTasks = new();
private MccGameApi? _game;
protected McClient Handler
{
get
@ -60,6 +61,11 @@ namespace MinecraftClient.Scripting
}
}
/// <summary>
/// Shared gameplay and observed-state API used by MCP and available to built-in bots and scripts.
/// </summary>
protected MccGameApi Game => _game ??= new MccGameApi(() => Handler);
/// <summary>
/// Will be called every client tick (~50ms at 20 TPS).
/// </summary>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,216 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using MinecraftClient.Inventory;
using MinecraftClient.Mapping;
namespace MinecraftClient.Scripting;
/// <summary>
/// Represents a world coordinate rounded for tool and script consumption.
/// </summary>
public sealed class MccCoordinate
{
public required double X { get; init; }
public required double Y { get; init; }
public required double Z { get; init; }
}
/// <summary>
/// Represents a block state snapshot.
/// </summary>
public sealed class MccBlockStateSnapshot
{
public required string Material { get; init; }
public required string TypeLabel { get; init; }
public required int BlockId { get; init; }
public required int BlockMeta { get; init; }
}
/// <summary>
/// Represents a simple item stack snapshot.
/// </summary>
public sealed class MccItemStackSnapshot
{
public required string Type { get; init; }
public required int Count { get; init; }
}
/// <summary>
/// Shared normalization and formatting helpers for gameplay operations.
/// </summary>
public static class MccGameCommon
{
private const int CoordinateRoundingPrecision = 2;
public static bool TryParseItemType(string rawItemType, out ItemType itemType)
{
if (Enum.TryParse(rawItemType, true, out itemType) && itemType is not (ItemType.Unknown or ItemType.Null))
return true;
string normalized = NormalizeToken(rawItemType);
if (normalized.Length == 0)
{
itemType = ItemType.Unknown;
return false;
}
foreach (ItemType candidate in Enum.GetValues<ItemType>())
{
if (candidate is ItemType.Unknown or ItemType.Null)
continue;
if (NormalizeToken(candidate.ToString()) == normalized)
{
itemType = candidate;
return true;
}
}
itemType = ItemType.Unknown;
return false;
}
public static string NormalizeToken(string value)
{
if (string.IsNullOrWhiteSpace(value))
return string.Empty;
char[] buffer = value
.Where(char.IsLetterOrDigit)
.Select(char.ToLowerInvariant)
.ToArray();
return new string(buffer);
}
public static bool TextEqualsFilter(string text, string filter)
{
return text.Equals(filter, StringComparison.OrdinalIgnoreCase)
|| NormalizeToken(text) == NormalizeToken(filter);
}
public static bool TextMatchesFilter(string text, string filter)
{
if (text.Contains(filter, StringComparison.OrdinalIgnoreCase))
return true;
string normalizedFilter = NormalizeToken(filter);
if (normalizedFilter.Length == 0)
return false;
return NormalizeToken(text).Contains(normalizedFilter, StringComparison.Ordinal);
}
public static double GetDistance(Location from, Location to)
{
double dx = from.X - to.X;
double dy = from.Y - to.Y;
double dz = from.Z - to.Z;
return Math.Sqrt(dx * dx + dy * dy + dz * dz);
}
public static double RoundCoordinate(double value)
{
return Math.Round(value, CoordinateRoundingPrecision, MidpointRounding.AwayFromZero);
}
public static MccCoordinate ToCoordinate(Location location)
{
return ToCoordinate(location.X, location.Y, location.Z);
}
public static MccCoordinate ToCoordinate(double x, double y, double z)
{
return new MccCoordinate
{
X = RoundCoordinate(x),
Y = RoundCoordinate(y),
Z = RoundCoordinate(z)
};
}
public static Location ToBlockLocation(double x, double y, double z)
{
return new Location(Math.Floor(x), Math.Floor(y), Math.Floor(z));
}
public static MccBlockStateSnapshot ToBlockState(Block block)
{
return new MccBlockStateSnapshot
{
Material = block.Type.ToString(),
TypeLabel = block.GetTypeString(),
BlockId = block.BlockId,
BlockMeta = block.BlockMeta
};
}
public static object? DescribeMetadataValue(object? value)
{
return value switch
{
null => null,
string s => s,
bool b => b,
byte b => b,
sbyte b => b,
short s => s,
ushort s => s,
int i => i,
uint i => i,
long l => l,
ulong l => l,
float f => f,
double d => d,
decimal d => d,
Enum e => e.ToString(),
Location location => ToCoordinate(location),
Item item => ToItemStack(item),
byte[] data => new { bytes = data.Length },
_ => value.ToString()
};
}
public static MccItemStackSnapshot ToItemStack(Item item)
{
return new MccItemStackSnapshot
{
Type = item.Type.ToString(),
Count = item.Count
};
}
public static bool TryParseBlockQuery(string? query, out int? blockId, out int? blockMeta)
{
blockId = null;
blockMeta = null;
if (string.IsNullOrWhiteSpace(query))
return false;
string trimmed = query.Trim();
int separator = trimmed.IndexOf(':');
if (separator >= 0)
{
string idPart = trimmed[..separator].Trim();
string metaPart = trimmed[(separator + 1)..].Trim();
if (int.TryParse(idPart, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsedId))
{
blockId = parsedId;
if (int.TryParse(metaPart, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsedMeta))
blockMeta = parsedMeta;
return true;
}
return false;
}
if (int.TryParse(trimmed, NumberStyles.Integer, CultureInfo.InvariantCulture, out int blockStateId))
{
blockId = blockStateId;
return true;
}
return false;
}
}

View file

@ -0,0 +1,319 @@
using System;
using System.Collections.Generic;
namespace MinecraftClient.Scripting;
public sealed class MccRecentEventsResult
{
public required long AfterId { get; init; }
public required long LatestId { get; init; }
public required int Count { get; init; }
public required MccRecentEventEntry[] Events { get; init; }
}
public sealed class MccChatHistoryResult
{
public required int Count { get; init; }
public required MccChatHistoryEntry[] Entries { get; init; }
}
public sealed class MccPathPreviewResult
{
public required bool PathFound { get; init; }
public required bool ExactReachable { get; init; }
public required MccCoordinate Target { get; init; }
public required MccCoordinate StartLocation { get; init; }
public MccCoordinate? FinalWaypoint { get; init; }
public double? FinalDistance { get; init; }
public required int WaypointCount { get; init; }
public required bool Truncated { get; init; }
public required MccCoordinate[] Waypoints { get; init; }
public required bool AllowUnsafe { get; init; }
public required int MaxOffset { get; init; }
public required int MinOffset { get; init; }
public required int TimeoutMs { get; init; }
}
public sealed class MccReachabilityResult
{
public required bool Reachable { get; init; }
public required bool ExactReachable { get; init; }
public required MccCoordinate Target { get; init; }
public required MccCoordinate StartLocation { get; init; }
public MccCoordinate? FinalWaypoint { get; init; }
public double? FinalDistance { get; init; }
public required int WaypointCount { get; init; }
public required bool AllowUnsafe { get; init; }
public required int MaxOffset { get; init; }
public required int MinOffset { get; init; }
public required int TimeoutMs { get; init; }
}
public sealed class MccPlayersDetailedEntry
{
public string? Name { get; init; }
public required Guid Uuid { get; init; }
public required int Ping { get; init; }
public required int Gamemode { get; init; }
public required bool Listed { get; init; }
public string? DisplayName { get; init; }
public int? EntityId { get; init; }
public double? X { get; init; }
public double? Y { get; init; }
public double? Z { get; init; }
}
public sealed class MccPlayersDetailedResult
{
public required bool IncludeSelf { get; init; }
public required bool IncludeCoordinates { get; init; }
public required int Count { get; init; }
public required MccPlayersDetailedEntry[] Players { get; init; }
}
public sealed class MccNearbyPlayerEntry
{
public required int EntityId { get; init; }
public required Guid Uuid { get; init; }
public string? Name { get; init; }
public string? CustomName { get; init; }
public required double X { get; init; }
public required double Y { get; init; }
public required double Z { get; init; }
public required double Distance { get; init; }
public required int Latency { get; init; }
}
public sealed class MccPlayerNearbyResult
{
public required double Radius { get; init; }
public string? PlayerName { get; init; }
public required bool IncludeSelf { get; init; }
public required bool AnyNearby { get; init; }
public required int Count { get; init; }
public required MccNearbyPlayerEntry[] Players { get; init; }
}
public sealed class MccLocatedPlayerResult
{
public required string PlayerName { get; init; }
public string? MatchedName { get; init; }
public required int EntityId { get; init; }
public required Guid Uuid { get; init; }
public required double X { get; init; }
public required double Y { get; init; }
public required double Z { get; init; }
public required double Distance { get; init; }
}
public sealed class MccEntitySummary
{
public required int Id { get; init; }
public required string Type { get; init; }
public required string TypeLabel { get; init; }
public required Guid Uuid { get; init; }
public string? Name { get; init; }
public string? CustomName { get; init; }
public required double X { get; init; }
public required double Y { get; init; }
public required double Z { get; init; }
public double? Distance { get; init; }
public float? Health { get; init; }
public string? Pose { get; init; }
public int? Latency { get; init; }
}
public sealed class MccQueryEntitiesResult
{
public required int Count { get; init; }
public required MccEntitySummary[] Entities { get; init; }
}
public sealed class MccListEntitiesResult
{
public required int TotalTracked { get; init; }
public required int Count { get; init; }
public required MccEntitySummary[] Entities { get; init; }
}
public sealed class MccEntityEquipmentEntry
{
public required int Slot { get; init; }
public required string Type { get; init; }
public required int Count { get; init; }
}
public sealed class MccEffectSnapshot
{
public required string Id { get; init; }
public string? Name { get; init; }
public required int Amplifier { get; init; }
public required int RemainingSeconds { get; init; }
public required bool IsInfinite { get; init; }
}
public sealed class MccEntityInfoResult
{
public required int Id { get; init; }
public required string Type { get; init; }
public required string TypeLabel { get; init; }
public required Guid Uuid { get; init; }
public string? Name { get; init; }
public string? CustomName { get; init; }
public required bool CustomNameVisible { get; init; }
public required double X { get; init; }
public required double Y { get; init; }
public required double Z { get; init; }
public required float Yaw { get; init; }
public required float Pitch { get; init; }
public required float Health { get; init; }
public string? Pose { get; init; }
public int? Latency { get; init; }
public int? ObjectData { get; init; }
public Dictionary<string, object?>? Metadata { get; init; }
public MccEntityEquipmentEntry[]? Equipment { get; init; }
public MccEffectSnapshot[]? ActiveEffects { get; init; }
}
public sealed class MccMoveToPlayerTarget
{
public string? PlayerName { get; init; }
public required int EntityId { get; init; }
public required double X { get; init; }
public required double Y { get; init; }
public required double Z { get; init; }
}
public sealed class MccMoveToPlayerResult
{
public required bool PathFound { get; init; }
public required bool Arrived { get; init; }
public required double Tolerance { get; init; }
public required int VerifyWaitMs { get; init; }
public required MccMoveToPlayerTarget Target { get; init; }
public required MccCoordinate StartLocation { get; init; }
public required MccCoordinate FinalLocation { get; init; }
public required double FinalDistance { get; init; }
public required double DistanceMoved { get; init; }
public required bool AllowUnsafe { get; init; }
public required bool AllowDirectTeleport { get; init; }
public required int MaxOffset { get; init; }
public required int MinOffset { get; init; }
public required int TimeoutMs { get; init; }
}
public sealed class MccHotbarSelectionResult
{
public required bool Success { get; init; }
public required string ItemType { get; init; }
public required int InventorySlot { get; init; }
public required int SelectedSlot { get; init; }
public required int Count { get; init; }
}
public sealed class MccInventorySnapshotSlot
{
public required int Slot { get; init; }
public required string Type { get; init; }
public required int Count { get; init; }
}
public sealed class MccInventorySnapshotResult
{
public required int Id { get; init; }
public required string Type { get; init; }
public required string Title { get; init; }
public required int SlotCount { get; init; }
public required MccInventorySnapshotSlot[] Slots { get; init; }
public MccItemStackSnapshot? Cursor { get; init; }
}
public sealed class MccInventorySearchMatch
{
public required int InventoryId { get; init; }
public required string InventoryType { get; init; }
public required string InventoryTitle { get; init; }
public required int Slot { get; init; }
public required string ItemType { get; init; }
public required string TypeLabel { get; init; }
public required int Count { get; init; }
public required bool IsPlayerInventory { get; init; }
public int? HotbarSlot { get; init; }
}
public sealed class MccInventorySearchResult
{
public required string Query { get; init; }
public required bool ExactMatch { get; init; }
public required bool IncludeContainers { get; init; }
public required int Count { get; init; }
public required MccInventorySearchMatch[] Matches { get; init; }
}
public sealed class MccInventoryListEntry
{
public required int Id { get; init; }
public required string Type { get; init; }
public required string Title { get; init; }
public required int SlotCount { get; init; }
public required int NonEmptySlots { get; init; }
public required bool Active { get; init; }
}
public sealed class MccInventoryListResult
{
public required int Count { get; init; }
public required MccInventoryListEntry[] Inventories { get; init; }
}
public sealed class MccItemEntityEntry
{
public required int EntityId { get; init; }
public required string ItemType { get; init; }
public required string TypeLabel { get; init; }
public required int Count { get; init; }
public required double X { get; init; }
public required double Y { get; init; }
public required double Z { get; init; }
public required double Distance { get; init; }
}
public sealed class MccItemEntitiesResult
{
public string? ItemType { get; init; }
public required double Radius { get; init; }
public required int Count { get; init; }
public required MccItemEntityEntry[] Items { get; init; }
}
public sealed class MccPickupAttempt
{
public required int EntityId { get; init; }
public required string ItemType { get; init; }
public required string TypeLabel { get; init; }
public required int ExpectedCount { get; init; }
public required MccCoordinate Target { get; init; }
public required bool PathFound { get; init; }
public required bool Arrived { get; init; }
public required bool EntityGone { get; init; }
public required int InventoryDelta { get; init; }
public required MccCoordinate StartLocation { get; init; }
public required MccCoordinate FinalLocation { get; init; }
public required double FinalDistance { get; init; }
}
public sealed class MccPickupItemsResult
{
public required string ItemType { get; init; }
public required double Radius { get; init; }
public required int MaxItems { get; init; }
public required bool AllowUnsafe { get; init; }
public required int TimeoutMs { get; init; }
public required int Attempted { get; init; }
public required int SuccessfulPickups { get; init; }
public required int CollectedCount { get; init; }
public int? InitialInventoryCount { get; init; }
public int? FinalInventoryCount { get; init; }
public required int RemainingNearby { get; init; }
public required MccPickupAttempt[] Attempts { get; init; }
}

View file

@ -0,0 +1,63 @@
using System;
namespace MinecraftClient.Scripting;
/// <summary>
/// Represents the outcome of a shared MCC game operation.
/// </summary>
public class MccGameResult
{
public bool Success { get; init; }
public string? ErrorCode { get; init; }
public string? Message { get; init; }
public static MccGameResult Ok(string? message = null)
{
return new MccGameResult
{
Success = true,
Message = message
};
}
public static MccGameResult Fail(string errorCode, string? message = null)
{
ArgumentException.ThrowIfNullOrEmpty(errorCode);
return new MccGameResult
{
Success = false,
ErrorCode = errorCode,
Message = message
};
}
}
/// <summary>
/// Represents the outcome of a shared MCC game operation with typed payload data.
/// </summary>
public sealed class MccGameResult<T> : MccGameResult
{
public T? Data { get; init; }
public static MccGameResult<T> Ok(T? data, string? message = null)
{
return new MccGameResult<T>
{
Success = true,
Data = data,
Message = message
};
}
public static MccGameResult<T> Fail(string errorCode, string? message = null, T? data = default)
{
ArgumentException.ThrowIfNullOrEmpty(errorCode);
return new MccGameResult<T>
{
Success = false,
ErrorCode = errorCode,
Message = message,
Data = data
};
}
}

View file

@ -0,0 +1,205 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace MinecraftClient.Scripting;
/// <summary>
/// Represents a recent high-signal runtime event observed by MCC.
/// </summary>
public sealed class MccRecentEventEntry
{
public required long Id { get; init; }
public required DateTimeOffset TimestampUtc { get; init; }
public required string Type { get; init; }
public object? Data { get; init; }
}
/// <summary>
/// Represents a recent chat or system line observed by MCC.
/// </summary>
public sealed class MccChatHistoryEntry
{
public required DateTimeOffset TimestampUtc { get; init; }
public required string Kind { get; init; }
public required string Text { get; init; }
public string? Sender { get; init; }
public string? Message { get; init; }
public string? Json { get; init; }
}
/// <summary>
/// Represents the latest observed world time and weather values.
/// </summary>
public sealed class MccRuntimeStateSnapshot
{
public long? WorldAge { get; init; }
public long? TimeOfDay { get; init; }
public float? RainLevel { get; init; }
public float? ThunderLevel { get; init; }
}
/// <summary>
/// Shared observed-state store populated from ChatBot callbacks and consumed by MCP and bots/scripts.
/// </summary>
public static class MccObservedStateStore
{
private const int MaxEntries = 500;
private static readonly Lock s_recentEventsLock = new();
private static readonly Lock s_chatHistoryLock = new();
private static readonly Lock s_runtimeStateLock = new();
private static readonly List<MccRecentEventEntry> s_recentEvents = [];
private static readonly List<MccChatHistoryEntry> s_chatHistory = [];
private static long s_nextRecentEventId = 1;
private static long? s_worldAge;
private static long? s_timeOfDay;
private static float? s_rainLevel;
private static float? s_thunderLevel;
public static long AddRecentEvent(string type, object? data = null)
{
ArgumentException.ThrowIfNullOrEmpty(type);
lock (s_recentEventsLock)
{
long id = s_nextRecentEventId++;
s_recentEvents.Add(new MccRecentEventEntry
{
Id = id,
TimestampUtc = DateTimeOffset.UtcNow,
Type = type,
Data = data
});
TrimToMaxEntries(s_recentEvents);
return id;
}
}
public static long GetLatestRecentEventId()
{
lock (s_recentEventsLock)
{
return s_recentEvents.Count > 0 ? s_recentEvents[^1].Id : 0;
}
}
public static MccRecentEventEntry[] GetRecentEventsAfter(long afterId, int maxCount, string? typeFilter = null)
{
int count = Math.Clamp(maxCount, 1, MaxEntries);
string? normalizedFilter = string.IsNullOrWhiteSpace(typeFilter) ? null : typeFilter.Trim();
lock (s_recentEventsLock)
{
return s_recentEvents
.Where(entry => entry.Id > afterId)
.Where(entry => normalizedFilter is null
|| entry.Type.Contains(normalizedFilter, StringComparison.OrdinalIgnoreCase))
.Take(count)
.ToArray();
}
}
public static void AddChatHistoryEntry(MccChatHistoryEntry entry)
{
ArgumentNullException.ThrowIfNull(entry);
lock (s_chatHistoryLock)
{
s_chatHistory.Add(entry);
TrimToMaxEntries(s_chatHistory);
}
}
public static MccChatHistoryEntry[] GetLatestChatHistory(int maxCount)
{
int count = Math.Clamp(maxCount, 1, MaxEntries);
lock (s_chatHistoryLock)
{
return s_chatHistory.TakeLast(count).ToArray();
}
}
public static void SetTime(long worldAge, long timeOfDay)
{
lock (s_runtimeStateLock)
{
s_worldAge = worldAge;
s_timeOfDay = timeOfDay;
}
}
public static void SetRainLevel(float level)
{
lock (s_runtimeStateLock)
{
s_rainLevel = level;
}
}
public static void SetThunderLevel(float level)
{
lock (s_runtimeStateLock)
{
s_thunderLevel = level;
}
}
public static MccRuntimeStateSnapshot GetRuntimeStateSnapshot()
{
lock (s_runtimeStateLock)
{
return new MccRuntimeStateSnapshot
{
WorldAge = s_worldAge,
TimeOfDay = s_timeOfDay,
RainLevel = s_rainLevel,
ThunderLevel = s_thunderLevel
};
}
}
public static void ClearRecentEvents()
{
lock (s_recentEventsLock)
{
s_recentEvents.Clear();
}
}
public static void ClearChatHistory()
{
lock (s_chatHistoryLock)
{
s_chatHistory.Clear();
}
}
public static void ClearRuntimeState()
{
lock (s_runtimeStateLock)
{
s_worldAge = null;
s_timeOfDay = null;
s_rainLevel = null;
s_thunderLevel = null;
}
}
public static void ClearAll()
{
ClearChatHistory();
ClearRuntimeState();
ClearRecentEvents();
}
private static void TrimToMaxEntries<T>(List<T> entries)
{
if (entries.Count > MaxEntries)
entries.RemoveRange(0, entries.Count - MaxEntries);
}
}