๐ Source: snbt.php
<?php
/**
* Minimal SNBT (Stringified NBT) parser.
*
* Minecraft's /data get entity <player> Inventory returns the inventory as
* SNBT text, e.g.:
*
* [{Slot:0b,id:"minecraft:stone",Count:64b}, {Slot:1b,id:"minecraft:diamond_sword",Count:1b,tag:{...}}]
*
* This parser turns that into plain PHP arrays/objects we can work with.
*
* Supported:
* - compounds {key:value,key:value}
* - lists [value,value,value]
* - strings "text" or 'text'
* - numbers 123, -4, 1.5, 1e-3
* - suffixed 1b 1s 1l 1f 1d (numeric suffixes)
* - booleans true / false (as 1b / 0b in practice)
*
* Not supported (rarely needed for inventory):
* - typed arrays: [I;1,2,3], [L;...], [B;...]
*/
class SnbtParseError extends Exception {}
class SnbtParser {
private string $s;
private int $i = 0;
private int $len;
public function __construct(string $input) {
$this->s = $input;
$this->len = strlen($input);
}
public static function parse(string $input) {
$p = new self($input);
$p->skipWs();
$v = $p->parseValue();
$p->skipWs();
return $v;
}
private function skipWs(): void {
while ($this->i < $this->len && ctype_space($this->s[$this->i])) {
$this->i++;
}
}
private function peek(): ?string {
return $this->i < $this->len ? $this->s[$this->i] : null;
}
private function parseValue() {
$this->skipWs();
$c = $this->peek();
if ($c === null) throw new SnbtParseError('Unexpected end of input');
if ($c === '{') return $this->parseCompound();
if ($c === '[') return $this->parseList();
if ($c === '"' || $c === "'") return $this->parseString();
// Bare token: number, true/false, or unquoted string (SNBT allows it)
return $this->parseBareToken();
}
private function parseCompound(): array {
$this->expect('{');
$out = [];
$this->skipWs();
if ($this->peek() === '}') { $this->i++; return $out; }
while (true) {
$this->skipWs();
$key = $this->parseKey();
$this->skipWs();
$this->expect(':');
$val = $this->parseValue();
$out[$key] = $val;
$this->skipWs();
$c = $this->peek();
if ($c === ',') { $this->i++; continue; }
if ($c === '}') { $this->i++; break; }
throw new SnbtParseError("Expected , or } at offset {$this->i}");
}
return $out;
}
private function parseList(): array {
$this->expect('[');
// Check for typed array prefix: [I; ... ] [L; ... ] [B; ... ]
$save = $this->i;
if (preg_match('/\G([ILB]);/', $this->s, $m, 0, $this->i)) {
// Skip the type prefix โ treat contents as a plain list
$this->i += 2;
}
$out = [];
$this->skipWs();
if ($this->peek() === ']') { $this->i++; return $out; }
while (true) {
$out[] = $this->parseValue();
$this->skipWs();
$c = $this->peek();
if ($c === ',') { $this->i++; continue; }
if ($c === ']') { $this->i++; break; }
throw new SnbtParseError("Expected , or ] at offset {$this->i}");
}
return $out;
}
private function parseKey(): string {
$c = $this->peek();
if ($c === '"' || $c === "'") return $this->parseString();
// Unquoted key โ read until : or whitespace
$start = $this->i;
while ($this->i < $this->len) {
$ch = $this->s[$this->i];
if ($ch === ':' || ctype_space($ch)) break;
$this->i++;
}
return substr($this->s, $start, $this->i - $start);
}
private function parseString(): string {
$quote = $this->s[$this->i];
$this->i++;
$out = '';
while ($this->i < $this->len) {
$ch = $this->s[$this->i];
if ($ch === '\\') {
$this->i++;
$next = $this->peek();
$out .= match ($next) {
'n' => "\n", 't' => "\t", 'r' => "\r",
'\\', '"', "'" => $next,
default => $next,
};
$this->i++;
continue;
}
if ($ch === $quote) {
$this->i++;
return $out;
}
$out .= $ch;
$this->i++;
}
throw new SnbtParseError('Unterminated string');
}
private function parseBareToken() {
$start = $this->i;
while ($this->i < $this->len) {
$ch = $this->s[$this->i];
if ($ch === ',' || $ch === '}' || $ch === ']' || ctype_space($ch)) break;
$this->i++;
}
$tok = substr($this->s, $start, $this->i - $start);
// Suffixes: 64b, 1s, 1l, 1f, 1d
if (preg_match('/^(-?\d+)([bslBSL])$/', $tok, $m)) {
return (int)$m[1];
}
if (preg_match('/^(-?\d*\.?\d+(?:[eE][-+]?\d+)?)([fdFD])$/', $tok, $m)) {
return (float)$m[1];
}
if (is_numeric($tok)) {
return strpos($tok, '.') !== false ? (float)$tok : (int)$tok;
}
// true/false as bare tokens
if ($tok === 'true') return 1;
if ($tok === 'false') return 0;
return $tok; // unquoted string
}
private function expect(string $ch): void {
$this->skipWs();
if ($this->peek() !== $ch) {
throw new SnbtParseError("Expected '$ch' at offset {$this->i}");
}
$this->i++;
}
}
/**
* Turn a raw inventory NBT array into a clean, dashboard-friendly shape.
*
* Input: array of slot entries like
* ['Slot'=>0, 'id'=>'minecraft:stone', 'Count'=>64, 'tag'=>[...]]
*
* Output: ['items'=> [...], 'enderchest'=> [...], 'shulkers'=> [...]]
*/
function snbt_summarize_inventory(array $raw): array {
$items = [];
foreach ($raw as $entry) {
if (!is_array($entry)) continue;
$item = snbt_describe_item($entry);
if ($item !== null) $items[] = $item;
}
usort($items, fn($a, $b) => ($a['slot'] ?? 0) <=> ($b['slot'] ?? 0));
return ['items' => $items];
}
/**
* Turn a single inventory slot entry into a readable summary.
*/
function snbt_describe_item(array $entry): ?array {
if (!isset($entry['id'])) return null;
$id = (string)$entry['id'];
$count = isset($entry['Count']) ? (int)$entry['Count'] : 1;
$slot = isset($entry['Slot']) ? (int)$entry['Slot'] : null;
$customName = null;
$enchantments = [];
$durability = null;
$maxDamage = null;
$contained = null; // shulker box contents
$enderItems = null; // ender chest contents (when this is the ender items tag)
if (isset($entry['tag']) && is_array($entry['tag'])) {
$tag = $entry['tag'];
if (isset($tag['display']['Name'])) {
$raw = (string)$tag['display']['Name'];
// strip JSON formatting
$customName = trim(preg_replace('/\{[^}]*\}|ยง./u', '', $raw));
$customName = trim($customName, '"\' ');
}
if (isset($tag['Enchantments']) && is_array($tag['Enchantments'])) {
foreach ($tag['Enchantments'] as $en) {
if (!is_array($en)) continue;
$enchantments[] = [
'id' => (string)($en['id'] ?? 'unknown'),
'level' => (int)($en['lvl'] ?? 0),
];
}
}
if (isset($tag['Damage'])) $durability = (int)$tag['Damage'];
if (isset($tag['MaxDamage'])) $maxDamage = (int)$tag['MaxDamage'];
// Shulker box contents live in tag.BlockEntityTag.Items
if (isset($tag['BlockEntityTag']['Items']) && is_array($tag['BlockEntityTag']['Items'])) {
$contained = [];
foreach ($tag['BlockEntityTag']['Items'] as $sub) {
if (!is_array($sub)) continue;
$d = snbt_describe_item($sub);
if ($d !== null) $contained[] = $d;
}
usort($contained, fn($a, $b) => ($a['slot'] ?? 0) <=> ($b['slot'] ?? 0));
}
}
return [
'slot' => $slot,
'id' => $id,
'name' => snbt_pretty_item_name($id),
'count' => $count,
'custom_name' => $customName,
'enchantments' => $enchantments,
'damage' => $durability,
'max_damage' => $maxDamage,
'contains' => $contained,
];
}
/**
* "minecraft:diamond_sword" โ "Diamond Sword"
*/
function snbt_pretty_item_name(string $id): string {
$short = strpos($id, ':') !== false ? substr($id, strpos($id, ':') + 1) : $id;
return ucwords(str_replace('_', ' ', $short));
}
โ Back