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>
This commit is contained in:
1 parent
0877a6a179
commit
c2a87f178e
17 files changed
+558
-27
No files matched your search
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Make the machine test world (Phase 6; Adrian, 2026-11-02): a superflat creative world, cheats on, with every
|
||||
machine built by `/cobblesurvival machines showcase` (working with a Pokémon inside, empty, and the bare core),
|
||||
no wild Pokémon spawning and fixed daytime. It lands in build/machine-world; with --prism it is also copied into the
|
||||
Prism instance as the save "Machine Test (Phase 6)", replacing an older copy.
|
||||
|
||||
Needs the test server (tools/build.py first; the EULA accepted as for the other tools).
|
||||
|
||||
Usage: tools/make_machine_world.py [--prism] [--instance NAME]
|
||||
"""
|
||||
import argparse
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import nbtfile # noqa: E402
|
||||
from make_world import SKIP # noqa: E402
|
||||
from mcserver import BUILD, SERVER, Server, eula_accepted, prepare, scan_log # noqa: E402
|
||||
from prism_instance import PRISM_DIRS # noqa: E402
|
||||
|
||||
LEVEL = "machines"
|
||||
NAME = "Machine Test (Phase 6)"
|
||||
OUT = BUILD / "machine-world"
|
||||
FLAT = ('{"layers":[{"block":"minecraft:bedrock","height":1},{"block":"minecraft:stone","height":3},'
|
||||
'{"block":"minecraft:grass_block","height":1}],"biome":"minecraft:plains"}')
|
||||
# Ground is y -60; the showcase starts at the first air above it.
|
||||
ORIGIN = (0, -59, 0)
|
||||
SPAWN = (24, -59, 8)
|
||||
COMMANDS = [
|
||||
"gamerule doPokemonSpawning false",
|
||||
"gamerule doDaylightCycle false",
|
||||
"gamerule doWeatherCycle false",
|
||||
"time set day",
|
||||
f"setworldspawn {SPAWN[0]} {SPAWN[1]} {SPAWN[2]}",
|
||||
f"execute positioned {ORIGIN[0]} {ORIGIN[1]} {ORIGIN[2]} run cobblesurvival machines showcase",
|
||||
]
|
||||
|
||||
|
||||
def make_creative(level_dat):
|
||||
"""Creative mode, cheats on, and a name the world list can tell apart."""
|
||||
name, root = nbtfile.load(level_dat)
|
||||
data = root["Data"][1]
|
||||
data["LevelName"] = (nbtfile.STRING, NAME)
|
||||
data["GameType"] = (nbtfile.INT, 1)
|
||||
data["allowCommands"] = (nbtfile.BYTE, 1)
|
||||
nbtfile.save(level_dat, name, root)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument("--prism", action="store_true", help="copy the world into the Prism instance")
|
||||
parser.add_argument("--instance", default="Cobblemon Survival Pack (dev)")
|
||||
args = parser.parse_args()
|
||||
if not eula_accepted():
|
||||
sys.exit("build/server/eula.txt does not accept the Minecraft EULA yet (see tools/selftest.py --accept-eula).")
|
||||
|
||||
args_file = prepare()
|
||||
world = SERVER / LEVEL
|
||||
shutil.rmtree(world, ignore_errors=True)
|
||||
props = {"level-name": LEVEL, "level-type": "minecraft:flat", "generator-settings": FLAT, "gamemode": "creative"}
|
||||
with Server(args_file, props, stdout_name="machine-world-stdout.log") as server:
|
||||
print(f"server started in {server.wait_ready():.0f}s")
|
||||
for command in COMMANDS:
|
||||
print(f" {command}: {server.command(command).strip()}")
|
||||
server.command("save-all flush")
|
||||
server.stop()
|
||||
problems = scan_log(server.log)
|
||||
if problems:
|
||||
sys.exit("server log problems:\n" + "\n".join(f" - {p}" for p in problems))
|
||||
|
||||
shutil.rmtree(OUT, ignore_errors=True)
|
||||
shutil.copytree(world, OUT, ignore=SKIP)
|
||||
make_creative(OUT / "level.dat")
|
||||
print(f"machine world: {OUT.relative_to(BUILD.parent)}")
|
||||
|
||||
if args.prism:
|
||||
prism = next((d for d in PRISM_DIRS if d.exists()), None)
|
||||
if prism is None:
|
||||
sys.exit("no Prism Launcher data folder found")
|
||||
saves = prism / "instances" / args.instance / "minecraft" / "saves"
|
||||
if not saves.parent.exists():
|
||||
sys.exit(f"no Prism instance {args.instance!r}; run tools/prism_instance.py first")
|
||||
target = saves / NAME
|
||||
shutil.rmtree(target, ignore_errors=True)
|
||||
shutil.copytree(OUT, target)
|
||||
print(f"installed as the save {NAME!r} in {args.instance}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,85 @@
|
||||
"""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)))
|
||||
Reference in new issue
Block a user