๐Ÿ“‚ File Browser

//serversdashboard/lib
โ˜€๏ธ Light Mode
๐ŸŽฏ Quick Launch:

๐Ÿ“ Directories

๐Ÿ“„ Files

๐Ÿ“„ 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