Minecraft-Console-Client/MinecraftClient/Scripting/MccGameCommon.cs
Ítalo Seara f34b8ba989 feat(inventory): expose NBT/component data in inventory MCP tools
Add a nullable `Nbt` field to `MccInventorySnapshotSlot`,
`MccInventorySearchMatch`, and `MccItemStackSnapshot` so that callers
can access the full item metadata beyond just material and count.

For 1.20.6+ servers the field is a map of component name
(e.g. "minecraft:custom_data") to the serialized component object.
For legacy servers it is the raw NBT dictionary. The field is omitted
entirely for vanilla items that carry no extra data, so there is no
noise for plain items like Stone or Dirt.

Implementation:
- `StructuredComponent`: add `ComponentName` property (set by the
  registry during parse) and mark both it and `TypeId` with
  `[JsonIgnore]` so they are excluded from component serialization.
- `StructuredComponentRegistry.ParseComponent`: assign `ComponentName`
  after instantiation.
- `MccGameCommon.BuildNbt`: new helper that serializes components by
  runtime type (fixing the polymorphism issue with System.Text.Json)
  keyed by component name, falling back to the legacy NBT dictionary.
- Snapshot and search query builders updated to call `BuildNbt`.
2026-06-27 14:41:41 -03:00

233 lines
6.7 KiB
C#

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; }
public Dictionary<string, object>? Nbt { 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,
Nbt = BuildNbt(item)
};
}
public static Dictionary<string, object>? BuildNbt(Item item)
{
if (item.Components is { Count: > 0 })
{
return item.Components
.Where(c => !string.IsNullOrEmpty(c.ComponentName))
.ToDictionary(
c => c.ComponentName,
c => (object)System.Text.Json.JsonSerializer.SerializeToElement(c, c.GetType())
);
}
return item.NBT is { Count: > 0 } ? item.NBT : null;
}
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;
}
}