๐Ÿ“‚ File Browser

//serversdashboard/serversdashboard/lib
๐ŸŒ™ Dark Mode
๐ŸŽฏ Quick Launch:

๐Ÿ“ Directories

๐Ÿ“„ Files

๐Ÿ“„ Source: snbt.php

<?php
/**
 * Minimal SNBT (Stringified NBT) parser, tuned for modern Minecraft
 * (1.20.5+ component format) as well as the older tag format.
 */

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();

        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('[');

        // Typed array prefix: [I; ... ] [L; ... ] [B; ... ]
        if (preg_match('/\G([ILB]);/', $this->s, $m, 0, $this->i)) {
            $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();
        $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);

        // Numeric 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;
        }
        if ($tok === 'true')  return 1;
        if ($tok === 'false') return 0;
        return $tok;
    }

    private function expect(string $ch): void {
        $this->skipWs();
        if ($this->peek() !== $ch) {
            throw new SnbtParseError("Expected '$ch' at offset {$this->i}");
        }
        $this->i++;
    }
}

/* ---------------- Inventory shaping ---------------- */

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.
 *
 * Handles BOTH formats:
 *   OLD (<= 1.20.4):   {Slot:0b, id:"...", Count:64b, tag:{Damage:5, Enchantments:[...]}}
 *   NEW (>= 1.20.5):   {slot:0, id:"...", count:64, components:{"minecraft:damage":5, ...}}
 *
 * We accept either casing of Slot/Slot, Count/count.
 */
function snbt_describe_item(array $entry): ?array {
    if (!isset($entry['id'])) return null;

    $id    = (string)$entry['id'];
    $count = 1;
    if (isset($entry['count']))      $count = (int)$entry['count'];
    elseif (isset($entry['Count']))  $count = (int)$entry['Count'];

    $slot = null;
    if (isset($entry['slot']))       $slot = (int)$entry['slot'];
    elseif (isset($entry['Slot']))   $slot = (int)$entry['Slot'];

    $customName   = null;
    $enchantments = [];
    $damage       = null;
    $maxDamage    = null;
    $contained    = null;

    // Old format: tag:{...}
    // New format: components:{"minecraft:...": ...}
    $tag = $entry['tag'] ?? $entry['components'] ?? null;

    if (is_array($tag)) {
        // Custom name
        // Old: tag.display.Name = '{"text":"..."}'
        // New: components."minecraft:custom_name" = '{"text":"..."}' or a plain string
        $nameRaw = null;
        if (isset($tag['display']['Name'])) {
            $nameRaw = (string)$tag['display']['Name'];
        } elseif (isset($tag['minecraft:custom_name'])) {
            $nameRaw = is_string($tag['minecraft:custom_name'])
                ? $tag['minecraft:custom_name']
                : json_encode($tag['minecraft:custom_name']);
        }
        if ($nameRaw !== null) {
            // Strip JSON structure and legacy color codes
            $clean = preg_replace('/\{"text":"([^"]*)".*/', '$1', $nameRaw);
            $clean = preg_replace('/ยง./u', '', $clean);
            $clean = trim($clean, '"\' ');
            if ($clean !== '') $customName = $clean;
        }

        // Enchantments
        // Old: tag.Enchantments = [{id:"minecraft:sharpness", lvl:5}]
        // New: components."minecraft:enchantments" = {"minecraft:sharpness": 5}
        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),
                ];
            }
        } elseif (isset($tag['minecraft:enchantments']) && is_array($tag['minecraft:enchantments'])) {
            foreach ($tag['minecraft:enchantments'] as $enId => $lvl) {
                $enchantments[] = [
                    'id'    => (string)$enId,
                    'level' => (int)$lvl,
                ];
            }
        }

        // Damage / durability
        // Old: tag.Damage = 42, tag.MaxDamage (rare)
        // New: components."minecraft:damage" = 42, components."minecraft:max_damage" = 1561
        if (isset($tag['Damage']))              $damage    = (int)$tag['Damage'];
        elseif (isset($tag['minecraft:damage'])) $damage   = (int)$tag['minecraft:damage'];

        if (isset($tag['MaxDamage']))               $maxDamage = (int)$tag['MaxDamage'];
        elseif (isset($tag['minecraft:max_damage'])) $maxDamage = (int)$tag['minecraft:max_damage'];

        // Container contents
        // Old shulker: tag.BlockEntityTag.Items = [...]
        // New shulker: components."minecraft:container" = [{slot:0, item:{id:..., count:...}}, ...]
        // Bundle:      components."minecraft:bundle_contents" = [{id:..., count:...}, ...]
        $containerRaw = null;
        if (isset($tag['BlockEntityTag']['Items']) && is_array($tag['BlockEntityTag']['Items'])) {
            $containerRaw = $tag['BlockEntityTag']['Items'];
        } elseif (isset($tag['minecraft:container']) && is_array($tag['minecraft:container'])) {
            // Each entry: {slot:0b, item:{id:"...", count:N}}
            $containerRaw = [];
            foreach ($tag['minecraft:container'] as $cEntry) {
                if (!is_array($cEntry)) continue;
                $sub = $cEntry['item'] ?? null;
                if (!is_array($sub)) continue;
                $sub['slot'] = $cEntry['slot'] ?? null;
                $containerRaw[] = $sub;
            }
        } elseif (isset($tag['minecraft:bundle_contents']) && is_array($tag['minecraft:bundle_contents'])) {
            $containerRaw = $tag['minecraft:bundle_contents'];
        }

        if ($containerRaw !== null) {
            $contained = [];
            foreach ($containerRaw as $sub) {
                if (!is_array($sub)) continue;
                $d = snbt_describe_item($sub);
                if ($d !== null) $contained[] = $d;
            }
            // Bundles don't carry a slot on each sub-item โ€” assign sequential
            $needsSlots = false;
            foreach ($contained as $c) { if ($c['slot'] === null) { $needsSlots = true; break; } }
            if ($needsSlots) {
                foreach ($contained as $idx => &$c) { $c['slot'] = $idx; }
                unset($c);
            }
            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'       => $damage,
        'max_damage'   => $maxDamage,
        'contains'     => $contained,
    ];
}

function snbt_pretty_item_name(string $id): string {
    $short = strpos($id, ':') !== false ? substr($id, strpos($id, ':') + 1) : $id;
    return ucwords(str_replace('_', ' ', $short));
}
โ† Back