Expand voxel gameplay, lighting, full-height streaming and world imports
MVP checks / mvp (push) Canceled after 0s
MVP checks / mvp (push) Canceled after 0s
Add shared Rust/WASM physics, worker meshing and diagnostics, 64-chunk full-height streaming, atlas texture support, and baseline world import. Document the current implementation and include the supplied in-game lobby screenshot.
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Bundle selected pixel block textures with the local Shacraft packages.
|
||||
|
||||
Uses only the Python standard library. Source images are copied verbatim; this
|
||||
does not generate art, resample pixels, or infer distribution rights.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import struct
|
||||
import tempfile
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_TEXTURES = (
|
||||
"stone cobblestone oak_planks oak_log oak_log_top dirt grass_block_top "
|
||||
"grass_block_side sand gravel bricks stone_bricks glass oak_leaves "
|
||||
"diamond_ore iron_ore coal_ore gold_ore redstone_ore deepslate obsidian "
|
||||
"snow netherrack oak_door_bottom"
|
||||
).split()
|
||||
MARKER = ".shacraft-texture-bundle.json"
|
||||
|
||||
|
||||
def write_json(path, data):
|
||||
path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def resource(path, package, role):
|
||||
data = path.read_bytes()
|
||||
return {
|
||||
"path": path.relative_to(package).as_posix(),
|
||||
"scope": "client",
|
||||
"role": role,
|
||||
"size": len(data),
|
||||
"sha256": hashlib.sha256(data).hexdigest(),
|
||||
}
|
||||
|
||||
|
||||
def texture_bytes(path, pixel_size=32):
|
||||
data = path.read_bytes()
|
||||
if (
|
||||
len(data) < 33
|
||||
or data[:16] != b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR"
|
||||
or struct.unpack(">II", data[16:24]) != (pixel_size, pixel_size)
|
||||
):
|
||||
raise ValueError(f"Expected a real {pixel_size}x{pixel_size} PNG: {path}")
|
||||
if len(data) > 16 * 1024 * 1024:
|
||||
raise ValueError(f"Texture exceeds the server resource limit: {path}")
|
||||
return data
|
||||
|
||||
|
||||
def prepare(args):
|
||||
source = args.source.resolve()
|
||||
output = args.output.resolve()
|
||||
if source == output or source.is_relative_to(output):
|
||||
raise ValueError("The output must not contain the source pack.")
|
||||
if output == ROOT / "packages" or (ROOT / "packages").is_relative_to(output):
|
||||
raise ValueError("The output must not replace the bundled core packages.")
|
||||
if output.exists():
|
||||
if not args.overwrite:
|
||||
raise ValueError("Output already exists; use --overwrite to rebuild it.")
|
||||
marker = output / MARKER
|
||||
if not marker.is_file() or json.loads(marker.read_text()).get("generator") != "prepare_texture_pack.py":
|
||||
raise ValueError("Refusing to replace a directory not created by this script.")
|
||||
if not re.fullmatch(r"[a-z0-9_.-]+", args.id):
|
||||
raise ValueError("Package id must use lowercase letters, digits, '.', '_' or '-'.")
|
||||
if args.id in {"shacraft.base", "shacraft.trampoline"}:
|
||||
raise ValueError("The texture package needs its own id.")
|
||||
if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", args.version):
|
||||
raise ValueError("Package version must contain three numeric components.")
|
||||
if not args.name.strip() or len(args.name) > 120:
|
||||
raise ValueError("Texture pack name must contain 1 to 120 characters.")
|
||||
if not args.license or len(args.license) > 256 or any(ord(c) < 32 for c in args.license):
|
||||
raise ValueError("Supply a nonempty license or local-use notice (at most 256 characters).")
|
||||
names = args.textures or DEFAULT_TEXTURES
|
||||
if not 1 <= len(names) <= 127 or len(set(names)) != len(names):
|
||||
raise ValueError("Choose 1 to 127 unique texture names.")
|
||||
if any(not re.fullmatch(r"[a-z0-9_]+", name) for name in names):
|
||||
raise ValueError("Texture names must be plain Minecraft block texture names.")
|
||||
source_textures = source / "assets/minecraft/textures/block"
|
||||
# Validate every selected source before touching an existing output bundle.
|
||||
textures = [(name, texture_bytes(source_textures / f"{name}.png", args.pixel_size)) for name in names]
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
stage = Path(tempfile.mkdtemp(prefix=f".{output.name}-", dir=output.parent))
|
||||
backup = None
|
||||
try:
|
||||
for name in ("base", "trampoline"):
|
||||
shutil.copytree(ROOT / "packages" / name, stage / name)
|
||||
package = stage / f"pixel{args.pixel_size}"
|
||||
images = package / "client/textures"
|
||||
images.mkdir(parents=True)
|
||||
entries, resources = [], []
|
||||
for name, data in textures:
|
||||
path = images / f"{name}.png"
|
||||
path.write_bytes(data)
|
||||
entries.append({"name": name, "path": path.relative_to(package).as_posix()})
|
||||
resources.append(resource(path, package, "texture"))
|
||||
style = package / "client/style.json"
|
||||
write_json(style, {"schema": 1, "texture_pack": {
|
||||
"name": args.name, "pixel_size": args.pixel_size, "textures": entries,
|
||||
}})
|
||||
resources.append(resource(style, package, "client-style"))
|
||||
base_manifest = json.loads((stage / "base/manifest.json").read_text())
|
||||
write_json(package / "manifest.json", {
|
||||
"schema": 1, "id": args.id, "version": args.version,
|
||||
"license": args.license,
|
||||
"dependencies": [{"id": base_manifest["id"], "version": base_manifest["version"]}],
|
||||
"capabilities": ["client.texture", "client.style"],
|
||||
"resources": resources,
|
||||
})
|
||||
write_json(stage / MARKER, {
|
||||
"generator": "prepare_texture_pack.py", "schema": 1,
|
||||
"package": args.id, "name": args.name, "textures": len(entries), "pixel_size": args.pixel_size,
|
||||
"texture_bytes": sum(len(data) for _, data in textures),
|
||||
})
|
||||
if output.exists():
|
||||
backup = Path(tempfile.mkdtemp(prefix=f".{output.name}-old-", dir=output.parent))
|
||||
backup.rmdir()
|
||||
output.rename(backup)
|
||||
try:
|
||||
stage.rename(output)
|
||||
except OSError:
|
||||
if backup is not None:
|
||||
backup.rename(output)
|
||||
backup = None
|
||||
raise
|
||||
if backup is not None:
|
||||
shutil.rmtree(backup)
|
||||
print(f"Prepared {len(entries)} textures ({sum(len(data) for _, data in textures):,} PNG bytes)")
|
||||
print(f"Package bundle: {output}")
|
||||
finally:
|
||||
if stage.exists():
|
||||
shutil.rmtree(stage)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--source", type=Path, required=True, help="Unpacked Minecraft-format resource pack")
|
||||
parser.add_argument("--output", type=Path, required=True, help="Output bundle for the server --packages option")
|
||||
parser.add_argument("--name", default="Shacraft Pixel32 Study")
|
||||
parser.add_argument("--id", default="shacraft.pixel32.study")
|
||||
parser.add_argument("--version", default="1.0.0")
|
||||
parser.add_argument("--pixel-size", type=int, choices=(16, 32, 64, 128), default=32,
|
||||
help="Required native PNG dimensions (default: 32)")
|
||||
parser.add_argument("--license", default="Local reference-derived study; redistribution rights not granted")
|
||||
parser.add_argument("--textures", nargs="+", help="Selected texture names without .png (defaults to the 24-texture study)")
|
||||
parser.add_argument("--overwrite", action="store_true", help="Replace a bundle previously created by this script")
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
prepare(args)
|
||||
except (OSError, ValueError) as error:
|
||||
parser.exit(1, f"Texture pack preparation failed: {error}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user