Remaster thirteen original sprite and terrain pictures
This commit is contained in:
1 parent
f3b30df0f8
commit
05a2eae96b
148 files changed
+330320
-54
No files matched your search
@@ -0,0 +1,151 @@
|
||||
"""Reproduce a complete SHIP20 family using reviewed RGB and Original-tip steps.
|
||||
|
||||
This is a portable dispatcher, not a new pixel algorithm. It calls the unchanged
|
||||
DSHP complete-family renderer and functional Original retention helper in memory.
|
||||
All paths in the geometry recipe resolve against the outer recipe's directory.
|
||||
No artwork approval, engine operation or registration is performed.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image, __version__ as PILLOW_VERSION
|
||||
import hd_other_functional_cleanup as functional
|
||||
|
||||
METHOD = "derived-SHIP20-complete4-reviewed-generated-RGB-and-Original-tips-v1"
|
||||
FUNCTIONAL_SHA = "f8078284e300947aec4d2c9e70cbc7198fa3cb5d34de6b36c07cacc5f3820a84"
|
||||
ASSETS = ["200c4", "200c5", "200c6", "200c7"]
|
||||
|
||||
|
||||
def digest(payload):
|
||||
return hashlib.sha256(payload).hexdigest()
|
||||
|
||||
|
||||
def dependency_pins():
|
||||
path = Path(functional.__file__).resolve()
|
||||
if digest(path.read_bytes()) != FUNCTIONAL_SHA:
|
||||
raise ValueError("functional Original implementation changed")
|
||||
return {"functional_Original_tool": {"path": "tools/hd_other_functional_cleanup.py",
|
||||
"sha256": FUNCTIONAL_SHA},
|
||||
**functional.dependency_pins()}
|
||||
|
||||
|
||||
def run_recipe(recipe_path, output):
|
||||
recipe_path, output = Path(recipe_path).resolve(), Path(output)
|
||||
if output.exists():
|
||||
raise ValueError("fresh absent output directory required")
|
||||
dependencies = dependency_pins()
|
||||
rb = recipe_path.read_bytes()
|
||||
recipe = json.loads(rb)
|
||||
if (type(recipe) is not dict or set(recipe) != {
|
||||
"format_version", "method", "geometry_recipe", "functional_original_cells",
|
||||
"expected_output_sha256"}
|
||||
or type(recipe["format_version"]) is not int or recipe["format_version"] != 1
|
||||
or recipe["method"] != METHOD):
|
||||
raise ValueError("exact SHIP20 dispatcher recipe fields/version required")
|
||||
geometry = recipe["geometry_recipe"]
|
||||
frames = geometry.get("frames") if type(geometry) is dict else None
|
||||
if (type(frames) is not list or len(frames) != 4
|
||||
or any(type(f) is not dict for f in frames)
|
||||
or [f.get("asset") for f in frames] != ASSETS):
|
||||
raise ValueError("complete4 exact SHIP20 frame order required")
|
||||
selected = recipe["functional_original_cells"]
|
||||
expected = recipe["expected_output_sha256"]
|
||||
if (type(selected) is not dict or set(selected) != set(ASSETS)
|
||||
or type(expected) is not dict or set(expected) != set(ASSETS)
|
||||
or any(type(v) is not str or len(v) != 64
|
||||
or any(c not in "0123456789abcdef" for c in v)
|
||||
for v in expected.values())):
|
||||
raise ValueError("complete4 functional cells and expected PNG hashes required")
|
||||
|
||||
# Validate the complete selection before invoking the existing renderer.
|
||||
sources = []
|
||||
for frame in frames:
|
||||
_, sb = functional.base.checked(frame["source"], recipe_path.parent)
|
||||
w, h, cells = functional.base._indices(sb)
|
||||
if (w, h) != (48, 56) or frame.get("origin") != [0, 0]:
|
||||
raise ValueError("exact native48x56/origin0 SHIP20 canvas required")
|
||||
source = np.asarray(cells, np.uint16).reshape(h, w)
|
||||
points = selected[frame["asset"]]
|
||||
if (type(points) is not list or len(points) > 64
|
||||
or any(type(xy) is not list or len(xy) != 2
|
||||
or any(type(v) is not int for v in xy)
|
||||
or not 0 <= xy[0] < w or not 0 <= xy[1] < h
|
||||
or int(source[xy[1], xy[0]]) not in (82, 83, 84)
|
||||
for xy in points)
|
||||
or points != sorted(points)
|
||||
or len({tuple(xy) for xy in points}) != len(points)):
|
||||
raise ValueError("sorted unique painted Source yellow82..84 cells required")
|
||||
sources.append(source)
|
||||
|
||||
arrays, geometry_report = functional.base.render_family(geometry, recipe_path.parent)
|
||||
_, pb = functional.base.checked(geometry["palette"], recipe_path.parent)
|
||||
p6 = np.frombuffer(pb, np.uint8).reshape(256, 3).astype(np.uint16)
|
||||
palette = ((p6 << 2) | (p6 >> 4)).astype(np.uint8)
|
||||
payloads, reports = [], []
|
||||
for frame, source, rgba in zip(frames, sources, arrays):
|
||||
asset = frame["asset"]
|
||||
points = selected[asset]
|
||||
if points:
|
||||
out, retention = functional.retain_functional_original(rgba, source, palette, points)
|
||||
if not np.array_equal(out[:, :, 3], rgba[:, :, 3]):
|
||||
raise ValueError("functional selection changed SourceScale4 alpha")
|
||||
else:
|
||||
out = rgba
|
||||
retention = {"cells": [], "actual_Original_Scale4_RGBA_samples": 0,
|
||||
"outside_explicit_footprints_RGBA_exact": True}
|
||||
encoded = io.BytesIO()
|
||||
Image.fromarray(out).save(encoded, format="PNG", compress_level=9)
|
||||
png = encoded.getvalue()
|
||||
if digest(png) != expected[asset]:
|
||||
raise ValueError(f"reviewed PNG not reproduced: {asset}")
|
||||
payloads.append((asset, png))
|
||||
reports.append({"asset": asset, "functional_Original": retention,
|
||||
"sha256": digest(png), "RGBA_sha256": digest(out.tobytes())})
|
||||
|
||||
# Reject changed input/implementation bytes before the first output write.
|
||||
if recipe_path.read_bytes() != rb or dependency_pins() != dependencies:
|
||||
raise ValueError("recipe or implementation changed during reconstruction")
|
||||
for frame in frames:
|
||||
functional.base.checked(frame["source"], recipe_path.parent)
|
||||
functional.base.checked(frame["raw_png"], recipe_path.parent)
|
||||
functional.base.checked(geometry["palette"], recipe_path.parent)
|
||||
execution = {"status": "success", "method": METHOD,
|
||||
"recipe_sha256": digest(rb), "tool_sha256": digest(Path(__file__).read_bytes()),
|
||||
"source_sha256": {f["asset"]: f["source"]["sha256"] for f in frames},
|
||||
"parent_raw_sha256": sorted({f["raw_png"]["sha256"] for f in frames}),
|
||||
"output_sha256": {a: digest(b) for a, b in payloads}}
|
||||
report = {"execution": execution, "dependencies": dependencies,
|
||||
"geometry": geometry_report, "frames": reports,
|
||||
"Original_tip_cells_are_not_generated_HD": True,
|
||||
"actual_dispatcher_calls": 1, "actual_geometry_renderer_calls": 1,
|
||||
"actual_pure_retention_calls": sum(bool(selected[a]) for a in ASSETS),
|
||||
"python_version": sys.version, "numpy_version": np.__version__,
|
||||
"pillow_version": PILLOW_VERSION,
|
||||
"scope": "Portable byte-exact replay only; no new art/engine/QA/context approval."}
|
||||
output.mkdir(parents=True)
|
||||
for asset, png in payloads:
|
||||
with (output / f"{asset}-derived.png").open("xb") as stream:
|
||||
stream.write(png)
|
||||
with (output / "derived-provenance.json").open("x") as stream:
|
||||
json.dump(report, stream, indent=2, sort_keys=True)
|
||||
stream.write("\n")
|
||||
return report
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("recipe", type=Path)
|
||||
parser.add_argument("fresh_output", type=Path)
|
||||
args = parser.parse_args()
|
||||
print(json.dumps(run_recipe(args.recipe, args.fresh_output)["execution"], sort_keys=True))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,146 @@
|
||||
"""Synthetic dispatcher/portable-lineage tests; no real assets or engine calls."""
|
||||
import copy
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
spec = importlib.util.spec_from_file_location("ship_cleanup", Path(__file__).with_name("hd_ship20_cleanup.py"))
|
||||
c = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(c)
|
||||
|
||||
|
||||
class PortableShipDispatcherTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
self.root = Path(self.tmp.name)
|
||||
self.assets = c.ASSETS
|
||||
self.cells = dict(zip(self.assets, [[], [[14, 25]], [[25, 19]], [[36, 23]]]))
|
||||
y, x = np.indices((224, 192))
|
||||
rgba = np.empty((224, 192, 4), np.uint8)
|
||||
rgba[:, :, :3] = np.stack([(x * 3 + y) % 256, (x + y * 5) % 256,
|
||||
(x * 7 + y * 11) % 256], axis=-1)
|
||||
rgba[:, :, 3] = 255
|
||||
Image.fromarray(rgba).save(self.root / "parent.png")
|
||||
self.palette_bytes = bytes((np.arange(768) % 64).astype(np.uint8))
|
||||
(self.root / "palette").write_bytes(self.palette_bytes)
|
||||
frames = []
|
||||
for asset in self.assets:
|
||||
source = np.full((56, 48), 10, np.uint16)
|
||||
for xx, yy in self.cells[asset]:
|
||||
source[yy, xx] = 82
|
||||
payload = bytearray(struct.pack("<5i", 0, 1, 31, 48, 56))
|
||||
for yy in range(56):
|
||||
payload.extend(struct.pack("<4i", 0, yy, yy * 48, 48))
|
||||
payload.extend(bytes(source[yy].tolist()))
|
||||
payload.extend(struct.pack("<4i", 0, 0, -1, 0))
|
||||
(self.root / asset).write_bytes(payload)
|
||||
frames.append(dict(asset=asset, source=self.bind(asset), raw_png=self.bind("parent.png"),
|
||||
viewport=[0, 0, 192, 224], sampling_offset_native=[0, 0], origin=[0, 0]))
|
||||
self.geometry = dict(format_version=1, output_scale=4, max_fill_distance_pixels=12,
|
||||
shared_backing_anchor=1, palette=self.bind("palette"), frames=frames)
|
||||
# Independent sequential reference: published geometry stage, then the
|
||||
# unchanged pure Original stage; PNG compression matches the actual CLI.
|
||||
arrays, _ = c.functional.base.render_family(self.geometry, self.root)
|
||||
self.reference = {}
|
||||
for f, array in zip(frames, arrays):
|
||||
_, sb = c.functional.base.checked(f["source"], self.root)
|
||||
w, h, indices = c.functional.base._indices(sb)
|
||||
if self.cells[f["asset"]]:
|
||||
p6 = np.frombuffer(self.palette_bytes, np.uint8).reshape(256, 3).astype(np.uint16)
|
||||
palette = ((p6 << 2) | (p6 >> 4)).astype(np.uint8)
|
||||
array, _ = c.functional.retain_functional_original(
|
||||
array, np.asarray(indices, np.uint16).reshape(h, w), palette, self.cells[f["asset"]])
|
||||
buffer = io.BytesIO()
|
||||
Image.fromarray(array).save(buffer, format="PNG", compress_level=9)
|
||||
self.reference[f["asset"]] = buffer.getvalue()
|
||||
self.recipe = dict(format_version=1, method=c.METHOD, geometry_recipe=self.geometry,
|
||||
functional_original_cells=self.cells,
|
||||
expected_output_sha256={a: c.digest(b) for a, b in self.reference.items()})
|
||||
|
||||
def bind(self, name):
|
||||
return dict(path=name, sha256=hashlib.sha256((self.root / name).read_bytes()).hexdigest())
|
||||
|
||||
def recipe_path(self, recipe=None):
|
||||
path = self.root / "recipe.json"
|
||||
path.write_text(json.dumps(self.recipe if recipe is None else recipe))
|
||||
return path
|
||||
|
||||
def test_relative_recipe_from_other_cwd_reproduces_sequential_bytes_and_receipt(self):
|
||||
rp = self.recipe_path()
|
||||
before = {p: p.read_bytes() for p in self.root.iterdir()}
|
||||
old_cwd = Path.cwd()
|
||||
try:
|
||||
os.chdir("/")
|
||||
report = c.run_recipe(rp, self.root / "actual")
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
for asset, expected in self.reference.items():
|
||||
self.assertEqual((self.root / "actual" / (asset + "-derived.png")).read_bytes(), expected)
|
||||
self.assertEqual(report["execution"]["tool_sha256"], c.digest(Path(c.__file__).read_bytes()))
|
||||
self.assertEqual(report["execution"]["parent_raw_sha256"], [self.bind("parent.png")["sha256"]])
|
||||
self.assertEqual(report["actual_geometry_renderer_calls"], 1)
|
||||
self.assertEqual(report["actual_pure_retention_calls"], 3)
|
||||
self.assertEqual(report["frames"][0]["functional_Original"]["actual_Original_Scale4_RGBA_samples"], 0)
|
||||
self.assertIn("pillow_version", report)
|
||||
self.assertIn("numpy_version", report)
|
||||
for p, value in before.items():
|
||||
self.assertEqual(p.read_bytes(), value)
|
||||
with self.assertRaisesRegex(ValueError, "fresh absent"):
|
||||
c.run_recipe(rp, self.root / "actual")
|
||||
|
||||
def test_wrong_expected_png_rejected_before_output_mkdir(self):
|
||||
r = copy.deepcopy(self.recipe)
|
||||
r["expected_output_sha256"]["200c7"] = "0" * 64
|
||||
with self.assertRaisesRegex(ValueError, "reviewed PNG not reproduced"):
|
||||
c.run_recipe(self.recipe_path(r), self.root / "absent")
|
||||
self.assertFalse((self.root / "absent").exists())
|
||||
|
||||
def test_wrong_order_duplicate_bool_unpainted_and_unsorted_cells_reject(self):
|
||||
cases = []
|
||||
r = copy.deepcopy(self.recipe)
|
||||
r["geometry_recipe"]["frames"].reverse()
|
||||
cases.append(r)
|
||||
for cells in ([[14, 25], [14, 25]], [[True, 25]], [[0, 0]], [[14, 26], [14, 25]]):
|
||||
r = copy.deepcopy(self.recipe)
|
||||
r["functional_original_cells"]["200c5"] = cells
|
||||
cases.append(r)
|
||||
for r in cases:
|
||||
with self.subTest(recipe=r), self.assertRaises(ValueError):
|
||||
c.run_recipe(self.recipe_path(r), self.root / "absent")
|
||||
self.assertFalse((self.root / "absent").exists())
|
||||
|
||||
def test_corrupt_declared_input_or_dependency_rejects_before_output(self):
|
||||
r = copy.deepcopy(self.recipe)
|
||||
r["geometry_recipe"]["frames"][0]["raw_png"]["sha256"] = "0" * 64
|
||||
with self.assertRaisesRegex(ValueError, "hash mismatch"):
|
||||
c.run_recipe(self.recipe_path(r), self.root / "absent")
|
||||
with patch.object(c, "FUNCTIONAL_SHA", "0" * 64), self.assertRaisesRegex(ValueError, "implementation changed"):
|
||||
c.run_recipe(self.recipe_path(), self.root / "absent")
|
||||
self.assertFalse((self.root / "absent").exists())
|
||||
|
||||
def test_retention_cannot_change_alpha(self):
|
||||
original = c.functional.retain_functional_original
|
||||
def wrong_alpha(*args):
|
||||
out, report = original(*args)
|
||||
out[0, 0, 3] = 0
|
||||
return out, report
|
||||
with patch.object(c.functional, "retain_functional_original", wrong_alpha), self.assertRaisesRegex(ValueError, "alpha"):
|
||||
c.run_recipe(self.recipe_path(), self.root / "absent")
|
||||
self.assertFalse((self.root / "absent").exists())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
Reference in new issue
Block a user