Files
cobblemon-survival-pack/tools/nbtfile.py
T
claudeandClaude Opus 5.5 c2a87f178e Phase 6: shell preview, core tooltips, machine test world, faster furnace
Adrian (2026-11-02): double the furnace speed; there must be a way in
game to see how a machine is built; a test world with the machines.

- Large Furnace speed doubled (2 + 0.08 x level + 0.02 x SpA).
- Ghost preview: placing a core, using it with an incomplete shell, or
  sneak-using it empty shows every wrong spot as a small glowing block
  display for 10 s (never saved), in the core's orientation, with a
  chat line counting what's missing.
- Core tooltips: what the machine does, what its shell is made of.
- /cobblesurvival machines showcase builds every machine three times
  (working with a Showcase-owned Pokemon, empty, bare core) with signs,
  hoppers, chests and cables; tools/make_machine_world.py makes a
  superflat creative world from it (level.dat via tools/nbtfile.py)
  and --prism installs it as "Machine Test (Phase 6)".
- Self-tests machine_preview_shows_the_shell and
  machine_showcase_builds_every_machine; docs and checklist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 15:42:14 +01:00

86 lines
3.2 KiB
Python

"""Reads and writes gzipped NBT files (level.dat) keeping every tag's type, so a file can be changed and saved back.
A tag is (type, value): compounds are dicts of name -> tag, lists are (element type, [values]). Used by
tools/make_machine_world.py to make a world creative with cheats on.
"""
import gzip
import struct
END, BYTE, SHORT, INT, LONG, FLOAT, DOUBLE, BYTE_ARRAY, STRING, LIST, COMPOUND, INT_ARRAY, LONG_ARRAY = range(13)
_SCALARS = {BYTE: ">b", SHORT: ">h", INT: ">i", LONG: ">q", FLOAT: ">f", DOUBLE: ">d"}
class _Reader:
def __init__(self, data):
self.data, self.pos = data, 0
def take(self, n):
chunk = self.data[self.pos:self.pos + n]
self.pos += n
return chunk
def unpack(self, fmt):
return struct.unpack(fmt, self.take(struct.calcsize(fmt)))[0]
def string(self):
return self.take(self.unpack(">H")).decode("utf-8")
def payload(self, kind):
if kind in _SCALARS:
return self.unpack(_SCALARS[kind])
if kind == BYTE_ARRAY:
return list(self.take(self.unpack(">i")))
if kind == STRING:
return self.string()
if kind == LIST:
element, length = self.unpack(">b"), self.unpack(">i")
return element, [self.payload(element) for _ in range(length)]
if kind == COMPOUND:
result = {}
while (child := self.unpack(">b")) != END:
name = self.string()
result[name] = (child, self.payload(child))
return result
if kind == INT_ARRAY:
return [self.unpack(">i") for _ in range(self.unpack(">i"))]
if kind == LONG_ARRAY:
return [self.unpack(">q") for _ in range(self.unpack(">i"))]
raise ValueError(f"unknown NBT tag type {kind}")
def _string(text):
raw = text.encode("utf-8")
return struct.pack(">H", len(raw)) + raw
def _payload(kind, value):
if kind in _SCALARS:
return struct.pack(_SCALARS[kind], value)
if kind == BYTE_ARRAY:
return struct.pack(">i", len(value)) + bytes(v & 0xFF for v in value)
if kind == STRING:
return _string(value)
if kind == LIST:
element, items = value
return struct.pack(">bi", element, len(items)) + b"".join(_payload(element, v) for v in items)
if kind == COMPOUND:
return b"".join(struct.pack(">b", k) + _string(name) + _payload(k, v) for name, (k, v) in value.items()) + b"\x00"
if kind == INT_ARRAY:
return struct.pack(">i", len(value)) + b"".join(struct.pack(">i", v) for v in value)
if kind == LONG_ARRAY:
return struct.pack(">i", len(value)) + b"".join(struct.pack(">q", v) for v in value)
raise ValueError(f"unknown NBT tag type {kind}")
def load(path):
"""The root compound of a gzipped NBT file, as (name, compound dict)."""
reader = _Reader(gzip.decompress(path.read_bytes()))
if reader.unpack(">b") != COMPOUND:
raise ValueError(f"{path} doesn't start with a compound")
name = reader.string()
return name, reader.payload(COMPOUND)
def save(path, name, root):
path.write_bytes(gzip.compress(struct.pack(">b", COMPOUND) + _string(name) + _payload(COMPOUND, root)))