From 500d00113c9ec80d32b3d2a12ab3ad13864f50f5 Mon Sep 17 00:00:00 2001 From: zerodancing Date: Thu, 3 Sep 2026 21:07:54 +0700 Subject: [PATCH 1/6] Add temporary MCM staging importer --- .github/workflows/import-mcm-staging.yml | 102 +++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 .github/workflows/import-mcm-staging.yml diff --git a/.github/workflows/import-mcm-staging.yml b/.github/workflows/import-mcm-staging.yml new file mode 100644 index 0000000..697a962 --- /dev/null +++ b/.github/workflows/import-mcm-staging.yml @@ -0,0 +1,102 @@ +name: Import staged MCM payload + +on: + push: + branches: + - mcm-upload-staging + paths: + - '.mcm_upload/READY' + +permissions: + contents: write + +jobs: + import: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - name: Check out staging branch + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Decode and extract payload + shell: python + run: | + import hashlib + import shutil + import tarfile + from pathlib import Path, PurePosixPath + + payload_dir = Path('.mcm_upload') + chunks = sorted(payload_dir.glob('chunk_*.txt')) + if len(chunks) != 28: + raise SystemExit(f'Expected 28 chunks, found {len(chunks)}') + + text = ''.join(path.read_text(encoding='utf-8') for path in chunks) + if len(text) != 274552: + raise SystemExit(f'Encoded character count mismatch: {len(text)}') + if len(text) % 2: + raise SystemExit('Encoded payload has an odd character count') + + decoded = bytearray() + for index in range(0, len(text), 2): + hi = ord(text[index]) - 0x10000 + lo = ord(text[index + 1]) - 0x10000 + if not (0 <= hi <= 0xFFFFF and 0 <= lo <= 0xFFFFF): + raise SystemExit(f'Invalid encoded character at pair {index // 2}') + decoded.extend(((hi << 20) | lo).to_bytes(5, 'big')) + + data = bytes(decoded[:686376]) + digest = hashlib.sha256(data).hexdigest() + expected = 'f58feb299c14f592482c40e617910bb66c68f0fa50672c729a038f97dbc6187f' + if digest != expected: + raise SystemExit(f'Payload SHA-256 mismatch: {digest}') + + archive = Path('/tmp/mcm_payload.tar.xz') + archive.write_bytes(data) + + with tarfile.open(archive, 'r:xz') as tf: + members = tf.getmembers() + roots = set() + for member in members: + path = PurePosixPath(member.name) + if path.is_absolute() or '..' in path.parts: + raise SystemExit(f'Unsafe archive path: {member.name}') + if path.parts: + roots.add(path.parts[0]) + if roots != {'metamorph_creative_menu'}: + raise SystemExit(f'Unexpected archive root: {sorted(roots)}') + + target = Path('metamorph_creative_menu') + if target.exists(): + shutil.rmtree(target) + tf.extractall('.') + + required = [ + Path('metamorph_creative_menu/mod.xml'), + Path('metamorph_creative_menu/mod_id.txt'), + Path('metamorph_creative_menu/init.lua'), + Path('metamorph_creative_menu/NoitaPatcher/noitapatcher.dll'), + ] + missing = [str(path) for path in required if not path.is_file()] + if missing: + raise SystemExit('Missing required files: ' + ', '.join(missing)) + + - name: Verify exact mod tree + shell: bash + run: | + git add -A metamorph_creative_menu + root_tree="$(git write-tree)" + mod_tree="$(git ls-tree "$root_tree" metamorph_creative_menu | awk '{print $3}')" + test "$mod_tree" = "8d856676dd91030b1fd51f69e841d17014ef8b61" + + - name: Commit imported mod + shell: bash + run: | + rm -rf .mcm_upload + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + git commit -m "Stage exact Metamorph Creative Menu archive" + git push From 69631bc013c03330a3483e3842906cb737000998 Mon Sep 17 00:00:00 2001 From: zerodancing Date: Thu, 3 Sep 2026 21:17:46 +0700 Subject: [PATCH 2/6] Use compact delta payload for staging import --- .github/workflows/import-mcm-staging.yml | 47 ++++++++++-------------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/.github/workflows/import-mcm-staging.yml b/.github/workflows/import-mcm-staging.yml index 697a962..6314d67 100644 --- a/.github/workflows/import-mcm-staging.yml +++ b/.github/workflows/import-mcm-staging.yml @@ -20,7 +20,7 @@ jobs: with: fetch-depth: 0 - - name: Decode and extract payload + - name: Verify and apply compact payload shell: python run: | import hashlib @@ -28,33 +28,26 @@ jobs: import tarfile from pathlib import Path, PurePosixPath - payload_dir = Path('.mcm_upload') - chunks = sorted(payload_dir.glob('chunk_*.txt')) - if len(chunks) != 28: - raise SystemExit(f'Expected 28 chunks, found {len(chunks)}') - - text = ''.join(path.read_text(encoding='utf-8') for path in chunks) - if len(text) != 274552: - raise SystemExit(f'Encoded character count mismatch: {len(text)}') - if len(text) % 2: - raise SystemExit('Encoded payload has an odd character count') - - decoded = bytearray() - for index in range(0, len(text), 2): - hi = ord(text[index]) - 0x10000 - lo = ord(text[index + 1]) - 0x10000 - if not (0 <= hi <= 0xFFFFF and 0 <= lo <= 0xFFFFF): - raise SystemExit(f'Invalid encoded character at pair {index // 2}') - decoded.extend(((hi << 20) | lo).to_bytes(5, 'big')) - - data = bytes(decoded[:686376]) + archive = Path('.mcm_upload/payload.tar.xz') + data = archive.read_bytes() + if len(data) != 387280: + raise SystemExit(f'Payload size mismatch: {len(data)}') digest = hashlib.sha256(data).hexdigest() - expected = 'f58feb299c14f592482c40e617910bb66c68f0fa50672c729a038f97dbc6187f' + expected = '47a8e0c29919f5781244d70b84074be5a99b358a9e5bc5266a0d7c564421d641' if digest != expected: raise SystemExit(f'Payload SHA-256 mismatch: {digest}') - archive = Path('/tmp/mcm_payload.tar.xz') - archive.write_bytes(data) + replace_paths = [ + Path('metamorph_creative_menu/tests'), + Path('metamorph_creative_menu/files/features/forms'), + Path('metamorph_creative_menu/files/features/death_recovery'), + Path('metamorph_creative_menu/native_src'), + ] + for target in replace_paths: + if target.is_dir(): + shutil.rmtree(target) + elif target.exists(): + target.unlink() with tarfile.open(archive, 'r:xz') as tf: members = tf.getmembers() @@ -67,10 +60,6 @@ jobs: roots.add(path.parts[0]) if roots != {'metamorph_creative_menu'}: raise SystemExit(f'Unexpected archive root: {sorted(roots)}') - - target = Path('metamorph_creative_menu') - if target.exists(): - shutil.rmtree(target) tf.extractall('.') required = [ @@ -78,6 +67,7 @@ jobs: Path('metamorph_creative_menu/mod_id.txt'), Path('metamorph_creative_menu/init.lua'), Path('metamorph_creative_menu/NoitaPatcher/noitapatcher.dll'), + Path('metamorph_creative_menu/mcm_native_gameover.dll'), ] missing = [str(path) for path in required if not path.is_file()] if missing: @@ -89,6 +79,7 @@ jobs: git add -A metamorph_creative_menu root_tree="$(git write-tree)" mod_tree="$(git ls-tree "$root_tree" metamorph_creative_menu | awk '{print $3}')" + echo "mod tree: $mod_tree" test "$mod_tree" = "8d856676dd91030b1fd51f69e841d17014ef8b61" - name: Commit imported mod From e0c4b06be7265f9e190e902ec5b569becc572237 Mon Sep 17 00:00:00 2001 From: zerodancing Date: Thu, 3 Sep 2026 21:18:16 +0700 Subject: [PATCH 3/6] Read staged payload from binary chunks --- .github/workflows/import-mcm-staging.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/import-mcm-staging.yml b/.github/workflows/import-mcm-staging.yml index 6314d67..fb5167a 100644 --- a/.github/workflows/import-mcm-staging.yml +++ b/.github/workflows/import-mcm-staging.yml @@ -28,14 +28,19 @@ jobs: import tarfile from pathlib import Path, PurePosixPath - archive = Path('.mcm_upload/payload.tar.xz') - data = archive.read_bytes() + payload_dir = Path('.mcm_upload') + chunks = sorted(payload_dir.glob('chunk_*.bin')) + if len(chunks) != 10: + raise SystemExit(f'Expected 10 chunks, found {len(chunks)}') + data = b''.join(path.read_bytes() for path in chunks) if len(data) != 387280: raise SystemExit(f'Payload size mismatch: {len(data)}') digest = hashlib.sha256(data).hexdigest() expected = '47a8e0c29919f5781244d70b84074be5a99b358a9e5bc5266a0d7c564421d641' if digest != expected: raise SystemExit(f'Payload SHA-256 mismatch: {digest}') + archive = Path('/tmp/mcm_delta.tar.xz') + archive.write_bytes(data) replace_paths = [ Path('metamorph_creative_menu/tests'), From a3e94e2811fc9fbce4be86fae9ac8cfe39a9e88e Mon Sep 17 00:00:00 2001 From: zerodancing Date: Thu, 3 Sep 2026 21:19:33 +0700 Subject: [PATCH 4/6] Split staging payload into smaller binary chunks --- .github/workflows/import-mcm-staging.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/import-mcm-staging.yml b/.github/workflows/import-mcm-staging.yml index fb5167a..c3d362e 100644 --- a/.github/workflows/import-mcm-staging.yml +++ b/.github/workflows/import-mcm-staging.yml @@ -30,8 +30,8 @@ jobs: payload_dir = Path('.mcm_upload') chunks = sorted(payload_dir.glob('chunk_*.bin')) - if len(chunks) != 10: - raise SystemExit(f'Expected 10 chunks, found {len(chunks)}') + if len(chunks) != 40: + raise SystemExit(f'Expected 40 chunks, found {len(chunks)}') data = b''.join(path.read_bytes() for path in chunks) if len(data) != 387280: raise SystemExit(f'Payload size mismatch: {len(data)}') From d9d1828ec1f9a11995cccd66de8d47eb0d934d3c Mon Sep 17 00:00:00 2001 From: zerodancing Date: Thu, 3 Sep 2026 21:24:18 +0700 Subject: [PATCH 5/6] Decode staged payload from base64 chunks --- .github/workflows/import-mcm-staging.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/import-mcm-staging.yml b/.github/workflows/import-mcm-staging.yml index c3d362e..182eea4 100644 --- a/.github/workflows/import-mcm-staging.yml +++ b/.github/workflows/import-mcm-staging.yml @@ -23,16 +23,18 @@ jobs: - name: Verify and apply compact payload shell: python run: | + import base64 import hashlib import shutil import tarfile from pathlib import Path, PurePosixPath payload_dir = Path('.mcm_upload') - chunks = sorted(payload_dir.glob('chunk_*.bin')) + chunks = sorted(payload_dir.glob('chunk_*.txt')) if len(chunks) != 40: raise SystemExit(f'Expected 40 chunks, found {len(chunks)}') - data = b''.join(path.read_bytes() for path in chunks) + encoded = ''.join(path.read_text(encoding='ascii') for path in chunks) + data = base64.b64decode(encoded, validate=True) if len(data) != 387280: raise SystemExit(f'Payload size mismatch: {len(data)}') digest = hashlib.sha256(data).hexdigest() From fd2ecd44ea8ee62e98fa1f92575f83d1eaab7509 Mon Sep 17 00:00:00 2001 From: zerodancing Date: Thu, 3 Sep 2026 21:34:48 +0700 Subject: [PATCH 6/6] Prepare staged import for version 3.0.0 --- .github/workflows/import-mcm-staging.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/import-mcm-staging.yml b/.github/workflows/import-mcm-staging.yml index 182eea4..d8e7937 100644 --- a/.github/workflows/import-mcm-staging.yml +++ b/.github/workflows/import-mcm-staging.yml @@ -35,10 +35,10 @@ jobs: raise SystemExit(f'Expected 40 chunks, found {len(chunks)}') encoded = ''.join(path.read_text(encoding='ascii') for path in chunks) data = base64.b64decode(encoded, validate=True) - if len(data) != 387280: + if len(data) != 393532: raise SystemExit(f'Payload size mismatch: {len(data)}') digest = hashlib.sha256(data).hexdigest() - expected = '47a8e0c29919f5781244d70b84074be5a99b358a9e5bc5266a0d7c564421d641' + expected = '77f1c9901ed37f3eb8fde64b949ffb75473bcd84fae7c39ebd7678b9fee4945a' if digest != expected: raise SystemExit(f'Payload SHA-256 mismatch: {digest}') archive = Path('/tmp/mcm_delta.tar.xz') @@ -87,7 +87,7 @@ jobs: root_tree="$(git write-tree)" mod_tree="$(git ls-tree "$root_tree" metamorph_creative_menu | awk '{print $3}')" echo "mod tree: $mod_tree" - test "$mod_tree" = "8d856676dd91030b1fd51f69e841d17014ef8b61" + test "$mod_tree" = "768f71c49d39f9e79b3718e444ad41d9d05ce37d" - name: Commit imported mod shell: bash @@ -96,5 +96,5 @@ jobs: git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add -A - git commit -m "Stage exact Metamorph Creative Menu archive" + git commit -m "Stage exact Metamorph Creative Menu 3.0.0 archive" git push