diff --git a/.github/workflows/import-mod-archive.yml b/.github/workflows/import-mod-archive.yml
deleted file mode 100644
index cc7319c..0000000
--- a/.github/workflows/import-mod-archive.yml
+++ /dev/null
@@ -1,73 +0,0 @@
-name: Import full MCM archive
-
-on:
- push:
- paths:
- - 'Metamorph_Creative_Menu_standalone_noitapatcher*.zip'
-
-permissions:
- contents: write
-
-jobs:
- import:
- runs-on: ubuntu-latest
- steps:
- - name: Check out repository
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
-
- - name: Import standalone archive
- shell: python
- run: |
- import glob
- import os
- import shutil
- import zipfile
- from pathlib import Path, PurePosixPath
-
- archives = sorted(glob.glob('Metamorph_Creative_Menu_standalone_noitapatcher*.zip'))
- if not archives:
- raise SystemExit('Standalone archive was not found')
- archive = archives[-1]
-
- with zipfile.ZipFile(archive) as zf:
- members = zf.infolist()
- for member in members:
- path = PurePosixPath(member.filename)
- if path.is_absolute() or '..' in path.parts:
- raise SystemExit(f'Unsafe archive path: {member.filename}')
- roots = {PurePosixPath(m.filename).parts[0] for m in members if PurePosixPath(m.filename).parts}
- if roots != {'metamorph_creative_menu'}:
- raise SystemExit(f'Unexpected archive root: {sorted(roots)}')
-
- target = Path('metamorph_creative_menu')
- if target.exists():
- shutil.rmtree(target)
- zf.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))
-
- for item in archives:
- os.remove(item)
-
- - name: Commit imported mod
- shell: bash
- run: |
- git config user.name "github-actions[bot]"
- git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- git add -A
- if git diff --cached --quiet; then
- echo "Archive contents already match the repository."
- exit 0
- fi
- git commit -m "Import full standalone Metamorph Creative Menu"
- git push
diff --git a/.github/workflows/publish-ready-build.yml b/.github/workflows/publish-ready-build.yml
deleted file mode 100644
index 3d4565c..0000000
--- a/.github/workflows/publish-ready-build.yml
+++ /dev/null
@@ -1,94 +0,0 @@
-name: Publish ready-to-install build
-
-on:
- push:
- branches: [main]
- workflow_dispatch:
-
-permissions:
- contents: write
-
-concurrency:
- group: publish-ready-build
- cancel-in-progress: true
-
-jobs:
- publish:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- with:
- fetch-depth: 0
-
- - name: Keep one-click download table in README
- shell: bash
- run: |
- python3 - <<'PY'
- from pathlib import Path
-
- path = Path('README.md')
- text = path.read_text(encoding='utf-8')
- if '## Download / Скачать' not in text:
- needle = '| 한국어 | [한국어](#ko) |\n\n'
- lines = [
- '| 한국어 | [한국어](#ko) |',
- '',
- '## Download / Скачать',
- '',
- '| Build / Сборка | Download / Скачать |',
- '|---|---|',
- '| **Ready-to-install / Готовая версия** | **[⬇️ Download ZIP / Скачать ZIP](https://github.com/zerodancing/Metamorph-Creative-Menu/releases/download/latest-build/Metamorph-Creative-Menu.zip)** |',
- '| Build page / Страница сборки | [Open / Открыть](https://github.com/zerodancing/Metamorph-Creative-Menu/releases/tag/latest-build) |',
- '',
- '> The ZIP already contains the `metamorph_creative_menu` folder. Extract/copy that folder directly into `Noita/mods/`. ',
- '> ZIP уже содержит папку `metamorph_creative_menu`. Распакуйте/скопируйте её прямо в `Noita/mods/`.',
- '',
- ]
- block = '\n'.join(lines)
- if needle not in text:
- raise SystemExit('Language table marker not found in README.md')
- text = text.replace(needle, block, 1)
- text = text.replace(
- '> ZIP уже содержит папку `metamorph_creative_menu`. Распакуйте/скопируйте её прямо в `Noita/mods/`.\n> Choose your language above.',
- '> ZIP уже содержит папку `metamorph_creative_menu`. Распакуйте/скопируйте её прямо в `Noita/mods/`.\n\n> Choose your language above.',
- 1,
- )
- path.write_text(text, encoding='utf-8')
- PY
-
- if ! git diff --quiet -- README.md; then
- git config user.name "github-actions[bot]"
- git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- git add README.md
- git commit -m "Polish one-click download section"
- git push
- fi
-
- - name: Package playable mod
- shell: bash
- run: |
- rm -f Metamorph-Creative-Menu.zip
- zip -r -9 Metamorph-Creative-Menu.zip metamorph_creative_menu \
- -x 'metamorph_creative_menu/tests/*' \
- 'metamorph_creative_menu/README.txt'
- unzip -l Metamorph-Creative-Menu.zip | grep -q 'metamorph_creative_menu/mod.xml'
- unzip -l Metamorph-Creative-Menu.zip | grep -q 'metamorph_creative_menu/init.lua'
-
- - name: Publish stable download URL
- env:
- GH_TOKEN: ${{ github.token }}
- shell: bash
- run: |
- git tag -f latest-build HEAD
- git push origin refs/tags/latest-build --force
-
- if gh release view latest-build >/dev/null 2>&1; then
- gh release upload latest-build Metamorph-Creative-Menu.zip --clobber
- gh release edit latest-build \
- --title "Latest ready-to-install build" \
- --notes "Automatically packaged from the current main branch. Download Metamorph-Creative-Menu.zip, then copy the included metamorph_creative_menu folder into Noita/mods/."
- else
- gh release create latest-build Metamorph-Creative-Menu.zip \
- --title "Latest ready-to-install build" \
- --notes "Automatically packaged from the current main branch. Download Metamorph-Creative-Menu.zip, then copy the included metamorph_creative_menu folder into Noita/mods/."
- fi
diff --git a/.github/workflows/workshop-validation.yml b/.github/workflows/workshop-validation.yml
new file mode 100644
index 0000000..acd31e6
--- /dev/null
+++ b/.github/workflows/workshop-validation.yml
@@ -0,0 +1,366 @@
+name: Workshop build
+
+on:
+ push:
+ branches: [workshop]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: workshop-build
+ cancel-in-progress: true
+
+jobs:
+ validate:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7
+
+ - name: Validate exact Workshop payload
+ shell: bash
+ run: |
+ set -euo pipefail
+ python3 - <<'PY'
+ from pathlib import Path
+ import re
+ import struct
+ import xml.etree.ElementTree as ET
+
+ root = Path('metamorph_creative_menu')
+ required_source = [
+ 'mod.xml',
+ 'mod_id.txt',
+ 'init.lua',
+ 'settings.lua',
+ 'translations.csv',
+ 'workshop.xml',
+ 'workshop_preview_image.png',
+ 'README.txt',
+ ]
+ missing = [name for name in required_source if not (root / name).is_file()]
+ if missing:
+ raise SystemExit('Missing Workshop source files: ' + ', '.join(missing))
+
+ if (root / 'README.txt').read_text(encoding='utf-8') != 'Creator: zerodancing\n':
+ raise SystemExit("Workshop source README.txt must contain only 'Creator: zerodancing'")
+
+ if (root / 'NoitaPatcher').exists():
+ raise SystemExit('Workshop branch must not contain NoitaPatcher/')
+ dlls = sorted(p.relative_to(root).as_posix() for p in root.rglob('*.dll'))
+ if dlls:
+ raise SystemExit('Native DLLs are not allowed in Workshop branch: ' + ', '.join(dlls))
+
+ if (root / 'mod_id.txt').read_text(encoding='utf-8').strip() != 'metamorph_creative_menu':
+ raise SystemExit('Unexpected mod_id.txt value')
+
+ mod_root = ET.parse(root / 'mod.xml').getroot()
+ if mod_root.tag != 'Mod' or mod_root.attrib.get('name') != 'Metamorph: Creative Menu':
+ raise SystemExit('Unexpected mod.xml metadata')
+ if 'request_no_api_restrictions' in mod_root.attrib:
+ raise SystemExit('Workshop mod.xml must not request unrestricted API')
+ if mod_root.attrib.get('is_game_mode', '0') not in {'0', 'false'}:
+ raise SystemExit('Workshop build must not be a game mode')
+
+ workshop = ET.parse(root / 'workshop.xml').getroot()
+ if workshop.tag != 'Mod' or workshop.attrib.get('name') != 'Metamorph: Creative Menu':
+ raise SystemExit('Unexpected workshop.xml metadata')
+ if workshop.attrib.get('description') != '':
+ raise SystemExit('workshop.xml description must remain empty so Steam page text is not overwritten')
+
+ documented_tags = {
+ 'gameplay','graphics','quality of life','translations','perks','spells',
+ 'player characters','loadouts','biomes','total conversions','game modes',
+ 'creatures','bosses','alchemy','tweaks','items','audio','cheats','funny',
+ 'streaming integration','mod dependencies',
+ }
+ tags = [x.strip() for x in workshop.attrib.get('tags', '').split(',') if x.strip()]
+ unknown_tags = sorted(set(tags) - documented_tags)
+ if unknown_tags:
+ raise SystemExit('Unknown Workshop tags: ' + ', '.join(unknown_tags))
+ if 'gameplay' not in tags:
+ raise SystemExit('Workshop tags must include gameplay')
+
+ def split_pipe(value):
+ return [x.strip().replace('\\', '/') for x in value.split('|') if x.strip()]
+
+ excluded_dirs = set(split_pipe(workshop.attrib.get('dont_upload_folders', '')))
+ excluded_files = set(split_pipe(workshop.attrib.get('dont_upload_files', '')))
+ required_excluded_dirs = {'NoitaPatcher', 'tests', 'files/qa', 'files/diagnostics'}
+ required_excluded_files = {
+ 'README.txt',
+ 'dev_mode.lua',
+ 'files/core/bounded_log.lua',
+ 'files/features/creatures/diagnostics.lua',
+ 'files/integrations/ew/bridge/qa.lua',
+ 'files/integrations/ew/bridge/qa_reserved.lua',
+ }
+ missing_dir_exclusions = sorted(required_excluded_dirs - excluded_dirs)
+ missing_file_exclusions = sorted(required_excluded_files - excluded_files)
+ if missing_dir_exclusions:
+ raise SystemExit('Missing Workshop folder exclusions: ' + ', '.join(missing_dir_exclusions))
+ if missing_file_exclusions:
+ raise SystemExit('Missing Workshop file exclusions: ' + ', '.join(missing_file_exclusions))
+
+ preview = root / 'workshop_preview_image.png'
+ data = preview.read_bytes()
+ if len(data) < 24 or data[:8] != b'\x89PNG\r\n\x1a\n' or data[12:16] != b'IHDR':
+ raise SystemExit('workshop_preview_image.png is not a valid PNG with IHDR header')
+ width, height = struct.unpack('>II', data[16:24])
+ if (width, height) != (1280, 720) or width * 9 != height * 16:
+ raise SystemExit(f'Workshop preview must be exactly 1280x720 (16:9), got {width}x{height}')
+ if preview.stat().st_size >= 1_000_000:
+ raise SystemExit('Workshop preview should stay below 1 MB')
+
+ def excluded(relative):
+ name = relative.as_posix()
+ return name in excluded_files or any(name == d or name.startswith(d + '/') for d in excluded_dirs)
+
+ allowed_suffixes = {'.txt','.csv','.xml','.json','.bmp','.png','.lua','.frag','.vert','.bank','.bin','.plz'}
+ uploaded = []
+ bad_types = []
+ symlinks = []
+ for path in sorted(root.rglob('*')):
+ if not path.is_file():
+ continue
+ relative = path.relative_to(root)
+ if path.is_symlink():
+ symlinks.append(relative.as_posix())
+ continue
+ if excluded(relative):
+ continue
+ uploaded.append(relative.as_posix())
+ if path.suffix.lower() not in allowed_suffixes:
+ bad_types.append(relative.as_posix())
+
+ if symlinks:
+ raise SystemExit('Symlinks are not allowed in Workshop payload: ' + ', '.join(symlinks))
+ if bad_types:
+ raise SystemExit('Unsupported Workshop file types: ' + ', '.join(bad_types))
+
+ uploaded_set = set(uploaded)
+ essential = {'mod.xml','mod_id.txt','init.lua','settings.lua','translations.csv','workshop.xml','workshop_preview_image.png'}
+ missing_essential = sorted(essential - uploaded_set)
+ if missing_essential:
+ raise SystemExit('Essential files excluded from Workshop upload: ' + ', '.join(missing_essential))
+
+ forbidden_upload = sorted(
+ name for name in uploaded
+ if name in required_excluded_files
+ or name.lower().endswith('.dll')
+ or name.startswith('NoitaPatcher/')
+ or name.startswith('tests/')
+ or name.startswith('files/qa/')
+ or name.startswith('files/diagnostics/')
+ )
+ if forbidden_upload:
+ raise SystemExit('Developer/native files leaked into Workshop payload: ' + ', '.join(forbidden_upload))
+
+ strong_rules = [
+ ('numbered test chronology', re.compile(r'\bTEST\s+\d+\b', re.I)),
+ ('test-only production API', re.compile(r'\b[A-Za-z0-9_]+_for_test\b')),
+ ('conversation-specific wording', re.compile(r"\b(?:the\s+)?user['’]s\s+build\b|\bat\s+the\s+user['’]s\s+request\b", re.I)),
+ ('numbered runtime label', re.compile(r'\[MCM\s*(?:TEST\s*)?\d{2,}\]', re.I)),
+ ('AI attribution residue', re.compile(r'\b(?:ChatGPT|OpenAI|Claude|Gemini|Copilot|LLM|AI[- ]generated|generated\s+by\s+AI)\b', re.I)),
+ ('debug-named runtime API', re.compile(r'\b(?:debug_[A-Za-z0-9_]+|[A-Za-z0-9_]+_debug|[A-Za-z0-9_]+\.debug(?:_[A-Za-z0-9_]+)?)\b')),
+ ('development mode wiring', re.compile(r'\b(?:dev_mode|DEV_MODE|METAMORPH_CREATIVE_MENU_DEV_MODE)\b')),
+ ('diagnostics runtime hook', re.compile(r'METAMORPH_CREATIVE_MENU_DIAGNOSTICS|\bdiagnostic_event\b')),
+ ('UI action audit hook', re.compile(r'\bui\.audit\b|\blocal\s+audit\s*=\s*ui\.audit\b')),
+ ]
+ residue = []
+ for name in uploaded:
+ if not name.endswith('.lua'):
+ continue
+ text = (root / name).read_text(encoding='utf-8', errors='replace')
+ for lineno, line in enumerate(text.splitlines(), 1):
+ for label, pattern in strong_rules:
+ if pattern.search(line):
+ residue.append(f'{name}:{lineno}: {label}: {line.strip()}')
+ if residue:
+ raise SystemExit('Workshop runtime residue detected:\n' + '\n'.join(residue))
+
+ unsafe_api = re.compile(r'(? {dependency}')
+ if missing_refs:
+ raise SystemExit('Workshop payload references missing local files:\n' + '\n'.join(sorted(set(missing_refs))))
+
+ mode = 'update' if (root / 'workshop_id.txt').exists() else 'first-publication'
+ print(
+ f'Workshop payload: PASS mode={mode} files={len(uploaded)} '
+ f'preview={width}x{height} preview_bytes={preview.stat().st_size}'
+ )
+ PY
+
+ - name: Install texlua
+ shell: bash
+ run: |
+ sudo apt-get update -qq
+ sudo apt-get install -y --no-install-recommends texlive-binaries >/dev/null
+
+ - name: Run Workshop regression suite
+ working-directory: metamorph_creative_menu
+ shell: bash
+ run: |
+ python3 - <<'PY'
+ from pathlib import Path
+ import shutil
+ import subprocess
+ import sys
+
+ root = Path('.').resolve()
+ texlua = shutil.which('texlua')
+ if not texlua:
+ raise SystemExit('texlua not found')
+
+ excluded_contracts = {'standalone_patcher_contract.py'}
+ preferred = [
+ 'syntax_contract.py',
+ 'architecture_contract.py',
+ 'behavior_coverage_contract.py',
+ 'localization_contract.py',
+ 'qa_phase_contract.py',
+ ]
+ contracts = {
+ p.name: p for p in (root / 'tests').glob('*_contract.py')
+ if p.name not in excluded_contracts
+ }
+ ordered = []
+ for name in preferred:
+ path = contracts.pop(name, None)
+ if path is not None:
+ ordered.append(path)
+ ordered.extend(contracts[name] for name in sorted(contracts))
+ mocks = sorted((root / 'tests').glob('*_mock.lua'), key=lambda p: p.name)
+
+ commands = [[sys.executable, str(path), str(root)] for path in ordered]
+ commands.extend([[texlua, str(path), str(root)] for path in mocks])
+ for command in commands:
+ print('+', ' '.join(command), flush=True)
+ subprocess.run(command, check=True)
+
+ print(
+ 'WORKSHOP_REGRESSION_TESTS=PASS '
+ f'count={len(commands)} contracts={len(ordered)} behavioral_mocks={len(mocks)} '
+ 'excluded=standalone_patcher_contract.py'
+ )
+ PY
+
+ publish:
+ needs: validate
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ steps:
+ - uses: actions/checkout@v7
+
+ - name: Package exact Workshop upload set
+ shell: bash
+ run: |
+ set -euo pipefail
+ python3 - <<'PY'
+ from pathlib import Path
+ from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
+ import xml.etree.ElementTree as ET
+
+ root = Path('metamorph_creative_menu')
+ archive_path = Path('Metamorph-Creative-Menu-Workshop.zip')
+ workshop = ET.parse(root / 'workshop.xml').getroot()
+
+ def split_pipe(value):
+ return [x.strip().replace('\\', '/') for x in value.split('|') if x.strip()]
+
+ excluded_dirs = set(split_pipe(workshop.attrib.get('dont_upload_folders', '')))
+ excluded_files = set(split_pipe(workshop.attrib.get('dont_upload_files', '')))
+
+ def excluded(relative):
+ name = relative.as_posix()
+ return name in excluded_files or any(name == d or name.startswith(d + '/') for d in excluded_dirs)
+
+ files = [path for path in sorted(root.rglob('*')) if path.is_file() and not excluded(path.relative_to(root))]
+ with ZipFile(archive_path, 'w', compression=ZIP_DEFLATED, compresslevel=9) as archive:
+ for path in files:
+ relative = Path('metamorph_creative_menu') / path.relative_to(root)
+ info = ZipInfo(relative.as_posix(), date_time=(1980, 1, 1, 0, 0, 0))
+ info.compress_type = ZIP_DEFLATED
+ info.external_attr = 0o100644 << 16
+ archive.writestr(info, path.read_bytes())
+
+ with ZipFile(archive_path) as archive:
+ names = {name for name in archive.namelist() if not name.endswith('/')}
+ required = {
+ 'metamorph_creative_menu/mod.xml',
+ 'metamorph_creative_menu/mod_id.txt',
+ 'metamorph_creative_menu/init.lua',
+ 'metamorph_creative_menu/settings.lua',
+ 'metamorph_creative_menu/translations.csv',
+ 'metamorph_creative_menu/workshop.xml',
+ 'metamorph_creative_menu/workshop_preview_image.png',
+ }
+ missing = sorted(required - names)
+ if missing:
+ raise SystemExit('Missing required files in Workshop ZIP: ' + ', '.join(missing))
+ forbidden_exact = {
+ 'metamorph_creative_menu/README.txt',
+ 'metamorph_creative_menu/dev_mode.lua',
+ 'metamorph_creative_menu/files/core/bounded_log.lua',
+ 'metamorph_creative_menu/files/features/creatures/diagnostics.lua',
+ 'metamorph_creative_menu/files/integrations/ew/bridge/qa.lua',
+ 'metamorph_creative_menu/files/integrations/ew/bridge/qa_reserved.lua',
+ }
+ forbidden = sorted(
+ name for name in names
+ if name in forbidden_exact
+ or name.lower().endswith('.dll')
+ or name.startswith('metamorph_creative_menu/NoitaPatcher/')
+ or name.startswith('metamorph_creative_menu/tests/')
+ or name.startswith('metamorph_creative_menu/files/qa/')
+ or name.startswith('metamorph_creative_menu/files/diagnostics/')
+ )
+ if forbidden:
+ raise SystemExit('Forbidden files in Workshop ZIP: ' + ', '.join(forbidden))
+ broken = archive.testzip()
+ if broken is not None:
+ raise SystemExit('Workshop ZIP CRC failure: ' + broken)
+ print(f'Workshop ZIP: PASS files={len(files)} bytes={archive_path.stat().st_size}')
+ PY
+
+ - name: Publish stable Workshop download
+ env:
+ GH_TOKEN: ${{ github.token }}
+ shell: bash
+ run: |
+ set -euo pipefail
+ TAG=workshop-latest
+ ASSET=Metamorph-Creative-Menu-Workshop.zip
+ notes="Ready-to-upload Steam Workshop edition. Source commit: ${GITHUB_SHA}. This build intentionally excludes bundled NoitaPatcher/DLL and unrestricted API access."
+
+ if gh release view "$TAG" >/dev/null 2>&1; then
+ gh release upload "$TAG" "$ASSET" --clobber
+ gh release edit "$TAG" --title "Latest Steam Workshop build" --notes "$notes"
+ else
+ gh release create "$TAG" "$ASSET" --target "$GITHUB_SHA" --title "Latest Steam Workshop build" --notes "$notes"
+ fi
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..17713d7
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,28 @@
+Metamorph Creative Menu Attribution License 1.0
+
+Copyright (c) 2026 zerodancing
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of the original portions of Metamorph: Creative Menu and its associated source code, scripts, original assets, and documentation (the "Software"), to use, copy, reproduce, modify, merge, publish, distribute, sublicense, sell, rent, monetize, and otherwise use the Software for any commercial or non-commercial purpose, subject to the following conditions:
+
+1. License preservation.
+ Any redistribution of the Software, whether in original or modified form and whether in source or binary form, must include a readable copy of this license.
+
+2. Required attribution.
+ Any redistribution of the Software or a derivative work based on the Software must preserve the following attribution in a reasonably visible place available to recipients:
+
+ Original developer: zerodancing
+ Developer page: https://github.com/zerodancing
+ Original project: https://github.com/zerodancing/Metamorph-Creative-Menu
+
+ The attribution may appear in an accompanying LICENSE, NOTICE, or credits file; in documentation supplied with the distribution; on the mod, product, download, or store page from which the distribution is obtained; or in a credits/about screen. The attribution must remain readable and must not be deliberately hidden.
+
+3. Modified versions.
+ You may identify your own modifications, add your own copyright notices, and apply additional terms to your own original contributions. You may not remove or override the license-preservation and attribution requirements above for the original Software or portions derived from it.
+
+4. No endorsement.
+ This license does not grant permission to claim that zerodancing endorses, sponsors, maintains, or is responsible for a modified or redistributed version.
+
+5. Third-party material.
+ This license applies only to material for which zerodancing has the right to grant these permissions. Third-party components, libraries, game resources, trademarks, and other materials remain subject to their own licenses and terms. See THIRD_PARTY_NOTICES.md where applicable.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..2354104
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,7 @@
+Metamorph: Creative Menu
+
+Original developer: zerodancing
+Developer page: https://github.com/zerodancing
+Original project: https://github.com/zerodancing/Metamorph-Creative-Menu
+
+Redistributions and modified versions must preserve this attribution as required by the Metamorph Creative Menu Attribution License 1.0.
diff --git a/README.md b/README.md
index 08e019a..f4666c5 100644
--- a/README.md
+++ b/README.md
@@ -2,655 +2,82 @@
-Metamorph: Creative Menu
+Metamorph: Creative Menu — Steam Workshop build
-
- A standalone creative toolkit for Noita — transformations, possession, spells, items, perks, effects, weather and World Rules.
-
-
-
-
-## Choose your language
-
-| Language | Guide |
-|---|---|
-| English | [Open guide](#en) |
-| Русский | [Открыть руководство](#ru) |
-| Português (Brasil) | [Abrir guia](#pt-br) |
-| Español | [Abrir guía](#es) |
-| Deutsch | [Anleitung öffnen](#de) |
-| Français | [Ouvrir le guide](#fr) |
-| Italiano | [Apri la guida](#it) |
-| Polski | [Otwórz instrukcję](#pl) |
-| 简体中文 | [打开指南](#zh-cn) |
-| 日本語 | [ガイドを開く](#ja) |
-| 한국어 | [가이드 열기](#ko) |
-
-## Download
-
-| Package | Download |
-|---|---|
-| **Latest ready-to-install build** | **[⬇️ Download Metamorph-Creative-Menu.zip](https://github.com/zerodancing/Metamorph-Creative-Menu/releases/download/latest-build/Metamorph-Creative-Menu.zip)** |
-| Build page | [Latest ready-to-install build](https://github.com/zerodancing/Metamorph-Creative-Menu/releases/tag/latest-build) |
-
-> The ZIP already contains the complete `metamorph_creative_menu` folder, including the bundled NoitaPatcher runtime. Extract that folder directly into `Noita/mods/`.
->
-> ZIP уже содержит готовую папку `metamorph_creative_menu`, включая встроенный NoitaPatcher. Её нужно распаковать прямо в `Noita/mods/`.
-
-Correct final path:
-
-```text
-Noita/mods/metamorph_creative_menu/mod.xml
-```
-
-If you end up with `metamorph_creative_menu/metamorph_creative_menu/mod.xml`, the archive was extracted one folder too deep.
-
----
-
-
+Creative tools for Noita by zerodancing.
-## English
+This branch contains the **Steam Workshop build** of Metamorph: Creative Menu. It is intentionally different from the full standalone GitHub build: native NoitaPatcher-powered recovery and other standalone-only native features are not shipped here.
-### Installation
+## Install
-1. [Download the latest ready-to-install ZIP](https://github.com/zerodancing/Metamorph-Creative-Menu/releases/download/latest-build/Metamorph-Creative-Menu.zip).
-2. Close Noita before installing or updating the mod.
-3. On Steam, open **Library → right-click Noita → Manage → Browse local files**.
-4. Open the game's `mods` folder and copy the complete **`metamorph_creative_menu`** folder into it.
-5. Verify that `Noita/mods/metamorph_creative_menu/mod.xml` exists. Do not rename the mod folder.
-6. Start Noita, enable **Metamorph: Creative Menu**, allow **Unsafe mods / unrestricted API** when required, and restart Noita after enabling the mod.
-7. Start a run and press **TAB**. If the menu opens, installation is complete.
+Use the [Steam Workshop page](https://steamcommunity.com/sharedfiles/filedetails/?id=3785170245) to subscribe to this build. Steam manages the Workshop files; do not copy the standalone GitHub archive over the Workshop installation.
-**Updating:** close Noita, remove the old `metamorph_creative_menu` folder, then copy the new one into `mods`. Replacing the whole folder avoids stale files from older builds.
+After subscribing, enable **Metamorph: Creative Menu** in Noita and start a run. Press **TAB** to open or close the menu.
-### Controls
+## Main controls
- **TAB** — open or close the Creative Menu.
-- **TAB while transformed** — return to the human player form.
-- **G** by default — possess a supported creature under the cursor. The key can be changed in MCM settings.
-- Most catalog entries expose different **LMB/RMB** actions; the exact action is shown in the UI.
+- **TAB while transformed** — return to human form where the Workshop-safe path supports it.
+- **G** by default — possess a supported creature under the cursor; the binding can be changed in MCM settings.
+- Catalog entries may expose different **LMB/RMB** actions; the UI shows the active action.
-### What MCM can do
+## What this build includes
-- **Spells & wands** — search spells, replace a selected wand slot, delete a spell or drop it into the world.
-- **Items** — spawn supported items, containers, liquids, wands, books, quest objects and more; MCM can also attempt direct inventory placement.
-- **Perks** — spawn/apply perks and remove one or all tracked stacks while trying to restore only state owned by that perk.
-- **Effects** — apply supported timed/status effects, choose duration where supported and remove editor-owned effects.
-- **Creatures & forms** — spawn creatures or transform into supported creatures, objects and special forms.
-- **Human recovery** — normal TAB return uses the native polymorph lifecycle, with serialized NoitaPatcher recovery available for hard fallback paths.
-- **Death handoff** — supported transformed bodies can die while player authority is returned to the restored human body instead of immediately ending the run.
-- **Possession** — take over a supported existing creature under the cursor rather than simply spawning a duplicate.
-- **PLAYER companion** — spawn a player-like allied companion with copied presentation/inventory behavior and extended wand use when supported.
-- **Search** — large catalogs can be searched by localized names, IDs and descriptions depending on the editor.
-- **Weather** — control time of day, clouds, fog, wind, rain, lightning and presets; RELEASE stops MCM from holding the override.
-- **World Rules** — reversible overrides for creature relations, gold lifetime, spell uses, fog of war, trick-kill systems, healing drops, friendly rats, gore, damage flash, stain shedding, gravity, physics damping, blood volume, kick force, joint strength and day-cycle speed.
+The Workshop build keeps the normal creative editors and gameplay tools that work without the standalone native components, including supported spawning/editing for spells, wands, items, perks, effects, creatures/forms, weather and World Rules.
-
-Transformations, compatibility and recovery
+Some entities and recovery paths are inherently engine-sensitive. A feature that depends on the standalone native bridge may be unavailable or use a more limited safe fallback in this branch.
-MCM uses exact XML-path compatibility data and narrow safe-routing exceptions for entities that are known to be unsafe or unsuitable for direct native polymorph. Player-controlled forms try to retain useful native movement, attacks, visuals and physics while disabling AI that would fight player input. Complex bosses, scripted entities and physics objects can require dedicated adapters and may not reproduce every AI behavior exactly.
+## Standalone build
-NoitaPatcher is used for hard recovery capabilities such as entity serialization/deserialization, player handoff and other extended runtime functions. This is why the complete standalone build requests unrestricted/unsafe mod access.
+For the complete build, including the bundled NoitaPatcher runtime and native Game Over/recovery support, use the repository's ready-to-install GitHub release:
-
+- [Latest standalone release](https://github.com/zerodancing/Metamorph-Creative-Menu/releases/tag/latest-build)
+- [Direct standalone ZIP](https://github.com/zerodancing/Metamorph-Creative-Menu/releases/download/latest-build/Metamorph-Creative-Menu.zip)
-
-Entangled Worlds multiplayer integration
+Do **not** install the Workshop and standalone builds at the same time. They use the same mod identity and can conflict.
-**Entangled Worlds is optional.** MCM is designed to work as a complete standalone single-player mod without EW.
+## Entangled Worlds
-When `quant.ew` is enabled, MCM activates experimental integration for shared items, perks, weather, World Rules, forms/possession, companion requests and related authority/synchronization behavior. Use the same MCM version on every peer. Multiplayer support is intentionally considered experimental because not every Noita/EW edge case can be guaranteed to synchronize perfectly.
+Entangled Worlds integration is optional and experimental. When using multiplayer, keep MCM versions compatible between peers and expect some Noita/EW edge cases to remain engine-dependent.
-
+## Reporting problems
-### Requirements & third-party components
+When reporting a bug, specify that you are using the **Steam Workshop build**, describe the action that failed, and include the exact creature/item/effect or XML path when relevant.
-- **Noita** — required game, by Nolla Games.
-- **NoitaPatcher** by dextercd — bundled with MCM and used for extended runtime/recovery functionality.
-- **lbase64** by Ilya Kolbin — bundled local Base64 implementation.
-- **Entangled Worlds / Noita Proxy** by IntQuant and contributors — optional multiplayer integration; not required for single player.
-
-Exact upstream links, bundled paths and third-party license/status notes are in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
-
-### Troubleshooting
-
-- **TAB does nothing:** verify the exact `mod.xml` path, make sure MCM is enabled, allow Unsafe mods/unrestricted API, then restart Noita.
-- **Extended recovery or World Rules functionality is missing:** verify `metamorph_creative_menu/NoitaPatcher/noitapatcher.dll` is present and unrestricted API access is allowed.
-- **A form fails to return correctly:** report the exact creature name/XML and whether normal TAB return or fatal-death return failed.
-- **EW mismatch/desync:** verify that every peer uses the same MCM build and a compatible EW build.
-
-### Links
-
-- [Latest build](https://github.com/zerodancing/Metamorph-Creative-Menu/releases/tag/latest-build)
-- [Report a bug](https://github.com/zerodancing/Metamorph-Creative-Menu/issues)
-- [Third-party notices](THIRD_PARTY_NOTICES.md)
-- [Noita](https://noitagame.com/)
-- [NoitaPatcher](https://github.com/dextercd/NoitaPatcher)
-- [NoitaPatcher documentation](https://dexter.döpping.eu/NoitaPatcher/)
-- [Entangled Worlds](https://github.com/IntQuant/noita_entangled_worlds)
-- [lbase64](https://github.com/iskolbin/lbase64)
-
-[↑ Back to language selection](#languages)
+Issues: https://github.com/zerodancing/Metamorph-Creative-Menu/issues
---
-
-
## Русский
+Эта ветка содержит **Steam Workshop-сборку** Metamorph: Creative Menu. Она специально отличается от полной standalone-сборки GitHub: сюда не входят нативные возможности, завязанные на встроенный NoitaPatcher, включая расширенные standalone-only сценарии восстановления.
+
### Установка
-1. [Скачайте готовый ZIP последней сборки](https://github.com/zerodancing/Metamorph-Creative-Menu/releases/download/latest-build/Metamorph-Creative-Menu.zip).
-2. Полностью закройте Noita перед установкой или обновлением.
-3. В Steam откройте **Библиотека → ПКМ по Noita → Управление → Просмотреть локальные файлы**.
-4. Откройте папку `mods` и скопируйте туда целиком папку **`metamorph_creative_menu`** из архива.
-5. Проверьте, что существует файл `Noita/mods/metamorph_creative_menu/mod.xml`. Папку мода не переименовывайте.
-6. Запустите Noita, включите **Metamorph: Creative Menu**, разрешите **Unsafe mods / unrestricted API**, если игра этого требует, и перезапустите Noita после включения мода.
-7. Начните забег и нажмите **TAB**. Если меню открылось — установка закончена.
+Подпишитесь на мод на [странице Steam Workshop](https://steamcommunity.com/sharedfiles/filedetails/?id=3785170245). Файлы Workshop устанавливает Steam; не копируйте поверх них standalone-архив GitHub.
-**Обновление:** закройте Noita, удалите старую папку `metamorph_creative_menu` и скопируйте новую. Полная замена папки не оставляет устаревшие файлы предыдущих версий.
+Включите **Metamorph: Creative Menu** в Noita, начните забег и нажмите **TAB**.
### Управление
- **TAB** — открыть или закрыть Creative Menu.
-- **TAB во время превращения** — вернуться в человеческую форму.
-- **G** по умолчанию — занять тело поддерживаемого существа под курсором. Клавиша меняется в настройках MCM.
-- У большинства элементов каталога **ЛКМ/ПКМ** выполняют разные действия; точная подсказка показывается в интерфейсе.
-
-### Возможности MCM
-
-- **Заклинания и посохи** — поиск заклинаний, замена выбранного слота, удаление или выбрасывание заклинания в мир.
-- **Предметы** — создание предметов, контейнеров, жидкостей, посохов, книг, квестовых объектов и других поддерживаемых сущностей; возможна попытка сразу положить предмет в инвентарь.
-- **Перки** — создание/применение перков и удаление одного или всех отслеживаемых уровней с восстановлением принадлежащего перку состояния.
-- **Эффекты** — применение status/timed effects, выбор длительности и удаление состояния, созданного редактором.
-- **Существа и формы** — создание мобов и превращение в поддерживаемых существ, объекты и специальные формы.
-- **Возврат человека** — обычный TAB использует нативный lifecycle polymorph, а для аварийных сценариев доступно сериализованное восстановление через NoitaPatcher.
-- **Death handoff** — смерть поддерживаемой формы может вернуть player authority восстановленному человеку вместо немедленного Game Over.
-- **Possession** — занятие тела уже существующего поддерживаемого моба под курсором, а не простое создание копии.
-- **Союзник PLAYER** — создание союзника, похожего на игрока, с копированием визуального/инвентарного состояния и расширенным использованием посоха при доступных возможностях runtime.
-- **Поиск** — крупные каталоги ищут по локализованным именам, ID и описаниям в зависимости от вкладки.
-- **Погода** — время суток, облака, туман, ветер, дождь, молнии и пресеты; RELEASE прекращает активное удержание погодных значений.
-- **World Rules** — обратимые правила для отношений существ, времени жизни золота, использования заклинаний, fog of war, trick-kill механик, healing drops, friendly rats, gore, damage flash, stain shedding, гравитации, физического damping, объёма крови, силы пинка, прочности соединений и скорости цикла дня.
-
-
-Превращения, совместимость и восстановление
-
-MCM хранит совместимость по точным XML-путям и использует узкие safe-routing исключения для сущностей, которые опасно или бессмысленно превращать напрямую нативным polymorph. Управляемая игроком форма старается сохранить полезное движение, атаки, внешний вид и физику, отключая конфликтующий AI. Для сложных боссов, scripted-сущностей и physics-объектов могут использоваться отдельные адаптеры.
-
-NoitaPatcher нужен для жёстких recovery-сценариев: сериализации/десериализации сущностей, player handoff и других расширенных runtime-возможностей. Поэтому полная standalone-сборка использует unrestricted/unsafe mod access.
-
-
-
-
-Интеграция с Entangled Worlds
-
-**Entangled Worlds не обязателен.** В одиночной игре MCM является полноценным standalone-модом.
-
-При включённом `quant.ew` активируется экспериментальная синхронизация предметов, перков, погоды, World Rules, форм/possession, запросов companion и связанных authority-механизмов. На всех клиентах должна быть одна и та же версия MCM. Сетевая интеграция считается экспериментальной, потому что не все edge cases Noita/EW можно гарантированно синхронизировать.
-
-
-
-### Требования и сторонние компоненты
-
-- **Noita** — обязательная игра, Nolla Games.
-- **NoitaPatcher** от dextercd — включён в MCM и используется для расширенного runtime и recovery.
-- **lbase64** от Ilya Kolbin — локальная встроенная реализация Base64.
-- **Entangled Worlds / Noita Proxy** от IntQuant и contributors — опциональная мультиплеерная интеграция, для одиночной игры не нужна.
-
-Точные upstream-ссылки, пути встроенных компонентов и сведения об их лицензиях/статусе находятся в [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
-
-### Если что-то не работает
-
-- **TAB ничего не делает:** проверьте путь к `mod.xml`, включение MCM, Unsafe mods/unrestricted API и перезапустите Noita.
-- **Нет расширенного recovery или части World Rules:** проверьте наличие `metamorph_creative_menu/NoitaPatcher/noitapatcher.dll` и доступ к unrestricted API.
-- **Форма не возвращается правильно:** укажите точное имя/XML существа и уточните, сломался обычный возврат по TAB или возврат после смертельного урона.
-- **EW desync:** убедитесь, что у всех участников одинаковая сборка MCM и совместимая версия EW.
-
-### Ссылки
-
-- [Последняя готовая сборка](https://github.com/zerodancing/Metamorph-Creative-Menu/releases/tag/latest-build)
-- [Сообщить об ошибке](https://github.com/zerodancing/Metamorph-Creative-Menu/issues)
-- [Сторонние компоненты](THIRD_PARTY_NOTICES.md)
-- [Noita](https://noitagame.com/)
-- [NoitaPatcher](https://github.com/dextercd/NoitaPatcher)
-- [Документация NoitaPatcher](https://dexter.döpping.eu/NoitaPatcher/)
-- [Entangled Worlds](https://github.com/IntQuant/noita_entangled_worlds)
-- [lbase64](https://github.com/iskolbin/lbase64)
-
-[↑ К выбору языка](#languages)
-
----
-
-
-
-## Português (Brasil)
-
-### Instalação
-
-1. [Baixe o ZIP pronto para instalar](https://github.com/zerodancing/Metamorph-Creative-Menu/releases/download/latest-build/Metamorph-Creative-Menu.zip).
-2. Feche o Noita antes de instalar ou atualizar o mod.
-3. No Steam: **Biblioteca → clique direito em Noita → Gerenciar → Explorar arquivos locais**.
-4. Abra a pasta `mods` e copie para ela a pasta completa **`metamorph_creative_menu`**.
-5. Confirme que existe `Noita/mods/metamorph_creative_menu/mod.xml`.
-6. Inicie o Noita, ative **Metamorph: Creative Menu**, permita **Unsafe mods / unrestricted API** quando necessário e reinicie o jogo.
-7. Entre em uma partida e pressione **TAB**.
-
-### Controles
-
-- **TAB** — abre/fecha o menu; durante uma transformação, retorna à forma humana.
-- **G** por padrão — possui uma criatura compatível sob o cursor; a tecla pode ser alterada nas configurações.
-- **LMB/RMB** — ações diferentes dependendo do item do catálogo; a interface mostra a ação exata.
-
-### Recursos
-
-MCM permite editar **magias e varinhas**, criar **itens e líquidos**, aplicar/remover **perks e efeitos**, criar criaturas, **transformar-se**, possuir criaturas existentes, recuperar a forma humana, fazer **death handoff**, criar um aliado `PLAYER`, pesquisar grandes catálogos, controlar **clima** e aplicar **World Rules reversíveis** para relações entre criaturas, ouro, usos de magia, fog of war, trick kills, drops de cura, gore, gravidade, física, sangue, força de chute, juntas e ciclo do dia.
-
-Standalone, recuperação e Entangled Worlds
-
-O modo single-player não exige Entangled Worlds. O MCM inclui NoitaPatcher para serialização/recuperação, player handoff e outras funções avançadas; por isso a versão completa requer acesso Unsafe/unrestricted. Com `quant.ew` ativo, o MCM habilita integração multiplayer experimental para itens, perks, clima, World Rules, formas/possession e companion. Todos os peers devem usar a mesma versão do MCM.
-
-
-
-### Dependências e créditos
-
-- **Noita** — Nolla Games.
-- **NoitaPatcher** — dextercd, incluído.
-- **lbase64** — Ilya Kolbin, incluído.
-- **Entangled Worlds / Noita Proxy** — IntQuant e colaboradores, opcional.
-
-Veja caminhos, links upstream e informações de licença em [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
-
-### Problemas comuns
-
-Se TAB não funcionar, verifique `Noita/mods/metamorph_creative_menu/mod.xml`, ative o mod, permita Unsafe/unrestricted API e reinicie o Noita. Para problemas de transformação, informe a criatura/XML exata. Para EW, confirme versões idênticas do MCM nos peers.
-
-[Downloads](https://github.com/zerodancing/Metamorph-Creative-Menu/releases/tag/latest-build) · [Issues](https://github.com/zerodancing/Metamorph-Creative-Menu/issues) · [↑ Idiomas](#languages)
-
----
-
-
-
-## Español
-
-### Instalación
-
-1. [Descarga el ZIP listo para instalar](https://github.com/zerodancing/Metamorph-Creative-Menu/releases/download/latest-build/Metamorph-Creative-Menu.zip).
-2. Cierra Noita antes de instalar o actualizar.
-3. En Steam: **Biblioteca → clic derecho en Noita → Administrar → Ver archivos locales**.
-4. Copia la carpeta completa **`metamorph_creative_menu`** dentro de `Noita/mods/`.
-5. Comprueba que existe `Noita/mods/metamorph_creative_menu/mod.xml`.
-6. Activa **Metamorph: Creative Menu**, permite **Unsafe mods / unrestricted API** cuando sea necesario y reinicia Noita.
-7. Empieza una partida y pulsa **TAB**.
-
-### Controles
-
-- **TAB** — abre/cierra el menú; transformado, vuelve a la forma humana.
-- **G** por defecto — posee una criatura compatible bajo el cursor.
-- **LMB/RMB** — acciones diferentes según la entrada; la interfaz muestra la acción exacta.
-
-### Funciones
-
-MCM permite editar **hechizos y varitas**, crear **objetos y líquidos**, aplicar o quitar **perks y efectos**, crear criaturas, **transformarse**, poseer criaturas existentes, recuperar al jugador humano, realizar **death handoff**, crear un aliado `PLAYER`, buscar en catálogos, controlar el **clima** y aplicar **World Rules reversibles** para relaciones entre criaturas, oro, usos de hechizos, fog of war, trick kills, curación, gore, gravedad, física, sangre, fuerza de patada, uniones y ciclo diurno.
-
-Standalone, recuperación y Entangled Worlds
-
-Entangled Worlds no es necesario para un jugador. MCM incluye NoitaPatcher para serialización/recuperación, player handoff y funciones avanzadas, por lo que la versión completa requiere acceso Unsafe/unrestricted. Con `quant.ew` activo se habilita una integración multijugador experimental para objetos, perks, clima, World Rules, formas/possession y companion. Todos los peers deben usar la misma versión de MCM.
-
-
-
-### Dependencias y créditos
-
-- **Noita** — Nolla Games.
-- **NoitaPatcher** — dextercd, incluido.
-- **lbase64** — Ilya Kolbin, incluido.
-- **Entangled Worlds / Noita Proxy** — IntQuant y colaboradores, opcional.
-
-Consulta enlaces upstream y notas de licencia en [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
-
-### Solución rápida
-
-Si TAB no funciona, verifica la ruta de `mod.xml`, activa el mod, permite Unsafe/unrestricted API y reinicia Noita. Para errores de formas, informa la criatura/XML exacta. Para EW, verifica que todos usen la misma versión de MCM.
-
-[Descargas](https://github.com/zerodancing/Metamorph-Creative-Menu/releases/tag/latest-build) · [Issues](https://github.com/zerodancing/Metamorph-Creative-Menu/issues) · [↑ Idiomas](#languages)
-
----
-
-
-
-## Deutsch
-
-### Installation
-
-1. [Lade das installationsfertige ZIP herunter](https://github.com/zerodancing/Metamorph-Creative-Menu/releases/download/latest-build/Metamorph-Creative-Menu.zip).
-2. Beende Noita vor Installation oder Update vollständig.
-3. In Steam: **Bibliothek → Rechtsklick auf Noita → Verwalten → Lokale Dateien durchsuchen**.
-4. Kopiere den vollständigen Ordner **`metamorph_creative_menu`** nach `Noita/mods/`.
-5. Prüfe, dass `Noita/mods/metamorph_creative_menu/mod.xml` existiert.
-6. Aktiviere **Metamorph: Creative Menu**, erlaube **Unsafe mods / unrestricted API**, falls erforderlich, und starte Noita neu.
-7. Starte einen Run und drücke **TAB**.
-
-### Steuerung
-
-- **TAB** — Menü öffnen/schließen; verwandelt: zur menschlichen Form zurückkehren.
-- **G** standardmäßig — eine unterstützte Kreatur unter dem Cursor übernehmen.
-- **LMB/RMB** — unterschiedliche Aktionen je Katalogeintrag; die UI zeigt die genaue Aktion.
-
-### Funktionen
-
-MCM kann **Zauber und Zauberstäbe** bearbeiten, **Items und Flüssigkeiten** erzeugen, **Perks und Effekte** anwenden/entfernen, Kreaturen spawnen, den Spieler **verwandeln**, existierende Kreaturen übernehmen, den Menschen wiederherstellen, **Death Handoff** durchführen, einen `PLAYER`-Begleiter erzeugen, Kataloge durchsuchen, **Wetter** steuern und **reversible World Rules** für Kreaturenbeziehungen, Gold, Zaubernutzungen, fog of war, trick kills, Heilungsdrops, Gore, Gravitation, Physik, Blut, Kick-Kraft, Gelenke und Tageszyklus anwenden.
-
-Standalone, Recovery und Entangled Worlds
-
-Entangled Worlds ist für Singleplayer nicht erforderlich. MCM enthält NoitaPatcher für Serialisierung/Recovery, Player Handoff und weitere erweiterte Funktionen; deshalb benötigt die Vollversion Unsafe/unrestricted API. Mit aktivem `quant.ew` wird eine experimentelle Multiplayer-Integration für Items, Perks, Wetter, World Rules, Formen/Possession und Companion aktiviert. Alle Peers sollten dieselbe MCM-Version verwenden.
-
-
-
-### Abhängigkeiten & Credits
-
-- **Noita** — Nolla Games.
-- **NoitaPatcher** — dextercd, enthalten.
-- **lbase64** — Ilya Kolbin, enthalten.
-- **Entangled Worlds / Noita Proxy** — IntQuant und Mitwirkende, optional.
-
-Upstream-Links und Lizenzhinweise: [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
-
-### Fehlerbehebung
-
-Wenn TAB nicht funktioniert, prüfe den `mod.xml`-Pfad, aktiviere MCM, erlaube Unsafe/unrestricted API und starte Noita neu. Bei Formfehlern bitte die genaue Kreatur/XML nennen. Bei EW müssen alle Peers dieselbe MCM-Version verwenden.
-
-[Downloads](https://github.com/zerodancing/Metamorph-Creative-Menu/releases/tag/latest-build) · [Issues](https://github.com/zerodancing/Metamorph-Creative-Menu/issues) · [↑ Sprachen](#languages)
-
----
-
-
-
-## Français
-
-### Installation
-
-1. [Téléchargez le ZIP prêt à installer](https://github.com/zerodancing/Metamorph-Creative-Menu/releases/download/latest-build/Metamorph-Creative-Menu.zip).
-2. Fermez complètement Noita avant l'installation ou la mise à jour.
-3. Dans Steam : **Bibliothèque → clic droit sur Noita → Gérer → Parcourir les fichiers locaux**.
-4. Copiez le dossier complet **`metamorph_creative_menu`** dans `Noita/mods/`.
-5. Vérifiez que `Noita/mods/metamorph_creative_menu/mod.xml` existe.
-6. Activez **Metamorph: Creative Menu**, autorisez **Unsafe mods / unrestricted API** si nécessaire puis redémarrez Noita.
-7. Lancez une partie et appuyez sur **TAB**.
-
-### Commandes
-
-- **TAB** — ouvre/ferme le menu ; transformé, revient à la forme humaine.
-- **G** par défaut — prend possession d'une créature compatible sous le curseur.
-- **LMB/RMB** — actions différentes selon l'entrée ; l'interface affiche l'action exacte.
+- **TAB во время превращения** — вернуться в человеческую форму там, где это поддерживает Workshop-safe путь.
+- **G** по умолчанию — занять тело поддерживаемого существа под курсором; клавиша меняется в настройках MCM.
+- Для элементов каталогов ЛКМ/ПКМ могут выполнять разные действия; текущая подсказка показывается в интерфейсе.
-### Fonctionnalités
+### Возможности
-MCM permet de modifier **sorts et baguettes**, créer **objets et liquides**, appliquer/retirer **perks et effets**, faire apparaître des créatures, se **transformer**, posséder des créatures existantes, restaurer la forme humaine, effectuer le **death handoff**, créer un allié `PLAYER`, rechercher dans les catalogues, contrôler la **météo** et appliquer des **World Rules réversibles** concernant relations entre créatures, or, utilisations de sorts, fog of war, trick kills, soins, gore, gravité, physique, sang, puissance de coup de pied, articulations et cycle jour/nuit.
+Workshop-сборка сохраняет обычные creative-инструменты, которые работают без standalone native-компонентов: поддерживаемое редактирование/создание заклинаний, посохов, предметов, перков, эффектов, существ и форм, а также управление погодой и World Rules.
-Standalone, récupération et Entangled Worlds
+Функции, которым нужен standalone native bridge, в этой ветке могут быть недоступны или использовать более ограниченный безопасный fallback.
-Entangled Worlds n'est pas requis en solo. MCM inclut NoitaPatcher pour la sérialisation/récupération, le player handoff et d'autres fonctions avancées ; la version complète nécessite donc Unsafe/unrestricted API. Avec `quant.ew`, une intégration multijoueur expérimentale est activée pour objets, perks, météo, World Rules, formes/possession et companion. Tous les peers doivent utiliser la même version de MCM.
-
-
-
-### Dépendances & crédits
-
-- **Noita** — Nolla Games.
-- **NoitaPatcher** — dextercd, inclus.
-- **lbase64** — Ilya Kolbin, inclus.
-- **Entangled Worlds / Noita Proxy** — IntQuant et contributeurs, optionnel.
-
-Liens upstream et informations de licence : [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
-
-### Dépannage
-
-Si TAB ne fonctionne pas, vérifiez le chemin de `mod.xml`, activez MCM, autorisez Unsafe/unrestricted API et redémarrez Noita. Pour un problème de forme, indiquez la créature/XML exacte. Avec EW, utilisez la même version de MCM sur tous les peers.
-
-[Téléchargements](https://github.com/zerodancing/Metamorph-Creative-Menu/releases/tag/latest-build) · [Issues](https://github.com/zerodancing/Metamorph-Creative-Menu/issues) · [↑ Langues](#languages)
-
----
-
-
-
-## Italiano
-
-### Installazione
-
-1. [Scarica lo ZIP pronto all'installazione](https://github.com/zerodancing/Metamorph-Creative-Menu/releases/download/latest-build/Metamorph-Creative-Menu.zip).
-2. Chiudi completamente Noita prima di installare o aggiornare.
-3. Su Steam: **Libreria → clic destro su Noita → Gestisci → Sfoglia file locali**.
-4. Copia l'intera cartella **`metamorph_creative_menu`** in `Noita/mods/`.
-5. Verifica che esista `Noita/mods/metamorph_creative_menu/mod.xml`.
-6. Attiva **Metamorph: Creative Menu**, consenti **Unsafe mods / unrestricted API** se richiesto e riavvia Noita.
-7. Avvia una partita e premi **TAB**.
-
-### Controlli
-
-- **TAB** — apre/chiude il menu; durante una trasformazione torna alla forma umana.
-- **G** per impostazione predefinita — possiede una creatura supportata sotto il cursore.
-- **LMB/RMB** — azioni diverse a seconda della voce; l'interfaccia mostra l'azione esatta.
-
-### Funzioni
-
-MCM permette di modificare **incantesimi e bacchette**, creare **oggetti e liquidi**, applicare/rimuovere **perk ed effetti**, generare creature, **trasformarsi**, possedere creature esistenti, recuperare la forma umana, eseguire il **death handoff**, creare un alleato `PLAYER`, cercare nei cataloghi, controllare il **meteo** e applicare **World Rules reversibili** per relazioni tra creature, oro, usi degli incantesimi, fog of war, trick kill, cure, gore, gravità, fisica, sangue, forza del calcio, giunti e ciclo del giorno.
-
-Standalone, recovery ed Entangled Worlds
-
-Entangled Worlds non è necessario in single-player. MCM include NoitaPatcher per serializzazione/recovery, player handoff e altre funzioni avanzate; per questo la versione completa richiede Unsafe/unrestricted API. Con `quant.ew` attivo viene abilitata un'integrazione multiplayer sperimentale per oggetti, perk, meteo, World Rules, forme/possession e companion. Tutti i peer devono usare la stessa versione di MCM.
-
-
-
-### Dipendenze e crediti
-
-- **Noita** — Nolla Games.
-- **NoitaPatcher** — dextercd, incluso.
-- **lbase64** — Ilya Kolbin, incluso.
-- **Entangled Worlds / Noita Proxy** — IntQuant e collaboratori, opzionale.
-
-Link upstream e informazioni sulle licenze: [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
-
-### Risoluzione problemi
-
-Se TAB non funziona, controlla il percorso di `mod.xml`, abilita MCM, consenti Unsafe/unrestricted API e riavvia Noita. Per problemi con le forme indica creatura/XML esatta. Con EW usa la stessa versione MCM su tutti i peer.
-
-[Download](https://github.com/zerodancing/Metamorph-Creative-Menu/releases/tag/latest-build) · [Issues](https://github.com/zerodancing/Metamorph-Creative-Menu/issues) · [↑ Lingue](#languages)
-
----
-
-
-
-## Polski
-
-### Instalacja
-
-1. [Pobierz gotowy ZIP](https://github.com/zerodancing/Metamorph-Creative-Menu/releases/download/latest-build/Metamorph-Creative-Menu.zip).
-2. Zamknij Noitę przed instalacją lub aktualizacją.
-3. Steam: **Biblioteka → prawy przycisk na Noita → Zarządzaj → Przeglądaj pliki lokalne**.
-4. Skopiuj cały folder **`metamorph_creative_menu`** do `Noita/mods/`.
-5. Sprawdź, czy istnieje `Noita/mods/metamorph_creative_menu/mod.xml`.
-6. Włącz **Metamorph: Creative Menu**, zezwól na **Unsafe mods / unrestricted API**, jeśli jest to wymagane, i uruchom Noitę ponownie.
-7. Rozpocznij grę i naciśnij **TAB**.
-
-### Sterowanie
-
-- **TAB** — otwiera/zamyka menu; podczas transformacji wraca do ludzkiej formy.
-- **G** domyślnie — przejmuje wspieraną istotę pod kursorem.
-- **LMB/RMB** — różne akcje zależnie od wpisu; interfejs pokazuje dokładne działanie.
-
-### Możliwości
-
-MCM pozwala edytować **zaklęcia i różdżki**, tworzyć **przedmioty i płyny**, nakładać/usuwać **perki i efekty**, tworzyć stworzenia, **transformować się**, przejmować istniejące stworzenia, odzyskiwać ludzką formę, wykonywać **death handoff**, tworzyć sojusznika `PLAYER`, przeszukiwać katalogi, kontrolować **pogodę** oraz stosować **odwracalne World Rules** dotyczące relacji stworzeń, złota, użyć zaklęć, fog of war, trick kills, leczenia, gore, grawitacji, fizyki, krwi, siły kopnięcia, połączeń i cyklu dnia.
-
-Standalone, recovery i Entangled Worlds
-
-Entangled Worlds nie jest wymagany w single-player. MCM zawiera NoitaPatcher do serializacji/recovery, player handoff i innych zaawansowanych funkcji, dlatego pełna wersja wymaga Unsafe/unrestricted API. Przy aktywnym `quant.ew` włącza się eksperymentalna integracja multiplayer dla przedmiotów, perków, pogody, World Rules, form/possession i companion. Wszyscy gracze powinni używać tej samej wersji MCM.
-
-
-
-### Zależności i autorzy
-
-- **Noita** — Nolla Games.
-- **NoitaPatcher** — dextercd, dołączony.
-- **lbase64** — Ilya Kolbin, dołączony.
-- **Entangled Worlds / Noita Proxy** — IntQuant i współtwórcy, opcjonalny.
-
-Linki upstream i informacje licencyjne: [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
-
-### Rozwiązywanie problemów
-
-Jeśli TAB nie działa, sprawdź ścieżkę `mod.xml`, włącz MCM, zezwól na Unsafe/unrestricted API i uruchom Noitę ponownie. Przy błędzie formy podaj dokładną istotę/XML. Dla EW wszyscy powinni mieć tę samą wersję MCM.
-
-[Pobieranie](https://github.com/zerodancing/Metamorph-Creative-Menu/releases/tag/latest-build) · [Issues](https://github.com/zerodancing/Metamorph-Creative-Menu/issues) · [↑ Języki](#languages)
-
----
-
-
-
-## 简体中文
-
-### 安装
-
-1. [下载可直接安装的 ZIP](https://github.com/zerodancing/Metamorph-Creative-Menu/releases/download/latest-build/Metamorph-Creative-Menu.zip)。
-2. 安装或更新前完全关闭 Noita。
-3. Steam:**库 → 右键 Noita → 管理 → 浏览本地文件**。
-4. 将完整的 **`metamorph_creative_menu`** 文件夹复制到 `Noita/mods/`。
-5. 确认 `Noita/mods/metamorph_creative_menu/mod.xml` 存在。
-6. 启用 **Metamorph: Creative Menu**;需要时允许 **Unsafe mods / unrestricted API**,然后重启 Noita。
-7. 进入游戏并按 **TAB**。
-
-### 操作
-
-- **TAB** — 打开/关闭菜单;变形状态下返回人类形态。
-- **G**(默认)— 附身光标下的受支持生物。
-- **LMB/RMB** — 根据目录项目执行不同操作;界面会显示具体动作。
-
-### 功能
-
-MCM 可以编辑**法术与法杖**、生成**物品和液体**、添加/移除**Perk 与效果**、生成生物、进行**变形**、附身已有生物、恢复人类形态、执行 **death handoff**、生成 `PLAYER` 盟友、搜索大型目录、控制**天气**,并提供可恢复的 **World Rules**,包括生物关系、金币、法术次数、fog of war、trick kill、治疗掉落、gore、重力、物理阻尼、血量、踢击力度、关节强度和昼夜循环等。
-
-独立模式、恢复与 Entangled Worlds
-
-单人模式不需要 Entangled Worlds。MCM 内置 NoitaPatcher,用于实体序列化/恢复、player handoff 和其他高级功能,因此完整版本需要 Unsafe/unrestricted API。启用 `quant.ew` 后,会开启针对物品、Perk、天气、World Rules、形态/附身和 companion 的实验性多人同步。所有玩家应使用相同版本的 MCM。
-
-
-
-### 依赖与致谢
-
-- **Noita** — Nolla Games。
-- **NoitaPatcher** — dextercd,已内置。
-- **lbase64** — Ilya Kolbin,已内置。
-- **Entangled Worlds / Noita Proxy** — IntQuant 与贡献者,可选。
-
-上游链接和许可状态见 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)。
-
-### 故障排除
-
-如果 TAB 无响应,请检查 `mod.xml` 路径、确认 MCM 已启用、允许 Unsafe/unrestricted API,并重启 Noita。形态问题请报告准确的生物/XML。EW 问题请确保所有玩家使用同一 MCM 版本。
-
-[下载](https://github.com/zerodancing/Metamorph-Creative-Menu/releases/tag/latest-build) · [Issues](https://github.com/zerodancing/Metamorph-Creative-Menu/issues) · [↑ 语言](#languages)
-
----
-
-
-
-## 日本語
-
-### インストール
-
-1. [インストール用 ZIP をダウンロード](https://github.com/zerodancing/Metamorph-Creative-Menu/releases/download/latest-build/Metamorph-Creative-Menu.zip)します。
-2. インストールまたは更新前に Noita を完全に終了します。
-3. Steam:**ライブラリ → Noita を右クリック → 管理 → ローカルファイルを閲覧**。
-4. **`metamorph_creative_menu`** フォルダ全体を `Noita/mods/` にコピーします。
-5. `Noita/mods/metamorph_creative_menu/mod.xml` が存在することを確認します。
-6. **Metamorph: Creative Menu** を有効化し、必要な場合は **Unsafe mods / unrestricted API** を許可して Noita を再起動します。
-7. ゲームを開始して **TAB** を押します。
-
-### 操作
-
-- **TAB** — メニューを開閉。変身中は人間形態へ戻ります。
-- **G**(デフォルト)— カーソル下の対応クリーチャーを possession します。
-- **LMB/RMB** — カタログ項目ごとに異なる操作。正確な動作は UI に表示されます。
-
-### 機能
-
-MCM では**スペルとワンド**の編集、**アイテムと液体**の生成、**Perk とエフェクト**の適用/削除、クリーチャー生成、**変身**、既存クリーチャーの possession、人間形態の復元、**death handoff**、`PLAYER` 味方の生成、カタログ検索、**天候**制御、そしてクリーチャー関係・ゴールド・スペル使用回数・fog of war・trick kill・回復ドロップ・gore・重力・物理・血液量・キック力・ジョイント強度・昼夜サイクルなどの**可逆 World Rules**を利用できます。
-
-Standalone、Recovery、Entangled Worlds
-
-シングルプレイヤーでは Entangled Worlds は不要です。MCM には NoitaPatcher が同梱され、シリアライズ/復元、player handoff、その他の高度な機能に使われるため、完全版では Unsafe/unrestricted API が必要です。`quant.ew` が有効な場合、アイテム・Perk・天候・World Rules・形態/possession・companion の実験的マルチプレイヤー統合が有効になります。全 peer で同じ MCM バージョンを使用してください。
-
-
-
-### 依存関係とクレジット
-
-- **Noita** — Nolla Games。
-- **NoitaPatcher** — dextercd、同梱。
-- **lbase64** — Ilya Kolbin、同梱。
-- **Entangled Worlds / Noita Proxy** — IntQuant と contributors、任意。
-
-Upstream リンクとライセンス情報:[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)。
-
-### トラブルシューティング
-
-TAB が反応しない場合は `mod.xml` のパス、MCM の有効化、Unsafe/unrestricted API、Noita の再起動を確認してください。形態の不具合は正確なクリーチャー/XML を報告してください。EW では全 peer の MCM バージョンを揃えてください。
-
-[Download](https://github.com/zerodancing/Metamorph-Creative-Menu/releases/tag/latest-build) · [Issues](https://github.com/zerodancing/Metamorph-Creative-Menu/issues) · [↑ 言語](#languages)
-
----
-
-
-
-## 한국어
-
-### 설치
-
-1. [바로 설치 가능한 ZIP을 다운로드](https://github.com/zerodancing/Metamorph-Creative-Menu/releases/download/latest-build/Metamorph-Creative-Menu.zip)합니다.
-2. 설치 또는 업데이트 전에 Noita를 완전히 종료합니다.
-3. Steam: **라이브러리 → Noita 우클릭 → 관리 → 로컬 파일 찾아보기**.
-4. **`metamorph_creative_menu`** 폴더 전체를 `Noita/mods/`에 복사합니다.
-5. `Noita/mods/metamorph_creative_menu/mod.xml`이 존재하는지 확인합니다.
-6. **Metamorph: Creative Menu**를 활성화하고 필요한 경우 **Unsafe mods / unrestricted API**를 허용한 뒤 Noita를 재시작합니다.
-7. 게임을 시작하고 **TAB**을 누릅니다.
-
-### 조작
-
-- **TAB** — 메뉴 열기/닫기. 변신 상태에서는 인간 형태로 돌아갑니다.
-- **G**(기본값) — 커서 아래의 지원되는 생물을 possession 합니다.
-- **LMB/RMB** — 카탈로그 항목에 따라 다른 동작을 수행하며 UI에 정확한 동작이 표시됩니다.
-
-### 기능
-
-MCM은 **주문과 완드** 편집, **아이템과 액체** 생성, **Perk와 효과** 적용/제거, 생물 생성, **변신**, 기존 생물 possession, 인간 형태 복원, **death handoff**, `PLAYER` 동료 생성, 카탈로그 검색, **날씨** 제어 및 생물 관계·골드·주문 사용 횟수·fog of war·trick kill·회복 드롭·gore·중력·물리·혈액량·킥 힘·조인트 강도·낮/밤 주기 등을 다루는 **되돌릴 수 있는 World Rules**를 제공합니다.
-
-Standalone, 복구 및 Entangled Worlds
-
-싱글플레이에서는 Entangled Worlds가 필요하지 않습니다. MCM에는 직렬화/복구, player handoff 및 기타 고급 기능을 위한 NoitaPatcher가 포함되어 있어 전체 버전은 Unsafe/unrestricted API가 필요합니다. `quant.ew`가 활성화되면 아이템, Perk, 날씨, World Rules, 형태/possession, companion에 대한 실험적 멀티플레이 통합이 활성화됩니다. 모든 peer는 동일한 MCM 버전을 사용해야 합니다.
-
-
-
-### 의존성 및 크레딧
-
-- **Noita** — Nolla Games.
-- **NoitaPatcher** — dextercd, 포함됨.
-- **lbase64** — Ilya Kolbin, 포함됨.
-- **Entangled Worlds / Noita Proxy** — IntQuant 및 contributors, 선택 사항.
-
-Upstream 링크와 라이선스 정보는 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)를 참고하세요.
-
-### 문제 해결
-
-TAB이 작동하지 않으면 `mod.xml` 경로, MCM 활성화, Unsafe/unrestricted API 허용 여부를 확인하고 Noita를 재시작하세요. 형태 문제는 정확한 creature/XML을 보고해 주세요. EW에서는 모든 peer가 동일한 MCM 버전을 사용해야 합니다.
-
-[Download](https://github.com/zerodancing/Metamorph-Creative-Menu/releases/tag/latest-build) · [Issues](https://github.com/zerodancing/Metamorph-Creative-Menu/issues) · [↑ 언어](#languages)
-
----
+### Полная standalone-сборка
-## For developers
+Полная версия со встроенным NoitaPatcher и расширенным Game Over/recovery доступна в GitHub Releases:
-The playable mod lives in `metamorph_creative_menu/`.
+- [Последняя standalone-сборка](https://github.com/zerodancing/Metamorph-Creative-Menu/releases/tag/latest-build)
+- [Прямая ссылка на ZIP](https://github.com/zerodancing/Metamorph-Creative-Menu/releases/download/latest-build/Metamorph-Creative-Menu.zip)
-- Architecture/developer notes: `metamorph_creative_menu/README.txt`
-- Regression suite: `metamorph_creative_menu/tests/`
-- Test instructions: `metamorph_creative_menu/tests/TESTING.txt`
-- Third-party notices: [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)
+Не устанавливайте Workshop и standalone одновременно: они используют один и тот же mod identity и могут конфликтовать.
-The repository's automatic `latest-build` workflow packages the playable `metamorph_creative_menu` folder into a ready-to-install ZIP and updates the stable download URL above.
+**Creator: zerodancing**
diff --git a/STEAM_WORKSHOP.md b/STEAM_WORKSHOP.md
new file mode 100644
index 0000000..654958b
--- /dev/null
+++ b/STEAM_WORKSHOP.md
@@ -0,0 +1,70 @@
+# Steam Workshop release guide
+
+This branch is the Steam Workshop edition of **Metamorph: Creative Menu**.
+
+The full standalone build remains on `main` and includes the bundled NoitaPatcher runtime. The Workshop edition intentionally does **not** request unrestricted API access and does **not** contain the bundled native NoitaPatcher DLL, because Noita Workshop does not support mods that require `request_no_api_restrictions="1"`, and the Workshop uploader only accepts its documented file types.
+
+## Files prepared for Workshop
+
+Inside `metamorph_creative_menu/`:
+
+- `mod.xml` — Workshop-safe metadata; no `request_no_api_restrictions`.
+- `workshop.xml` — Workshop title, tags and upload exclusions.
+- `workshop_preview_image.png` — 1280×720, 16:9 preview image.
+- `NoitaPatcher/` — intentionally absent from this branch.
+- `tests/`, `files/qa/`, `files/diagnostics/` and `README.txt` remain available in the repository where applicable for development/review, but are excluded by `workshop.xml` from the Steam upload.
+- Release runtime ships with `dev_mode = 0`.
+
+The Workshop validation workflow parses both XML files, checks the mod ID and release dev-mode flag, verifies that no DLL or unrestricted-API request is present, validates the actual PNG header/dimensions/size, simulates the upload exclusions and extension whitelist, scans the uploaded runtime for unrestricted Lua API usage, and runs the Workshop regression suite.
+
+## First publication
+
+1. Check out/download the `workshop` branch.
+2. If a full standalone MCM copy is already installed locally, **delete the old `Noita/mods/metamorph_creative_menu` folder first**. Do not copy the Workshop build over it: otherwise a stale `NoitaPatcher/noitapatcher.dll` can remain on disk and invalidate the Workshop-only test.
+3. Copy the complete `metamorph_creative_menu` folder from the `workshop` branch into `Noita/mods/`.
+4. Start Noita once and test the Workshop build with **Unsafe mods disabled**.
+5. Verify the menu opens with **TAB**. Smoke-test an item spawn, a wand/spell edit, perk apply/remove, weather, a supported World Rule, a normal transformation and **TAB return**.
+6. Do not use transformed-form death as a Workshop acceptance test: the hard serialized death handoff is a NoitaPatcher-powered standalone feature and is not guaranteed in this build.
+7. Close Noita.
+8. From the Noita installation directory run `workshop_upload.bat`, or run `noita_dev.exe -workshop_upload`.
+9. Select `metamorph_creative_menu` in the uploader and create the Workshop item.
+10. After the first upload, keep the generated `workshop_id.txt`. It identifies the existing Workshop item for future updates. Do not replace it with another item's ID.
+11. Open the new Steam Workshop page, add the full description/screenshots, then set visibility to Public when ready.
+
+## Suggested Workshop description
+
+**Metamorph: Creative Menu (MCM)** is a creative toolkit for Noita: edit wands and spells, spawn items and liquids, manage perks/effects, transform into supported creatures, possess existing creatures, control weather, use reversible World Rules and spawn a PLAYER-style companion.
+
+### Steam Workshop edition
+
+This Workshop build is intentionally compatible with Noita's Workshop restrictions. It does not bundle the native NoitaPatcher DLL and does not request unrestricted API access.
+
+Most normal menu/editor functionality remains available, but some advanced recovery, player-authority, exact entity serialization and other NoitaPatcher-powered paths are only available in the **Full Standalone Version**. Rules that require an unavailable native capability are presented as unsupported instead of pretending to work.
+
+**Full Standalone Version:**
+https://github.com/zerodancing/Metamorph-Creative-Menu/releases/tag/latest-build
+
+**Source / Issues:**
+https://github.com/zerodancing/Metamorph-Creative-Menu
+
+### Controls
+
+- **TAB** — open/close Creative Menu.
+- **TAB while transformed** — request return to human form through the normal native polymorph path.
+- **G** by default — possess a supported creature under the cursor.
+- LMB/RMB actions are shown in the menu for each catalog entry.
+
+### Entangled Worlds
+
+Entangled Worlds integration is experimental. For multiplayer, use the same MCM build on every peer and a compatible EW setup. If another enabled environment exposes a compatible NoitaPatcher bridge, MCM can reuse capabilities that are actually present, but the Workshop package itself does not include or require that native provider.
+
+## Updating the Workshop item
+
+For updates, work from the `workshop` branch, preserve the Workshop item's `workshop_id.txt`, replace the local Noita mod folder with the updated Workshop folder and run the uploader again. Keep `description=""` in `workshop.xml` if you want Steam's manually edited Workshop description to remain untouched by uploads.
+
+## Branch policy
+
+- `main` = full standalone GitHub build.
+- `workshop` = Steam Workshop-safe build.
+- Do not merge the Workshop removal of NoitaPatcher back into `main`.
+- When syncing future gameplay changes from `main` into `workshop`, re-check `mod.xml`, `workshop.xml`, the absence of `NoitaPatcher/`, `dev_mode = 0`, and test with Unsafe mods disabled before uploading.
diff --git a/metamorph_creative_menu/LICENSE.txt b/metamorph_creative_menu/LICENSE.txt
new file mode 100644
index 0000000..17713d7
--- /dev/null
+++ b/metamorph_creative_menu/LICENSE.txt
@@ -0,0 +1,28 @@
+Metamorph Creative Menu Attribution License 1.0
+
+Copyright (c) 2026 zerodancing
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of the original portions of Metamorph: Creative Menu and its associated source code, scripts, original assets, and documentation (the "Software"), to use, copy, reproduce, modify, merge, publish, distribute, sublicense, sell, rent, monetize, and otherwise use the Software for any commercial or non-commercial purpose, subject to the following conditions:
+
+1. License preservation.
+ Any redistribution of the Software, whether in original or modified form and whether in source or binary form, must include a readable copy of this license.
+
+2. Required attribution.
+ Any redistribution of the Software or a derivative work based on the Software must preserve the following attribution in a reasonably visible place available to recipients:
+
+ Original developer: zerodancing
+ Developer page: https://github.com/zerodancing
+ Original project: https://github.com/zerodancing/Metamorph-Creative-Menu
+
+ The attribution may appear in an accompanying LICENSE, NOTICE, or credits file; in documentation supplied with the distribution; on the mod, product, download, or store page from which the distribution is obtained; or in a credits/about screen. The attribution must remain readable and must not be deliberately hidden.
+
+3. Modified versions.
+ You may identify your own modifications, add your own copyright notices, and apply additional terms to your own original contributions. You may not remove or override the license-preservation and attribution requirements above for the original Software or portions derived from it.
+
+4. No endorsement.
+ This license does not grant permission to claim that zerodancing endorses, sponsors, maintains, or is responsible for a modified or redistributed version.
+
+5. Third-party material.
+ This license applies only to material for which zerodancing has the right to grant these permissions. Third-party components, libraries, game resources, trademarks, and other materials remain subject to their own licenses and terms. See THIRD_PARTY_NOTICES.md where applicable.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/metamorph_creative_menu/NOTICE.txt b/metamorph_creative_menu/NOTICE.txt
new file mode 100644
index 0000000..2354104
--- /dev/null
+++ b/metamorph_creative_menu/NOTICE.txt
@@ -0,0 +1,7 @@
+Metamorph: Creative Menu
+
+Original developer: zerodancing
+Developer page: https://github.com/zerodancing
+Original project: https://github.com/zerodancing/Metamorph-Creative-Menu
+
+Redistributions and modified versions must preserve this attribution as required by the Metamorph Creative Menu Attribution License 1.0.
diff --git a/metamorph_creative_menu/NoitaPatcher/load.lua b/metamorph_creative_menu/NoitaPatcher/load.lua
deleted file mode 100644
index 5456432..0000000
--- a/metamorph_creative_menu/NoitaPatcher/load.lua
+++ /dev/null
@@ -1,22 +0,0 @@
--- You're supposed to `dofile_once("path/to/load.lua")` this file.
-
-local orig_do_mod_appends = do_mod_appends
-
-do_mod_appends = function(filename, ...)
- do_mod_appends = orig_do_mod_appends
- do_mod_appends(filename, ...)
-
- local noitapatcher_path = string.match(filename, "(.*)/load.lua")
- if not noitapatcher_path then
- print("Couldn't detect NoitaPatcher path")
- end
-
- __nsew_path = noitapatcher_path .. "/noitapatcher/nsew/"
-
- package.cpath = package.cpath .. ";./" .. noitapatcher_path .. "/?.dll"
- package.path = package.path .. ";./" .. noitapatcher_path .. "/?.lua"
-
- -- Lua's loader should now be setup properly:
- -- local np = require("noitapatcher")
- -- local nsew = require("noitapatcher.nsew")
-end
diff --git a/metamorph_creative_menu/NoitaPatcher/noitapatcher.dll b/metamorph_creative_menu/NoitaPatcher/noitapatcher.dll
deleted file mode 100644
index 8b44f4c..0000000
Binary files a/metamorph_creative_menu/NoitaPatcher/noitapatcher.dll and /dev/null differ
diff --git a/metamorph_creative_menu/README.txt b/metamorph_creative_menu/README.txt
index 90852e3..0255892 100644
--- a/metamorph_creative_menu/README.txt
+++ b/metamorph_creative_menu/README.txt
@@ -1,367 +1 @@
-Metamorph: Creative Menu — project behavior and architecture contract
-=====================================================================
-
-About the project
------------------
-
-Metamorph: Creative Menu (MCM) is a creative/developer menu for Noita. The project is developed with AI assistance and is intended to remain useful both as a player-facing creative tool and as a developer/debugging toolkit.
-
-MCM is standalone in single player. The required NoitaPatcher runtime and Base64 codec are bundled with the mod. Entangled Worlds / Noita Proxy (`quant.ew`) is optional and only provides the experimental multiplayer integration layer. If EW already exposes a compatible NoitaPatcher API, MCM may reuse it instead of loading a second provider.
-
-The bundled NoitaPatcher is a native API extension. Full MCM functionality therefore requires Noita's Unsafe mods / unrestricted API permission. Bundling NoitaPatcher removes the hard dependency on the EW folder; it does not turn native DLL functionality into the safe Noita API.
-
-This document defines the player-visible behavior and the architecture contract that must be preserved during refactors. It is not an exhaustive description of every implementation detail. The code may contain additional recovery state, helper entities, RPCs, compatibility shims, diagnostics, caches and engine workarounds required to preserve the behavior described here.
-
-Core development rule: working behavior must not be removed or simplified merely because its implementation looks complicated, unusual or workaround-heavy. Before deleting or substantially rewriting such code, determine which Noita/EW/runtime problem it solves and prove with tests that the replacement preserves the same behavior.
-
-Equal host and peer rights
---------------------------
-
-In supported multiplayer scenarios, host and peer users are intended to have the same MCM-facing rights and capabilities. The host may still be a technical routing, confirmation or rebroadcast point when required by Entangled Worlds, but that does not make a feature host-only from the user's perspective.
-
-Any old comment, document or test claiming that peers must be forbidden from changing weather, World Rules or another menu feature merely because they are not the host is obsolete unless a newer explicit protocol requirement says otherwise.
-
-Main behavior
--------------
-
-1. Creative menu
-~~~~~~~~~~~~~~~~
-
-TAB opens the creative/developer menu. The current tabs are Spells, Items, Perks, Mobs, Effects, Weather and Rules. Search is part of normal use in large catalogs and must survive refactoring.
-
-2. Spells
-~~~~~~~~~
-
-The Spells tab can edit the currently held wand: select a slot, place supported spells in arbitrary order, replace an existing action, delete it or drop it into the world. Operations that must be visible to other players should use the EW integration when EW is active.
-
-Spell replacement is transactional: do not destroy the old spell until the replacement has been attached and verified.
-
-3. Items
-~~~~~~~~
-
-The Items tab can spawn supported Noita items such as wands, books, eggs, stones, containers, quest objects and other catalog entries.
-
-LMB spawns the item near the player. RMB attempts to place it into the appropriate inventory row. The standard quick inventory has four wand slots and four item slots. If no suitable slot is available, the item must remain in the world rather than disappearing, replacing another item or corrupting inventory state.
-
-World-spawn and inventory operations that require multiplayer visibility must use the EW bridge when EW is enabled.
-
-4. Perks
-~~~~~~~~
-
-LMB spawns a normal perk pickup. RMB applies the perk to the local player through the canonical pickup path. The editor can also remove supported perks, including perks obtained outside MCM when a safe inverse is known.
-
-Removing a perk must remove the state owned by that perk without blindly overwriting unrelated state from other perks, mods or the game. Owned state may include entities, companions, tentacles, visual effects, components, physics changes, statistics, globals, run flags and world changes.
-
-Perks remain per-player. Multiplayer synchronization should expose visible/shared consequences without turning one player's perk ownership into every player's perk ownership.
-
-5. Search
-~~~~~~~~~
-
-Large catalogs must remain searchable. Search supports translated names and relevant IDs/paths/descriptions depending on the tab. Exclusion tokens and normalized separators are part of the current search behavior.
-
-6. Creatures, objects and transformations
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-In the MOBS catalog, LMB spawns an entry and RMB transforms the current player into it. The system attempts to preserve useful native attacks, animations, movement, physics, presentation and authored behavior while disabling AI that would fight player controls or cause duplicate simulation.
-
-Compatibility is exact-path based. Do not add broad substring blacklists such as blocking every filename containing `physics`, `effect`, `sprite` or `body`. Different XML paths with the same basename are separate authored entities and must remain separate catalog entries.
-
-Known unsafe forms are recorded as exact paths. Known safe exceptions are also exact paths. Wrapper-to-canonical routing is permitted only for explicitly validated path pairs. Static XML analysis and native polymorph-table membership are useful signals, but a hard engine crash cannot be proven impossible by Lua `pcall` inside the same process.
-
-7. Returning to human form
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-While transformed, TAB must return the player to the normal human form. Native polymorph expiry is the primary route; serialized backup/hard recovery is a fallback. TAB return is a normal part of the form lifecycle, not an optional emergency-only feature.
-
-8. Death while transformed
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Fatal damage to a supported transformed body should kill the creature form while allowing the player to continue as a restored human at the death position instead of ending the run because the temporary body died.
-
-The form corpse should remain when appropriate. Human inventory and relevant player state should survive the handoff. The death flow is engine-sensitive, so recovery code must treat unknown player-authority states conservatively and must not destroy an entity when authority is uncertain.
-
-9. Special forms
-~~~~~~~~~~~~~~~~
-
-Boss Dragon, Maggot Tiny and other unusual families may require dedicated adapters. Do not re-enable original AI merely to make an adapter look simpler: competing AI can create duplicated attacks, movement conflicts, lag, physics problems and multiplayer desync.
-
-10. Possession
-~~~~~~~~~~~~~~
-
-The default possession key is G and can be changed in mod settings. The player points at a supported creature and takes over its compatible form. The original target is retired only after the transformation is confirmed. Possession should feel like taking the target's place, not creating an unrelated duplicate nearby.
-
-11. Effects
-~~~~~~~~~~~
-
-The Effects tab applies supported status/timed effects and removes them when safe. Removal must preserve unrelated protected/perk/internal effects and restore editor-owned state where possible.
-
-12. Weather
-~~~~~~~~~~~
-
-The Weather tab controls time presets, weather presets and advanced supported WorldState fields such as cloud cover, fog, wind, rain and lightning behavior. Weather state is synchronized through EW when EW is active. Host and peer users have equal MCM-facing rights; routing through a host is an implementation detail.
-
-13. World Rules
-~~~~~~~~~~~~~~~
-
-World Rules are reversible overrides rather than permanent save edits. NATIVE/RESET restores the baseline captured for state owned by MCM. Repeated multipliers must not accumulate against an already overridden value. Critical persistent values keep recovery information so a later Lua session can restore native state after an interrupted run.
-
-Expensive physics/entity scans must not run directly from GUI draw/click paths. UI actions update desired rule state; broad application belongs in bounded world-update work.
-
-14. Experimental multiplayer integration
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-MCM is primarily a standalone single-player mod. Entangled Worlds support is partial/experimental and does not guarantee every Noita/EW edge case. The integration aims to synchronize shared world effects such as items, supported spell operations, forms, possession, visible perk consequences, effects, weather, time and World Rules while keeping player-local ownership local when appropriate.
-
-All peers should use the same MCM build and a compatible EW version.
-
-15. Performance
-~~~~~~~~~~~~~~~
-
-The mod should remain usable on weaker systems. Avoid unnecessary work every frame, broad world scans, repeated XML parsing, eager loading of large catalogs and duplicated network simulation. Optimize without silently removing existing features or weakening recovery/synchronization guarantees.
-
-Refactoring and cleanup rules
------------------------------
-
-Current priority is reliability and maintainability of existing features, not feature count.
-
-- Preserve existing player-facing behavior.
-- Preserve standalone functionality without requiring Entangled Worlds.
-- Preserve experimental EW integration and equal user-facing host/peer rights.
-- Remove code only when it is demonstrably unused.
-- Remove duplication when behavior is preserved.
-- Split large modules by responsibility rather than by arbitrary line count.
-- Keep exact-path compatibility registries and protocol slots stable when they are part of compatibility.
-- Prefer behavior tests over tests that search for a particular source string.
-- Keep necessary Noita/EW workarounds until a tested replacement exists.
-- Treat current verified in-game behavior as a stronger source of truth than stale comments.
-
-Project architecture and code navigation
-----------------------------------------
-
-The architecture should answer: "If a specific gameplay aspect breaks, which folder owns it?"
-
-Top-level layers:
-
-- `files/features/` — MCM gameplay features.
-- `files/integrations/ew/` — code that exists specifically for Entangled Worlds / Noita Proxy: RPCs, network bridges, mailboxes, sync and EW-specific resilience patches.
-- `files/platform/noita/` — low-level Noita API/component adapters. UI should not directly implement Entity/Component transactions when a platform/feature boundary exists.
-- `files/ui/` — presentation, search, buttons, tabs, localization lookup and forwarding user intent to feature services.
-- `files/core/` — small generic algorithms/utilities with no gameplay or Noita ownership.
-- `files/diagnostics/` — bounded logs, passive observation and diagnostics.
-- `files/qa/` — in-game Z QA scenario infrastructure.
-- `tests/` — offline regression tests; `tests/TESTING.txt` documents the test system.
-
-`files/item_registry.lua` and `files/creature_registry.lua` intentionally remain stable extension entry points for external mods.
-
-Spells — `files/features/spells/`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- `catalog.lua` — vanilla spell catalog normalization, categories and icon metadata.
-- `service.lua` — wand contents, slots/capacity, add/replace/delete/drop, mana preservation and required EW sync.
-- `files/ui/tabs/spells.lua` — rendering, search and commands only.
-
-Items — `files/features/items/`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- `catalog.lua` — built-in item catalog.
-- `ui_catalog.lua` — combined built-in/external/liquid menu catalog.
-- `service.lua` — world spawn, RMB inventory delivery, world fallback and filled containers.
-- `liquid_preview.lua` — hidden probe used to obtain actual liquid colors.
-- `files/platform/noita/inventory_slots.lua` — low-level slot/pickup confirmation.
-- `files/integrations/ew/world_items.lua` — multiplayer world-item/inventory transport.
-- `files/ui/tabs/items.lua` — rendering and user commands.
-
-Perks — `files/features/perks/`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- `catalog.lua` — unique vanilla perk catalog.
-- `service.lua` — public spawn/apply/count/remove API and removal strategy selection.
-- `pickup_service.lua` — canonical application using a real perk entity.
-- `transactions.lua` — ownership journal for MCM-applied perk copies and rollback.
-- `transactions/global_journal.lua` — Globals/run-flag capture and restoration.
-- `inverse_registry.lua` — explicit inverse dispatcher.
-- `inverse/player.lua`, `inverse/world.lua`, `inverse/companions.lua`, `inverse/lukki.lua` — special inverse families.
-- `root_companions.lua` — ownership of detached companion entities.
-- `presentation.lua` — perk icons/GameEffect/presentation residue cleanup.
-- `files/ui/tabs/perks.lua` — menu only.
-
-Creatures — `files/features/creatures/`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- `catalog.lua` — known vanilla entity paths.
-- `catalog_builder.lua` — full catalog construction and incremental warmup.
-- `classification.lua` — structural independent-creature classification; filename hints are diagnostic only.
-- `compatibility.lua` — verified/candidate/unsafe/unsupported status without dangerous trial polymorph.
-- `compatibility_overrides.lua` — the manual exact-path safe/unsafe/canonical registry.
-- `metadata.lua` — XML/name metadata.
-- `diagnostics.lua` — deeper component/attack/profile inspection.
-- `ui_catalog.lua` — MOBS-ready data.
-- `service.lua` — collect/canonical/prewarm/spawn facade.
-- `files/ui/tabs/creatures.lua` — rendering, search and spawn/transform intent.
-
-Forms — `files/features/forms/`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-This is the most sensitive subsystem.
-
-- `manager.lua` — session lifecycle, transform start, TAB return, death handoff and transition coordination.
-- `runtime.lua` — active-form dispatcher.
-- `family.lua` — form family/lifecycle root detection.
-- `controls.lua` — normalized player input.
-- `combat.lua` — attacks, aiming, lasers and manual combat adapters.
-- `presentation.lua` — vision/herd/DamageModel/CharacterData/presentation profile.
-- `component_ops.lua` — shared component operations.
-- `entity_tree_cache.lua` — active-form entity-tree cache.
-- `adapters/ghost.lua`, `fish.lua`, `worm.lua`, `physics.lua`, `boss_dragon.lua` — family-specific behavior.
-- `exact_effects.lua` — exact polymorph wrappers/runtime clones.
-- `human_restore.lua` — serialized human restoration.
-- `player_authority.lua` — transactional authoritative-player switching.
-- `corpse_service.lua` — creature-form corpse lifecycle/synchronization.
-- `death_guard.lua` — native death callback boundary.
-- `transform_flash.lua` — temporary polymorph-flash ownership and restoration.
-- `profile.lua` — form profile analysis.
-- `noop.lua` — deliberately empty runtime script used when original lifecycle/AI must be disabled.
-
-Possession — `files/features/possession/`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- `keybinds.lua` — configured key handling.
-- `targeting.lua` — entity selection under the cursor, including EW tolerance/fallback.
-- `service.lua` — possession state machine/transaction.
-- `retirement.lua` — local target retirement.
-- `files/integrations/ew/possession_retire.lua` — network retire/handoff.
-
-Effects — `files/features/effects/`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- `catalog.lua` — available effects and metadata.
-- `policy.lua` — shared identity/safety policy.
-- `service.lua` — application, ownership, removal, expiry, snapshot and residue checks.
-- `files/ui/tabs/effects.lua` — presentation and commands.
-
-Weather — `files/features/weather/`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- `definitions.lua` — fields and presets.
-- `runtime_effects.lua` — rain/lightning/runtime weather effects.
-- `service.lua` — local user operations and ownership state.
-- `files/integrations/ew/weather_sync.lua` — network snapshot/mailbox and equal-rights routing.
-- `files/ui/tabs/weather.lua` — UI.
-
-World Rules — `files/features/world_rules/`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- `definitions.lua` — rule definitions and choices.
-- `service.lua` — public state machine and orchestration.
-- `world_state.lua` — WorldState fields.
-- `physics.lua` — bounded runtime physics/character gravity and damping ownership.
-- `stains.lua` — stain/status rules.
-- `magic_numbers.lua` — MagicNumbers ownership/transactions.
-- `gold_lifetime.lua` — gold lifetime behavior.
-- `files/integrations/ew/world_rules_sync.lua` — EW snapshots/mailbox/equal peer rights.
-- `files/ui/tabs/world_rules.lua` — UI.
-
-Companion — `files/features/companion/`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- `player_avatar.lua` — companion creation/lifecycle.
-- `ai.lua` — companion AI.
-- `health.lua` — narrow initial-health repair without undoing combat damage.
-- `spawn_guard.lua` — per-entity fallback for separate Lua VMs.
-- `player_clone.xml` — clone entity.
-- `files/integrations/ew/companion_request.lua` — peer-to-host request transport when required by EW.
-
-Entangled Worlds — `files/integrations/ew/`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- `bootstrap.lua` — small EW attachment point.
-- `runtime.lua` — EW discovery/common runtime capabilities.
-- `perk_sync.lua`, `world_items.lua`, `possession_retire.lua`, `companion_request.lua`, `weather_sync.lua`, `world_rules_sync.lua` — feature-specific integration.
-- `serialization.lua` — EW-specific serialized-data handling where needed.
-- `form_death_channel.lua` — CrossCall registration/sending for form death.
-- `resilience.lua` and `resilience_patches.lua` — version-sensitive EW compatibility patches and status reporting.
-- `bridge/protocol.lua` — RPC namespace/order/version; protocol slots must not move silently.
-- `bridge/*.lua` — feature-specific network bridges.
-
-A network bug should be debugged at the boundary between the feature and its EW bridge. Do not "fix" a network bug by adding a host-only user restriction.
-
-Noita platform — `files/platform/noita/`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- `inventory_slots.lua` — vanilla inventory mechanics.
-- `entity_tree.lua` — entity tree/root traversal.
-- `player_locator.lua` — local player lookup.
-- `keycodes.lua` — keycode normalization.
-- `input_guard.lua` — Alt-Tab/focus-gap action quarantine.
-- `localization.lua` — safe translation lookup.
-- `assets.lua` — asset/XML path helpers.
-- `patcher_bridge.lua` — NoitaPatcher capability provider/reuse.
-- `menu_inventory_guard.lua` — wheel-scroll conflict suppression and held-item restoration.
-
-Diagnostics — `files/diagnostics/`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- `logger.lua` — bounded persistent log and runtime error capture.
-- `entity_inspection.lua` — safe entity summaries/diagnostic storage reads.
-- `runtime_context.lua` — shared QA/EW runtime context.
-- `scan_support.lua`, `catalog_scanner.lua`, `runtime_scanner.lua`, `runtime_recorder.lua`, `scanner.lua`, `service.lua` — bounded diagnostics and performance sampling.
-
-QA — `files/qa/`
-~~~~~~~~~~~~~~~~~
-
-- `controller.lua` — Z key and lazy loading of the heavy runner.
-- `runner.lua` — sequential in-game regression scenario state machine.
-- `cases.lua` — required cases and timeouts.
-- `baselines.lua` — snapshots, rollback and residue checks.
-- `spell_roundtrip.lua` — spell-specific roundtrip scenario.
-
-The QA runner intentionally remains sequential because apply -> verify -> rollback order is part of test safety.
-
-Dependency rules
-~~~~~~~~~~~~~~~~
-
-1. UI displays state and calls feature services; it does not own gameplay transactions or network protocols.
-2. UI should not perform low-level Entity/Component mutations when a platform/feature boundary exists.
-3. `core` must not depend on Noita APIs, gameplay features, UI, EW or diagnostics.
-4. `platform` must not depend on gameplay features or EW.
-5. Gameplay features must not depend on UI, diagnostics or QA.
-6. EW-specific transport/mailbox/RPC belongs in `integrations/ew` rather than tabs or general gameplay services. A standalone entity script may inspect EW role only to prevent duplicate simulation, not to remove user rights.
-7. Large services/coordinators should be split by responsibility before becoming god objects.
-8. New production modules need a real runtime consumer unless they are documented stable extension points.
-9. External registry paths and wire-protocol slots are compatibility surfaces; do not rename/reorder them for aesthetics.
-10. Compatibility aliases may remain temporarily, but new internal names should describe actual roles (`service`, `runtime`, `catalog`, `sync`, `adapter`, etc.).
-11. Exported module tables should be named after responsibility rather than generic `api` in production code.
-12. Network mailbox/CrossCall implementation details belong to the integration layer.
-
-Testing
--------
-
-Static tests are appropriate for syntax, dependency direction, protocol layout, localization completeness and other genuinely static contracts. They should not replace behavior verification with fragile searches for a specific source line or documentation sentence.
-
-Prefer executable Lua mock/integration scenarios that load production modules, replace only the Noita/EW boundary and assert observable state/rollback/network results.
-
-After risky changes, run real in-game checks for transformations, TAB return, form death, Boss Dragon/Maggot Tiny, possession, inventory operations, perk apply/remove, effects, weather/World Rules synchronization, equal host/peer rights and rollback/residue cleanup.
-
-Completeness of this document
------------------------------
-
-This document focuses on behavior and ownership boundaries. Additional helper entities, caches, backup states, temporary components, RPCs, special-case handlers, compatibility patches, optimizations and diagnostics may exist to implement those behaviors.
-
-The goal of refactoring is not to minimize line count at any cost. The goal is a smaller, clearer, faster and more reliable project without losing verified functionality.
-
-Developer mode
---------------
-
-`dev_mode.lua` controls only built-in diagnostics/QA infrastructure.
-
-For normal play and public releases:
-
- dev_mode = 0
-
-At `0`, MCM does not load the runtime diagnostics service or Z QA controller, does not run their per-frame updates, does not show MOBS review/logging controls and does not load EW QA telemetry. The QA RPC slot remains reserved with a no-op handler so protocol ordering does not change.
-
-For development only:
-
- dev_mode = 1
-
-This enables persistent diagnostics, Z QA, MOBS review logging and EW QA telemetry. Offline tests in `tests/` are independent of `dev_mode` and are run manually. Restore `dev_mode = 0` before publishing a player build.
+Creator: zerodancing
diff --git a/metamorph_creative_menu/files/diagnostics/runtime_scanner.lua b/metamorph_creative_menu/files/diagnostics/runtime_scanner.lua
index 7fb6d54..31fd1c4 100644
--- a/metamorph_creative_menu/files/diagnostics/runtime_scanner.lua
+++ b/metamorph_creative_menu/files/diagnostics/runtime_scanner.lua
@@ -38,8 +38,8 @@ local function collect_world_and_runtime(report)
local ok_g, factor = pcall(rules.gravity_factor)
add(report, "INFO", "world_rules.gravity_factor", "ok=" .. tostring(ok_g) .. " factor=" .. tostring(factor))
end
- if type(rules.local_gravity_debug) == "function" then
- local ok_lg, dbg = pcall(rules.local_gravity_debug)
+ if type(rules.local_gravity_state) == "function" then
+ local ok_lg, dbg = pcall(rules.local_gravity_state)
if ok_lg and type(dbg) == "table" then
local rows = {}
for _, row in ipairs(dbg.rows or {}) do
@@ -95,8 +95,8 @@ local function collect_world_and_runtime(report)
" last=" .. one_line(GlobalsGetValue("mcm_remote_qa_last_v1", "")))
end
- if report.perk_service ~= nil and type(report.perk_service.root_companion_debug) == "function" then
- local ok_owned, value = pcall(report.perk_service.root_companion_debug)
+ if report.perk_service ~= nil and type(report.perk_service.root_companion_state) == "function" then
+ local ok_owned, value = pcall(report.perk_service.root_companion_state)
if ok_owned then add(report, "INFO", "perk.root_companion_ownership", tostring(value or "")) end
end
if report.perk_service ~= nil and type(report.perk_service.count) == "function" then
@@ -118,8 +118,8 @@ local function collect_world_and_runtime(report)
if ok then weather_values[#weather_values + 1] = tostring(field.id) .. "=" .. tostring(value) end
end
add(report, "INFO", "weather.values", sample_list(weather_values, 20))
- if type(report.weather.debug_state) == "function" then
- local ok_state, state = pcall(report.weather.debug_state)
+ if type(report.weather.state_snapshot) == "function" then
+ local ok_state, state = pcall(report.weather.state_snapshot)
if ok_state and type(state) == "table" then
add(report, "INFO", "weather.runtime",
"rainfall="..tostring(state.rainfall).." rain="..tostring(state.rain).." rain_target="..tostring(state.rain_target)..
diff --git a/metamorph_creative_menu/files/features/creatures/service.lua b/metamorph_creative_menu/files/features/creatures/service.lua
index fc1dd5f..dc3c307 100644
--- a/metamorph_creative_menu/files/features/creatures/service.lua
+++ b/metamorph_creative_menu/files/features/creatures/service.lua
@@ -2,7 +2,6 @@ local metadata = dofile("mods/metamorph_creative_menu/files/features/creatures/m
local classification = dofile("mods/metamorph_creative_menu/files/features/creatures/classification.lua")
local compatibility = dofile("mods/metamorph_creative_menu/files/features/creatures/compatibility.lua")
local transform_routing = dofile("mods/metamorph_creative_menu/files/features/creatures/transform_routing.lua")
-local creature_diagnostics = dofile("mods/metamorph_creative_menu/files/features/creatures/diagnostics.lua")
local catalog_builder = dofile("mods/metamorph_creative_menu/files/features/creatures/catalog_builder.lua")
local existing_creature_service = METAMORPH_CREATIVE_MENU_CREATURE_SERVICE or METAMORPH_CREATIVE_MENU_CREATURE_API
if type(existing_creature_service) == "table" then return existing_creature_service end
@@ -56,13 +55,6 @@ function creature_service.collect() return catalog_builder.collect() end
function creature_service.warmup_step(budget) return catalog_builder.warmup_step(budget) end
function creature_service.catalog_version() return catalog_builder.catalog_version() end
-function creature_service.collect_diagnostics()
- return creature_diagnostics.collect(creature_service)
-end
-
-function creature_service.diagnostic_info_for_path(path)
- return creature_diagnostics.info(creature_service, path)
-end
function creature_service.collect_prewarm_candidates()
-- Prewarm is intentionally broader than the lazy UI catalogue. Exact polymorph XML
diff --git a/metamorph_creative_menu/files/features/forms/corpse_service.lua b/metamorph_creative_menu/files/features/forms/corpse_service.lua
index 2f2101c..1502887 100644
--- a/metamorph_creative_menu/files/features/forms/corpse_service.lua
+++ b/metamorph_creative_menu/files/features/forms/corpse_service.lua
@@ -8,11 +8,6 @@ local POLYMORPH_EFFECTS = {
local pending_corpse_watch = {}
local pending_corpse_finalize = {}
-local function diagnostic_event(kind, details)
- if type(METAMORPH_CREATIVE_MENU_DIAGNOSTICS_EVENT) == "function" then
- pcall(METAMORPH_CREATIVE_MENU_DIAGNOSTICS_EVENT, tostring(kind or "FORM"), tostring(details or ""))
- end
-end
local CORPSE_PLAYER_TAGS = {
-- A dead player-form must become an ordinary world entity. In particular remove
@@ -148,10 +143,6 @@ local function detach(entity, source_path, reason, responsible)
born=frame, next_index=1, checks={1,3,10,60},
}
local gid = corpse_des_gid(entity)
- diagnostic_event("FORM CORPSE PENDING", string.format("entity=%s source=%s pos=%.1f,%.1f hp=%s/%s ew_synced=%s des=%s gid=%s release=%s",
- tostring(entity), tostring(source_path or ""), tonumber(x) or 0, tonumber(y) or 0,
- tostring(hp), tostring(max_hp), tostring(EntityHasTag(entity, "ew_synced")),
- tostring(EntityHasTag(entity, "ew_des")), tostring(gid or ""), tostring(frame + 1)))
return true
end
@@ -166,8 +157,6 @@ function corpse_service.update()
local entity = tonumber(record.entity) or 0
local alive = entity ~= 0 and EntityGetIsAlive(entity)
if not alive then
- diagnostic_event("FORM CORPSE FINALIZED", string.format("entity=%s source=%s age=%s native_death=true",
- tostring(entity), tostring(record.source or ""), tostring(frame - (tonumber(record.born) or frame))))
table.remove(pending_corpse_finalize, index)
elseif frame >= (tonumber(record.release_frame) or frame) and record.released ~= true then
local damage = EntityGetFirstComponentIncludingDisabled(entity, "DamageModelComponent")
@@ -177,9 +166,6 @@ function corpse_service.update()
record.released = true
record.release_frame = frame
pcall(EntityRemoveTag, entity, "metamorph_creative_menu_form_corpse_pending")
- diagnostic_event("FORM CORPSE RELEASE", string.format("entity=%s source=%s hp=%s/%s des=%s gid=%s",
- tostring(entity), tostring(record.source or ""), tostring(corpse_health(entity)),
- select(2, corpse_health(entity)), tostring(EntityHasTag(entity, "ew_des")), tostring(corpse_des_gid(entity) or "")))
elseif record.released == true and frame - (tonumber(record.release_frame) or frame) >= 3 and record.forced ~= true then
-- A few unusual entities cache the wait flag during the death callback. If
-- kill_now did not complete the death within three frames, clear the wait
@@ -193,7 +179,6 @@ function corpse_service.update()
end
freeze_pending_corpse(entity)
record.forced = true
- diagnostic_event("FORM CORPSE FORCE_RELEASE", string.format("entity=%s source=%s", tostring(entity), tostring(record.source or "")))
elseif record.released == true and frame - (tonumber(record.release_frame) or frame) >= 20 then
-- Last-resort protection: never leave a living detached player-form entity
-- immortal detached form in the world. For ordinary animals, replace only
@@ -203,8 +188,6 @@ function corpse_service.update()
-- consequential; a stuck body is removed instead.
local x, y = EntityGetTransform(entity)
local fallback = spawn_native_corpse_fallback(record, x, y)
- diagnostic_event("FORM CORPSE FALLBACK", string.format("old=%s fallback=%s source=%s",
- tostring(entity), tostring(fallback), tostring(record.source or "")))
if EntityGetIsAlive(entity) then pcall(EntityKill, entity) end
if fallback ~= 0 then
pending_corpse_watch[#pending_corpse_watch + 1] = {
@@ -227,10 +210,6 @@ function corpse_service.update()
local x, y, hp, max_hp = 0, 0, nil, nil
if alive then x, y = EntityGetTransform(entity); hp, max_hp = corpse_health(entity) end
local gid = alive and corpse_des_gid(entity) or nil
- diagnostic_event("FORM CORPSE WATCH", string.format("age=%s entity=%s alive=%s source=%s pos=%.1f,%.1f hp=%s/%s sync_request=%s des=%s gid=%s",
- tostring(check_at), tostring(entity), tostring(alive), tostring(record.source or ""), tonumber(x) or 0, tonumber(y) or 0,
- tostring(hp), tostring(max_hp), tostring(alive and EntityHasTag(entity, "ew_synced") or false),
- tostring(alive and EntityHasTag(entity, "ew_des") or false), tostring(gid or "")))
record.next_index = (record.next_index or 1) + 1
if not alive or record.next_index > #checks then table.remove(pending_corpse_watch, index) end
end
diff --git a/metamorph_creative_menu/files/features/forms/exact_effects.lua b/metamorph_creative_menu/files/features/forms/exact_effects.lua
index 2cd5a4d..33ecfbb 100644
--- a/metamorph_creative_menu/files/features/forms/exact_effects.lua
+++ b/metamorph_creative_menu/files/features/forms/exact_effects.lua
@@ -18,8 +18,8 @@ local function effect_xml(target)
end
local RUNTIME_ENTITY_CLONE_MODE = {
- -- `sheep.xml` is treated specially by Noita's polymorph path and repeatedly
- -- resolves to sheep_bat/sheep_fly in the user's build. A byte-identical copy keeps
+ -- `sheep.xml` is treated specially by Noita's polymorph path and can resolve
+ -- to sheep_bat/sheep_fly. A byte-identical copy keeps
-- the ordinary sheep data while removing the magic filename from the target.
["data/entities/animals/sheep.xml"] = "exact",
diff --git a/metamorph_creative_menu/files/features/forms/manager.lua b/metamorph_creative_menu/files/features/forms/manager.lua
index b37a60a..4ccf299 100644
--- a/metamorph_creative_menu/files/features/forms/manager.lua
+++ b/metamorph_creative_menu/files/features/forms/manager.lua
@@ -46,11 +46,6 @@ local session_counter = 0
local runtime_error_log = {}
local RUNTIME_ERROR_REPEAT_FRAMES = 600
-local function diagnostic_event(kind, details)
- if type(METAMORPH_CREATIVE_MENU_DIAGNOSTICS_EVENT) == "function" then
- pcall(METAMORPH_CREATIVE_MENU_DIAGNOSTICS_EVENT, tostring(kind or "FORM"), tostring(details or ""))
- end
-end
local function real_time_ms()
if type(GameGetRealWorldTimeSinceStarted) ~= "function" then return nil end
@@ -72,9 +67,6 @@ local function protected_runtime_call(label, fn, ...)
local last = runtime_error_log[signature]
if last == nil or frame - last >= RUNTIME_ERROR_REPEAT_FRAMES then
runtime_error_log[signature] = frame
- if type(METAMORPH_CREATIVE_MENU_DIAGNOSTICS_CAPTURE) == "function" then
- pcall(METAMORPH_CREATIVE_MENU_DIAGNOSTICS_CAPTURE, "form." .. tostring(label), message)
- end
print("[Metamorph: Creative Menu] " .. tostring(label) .. " failed: " .. message)
end
return false
@@ -195,9 +187,6 @@ function form_manager.handle_form_death(old_form, reason, responsible, damage, p
local source_path = session ~= nil and tostring(session.requested_target or session.target or "") or ""
local x0, y0 = 0, 0
if old_form ~= 0 and EntityGetIsAlive(old_form) then x0, y0 = EntityGetTransform(old_form) end
- diagnostic_event("FORM DEATH", string.format("entity=%s source=%s reason=%s responsible=%s damage=%s projectile=%s pos=%.1f,%.1f",
- tostring(old_form), source_path, tostring(reason or "death"), tostring(responsible or 0), tostring(damage), tostring(projectile or 0),
- tonumber(x0) or 0, tonumber(y0) or 0))
if session == nil then return false end
if session.kind ~= "polymorph"
@@ -240,9 +229,6 @@ function form_manager.handle_form_death(old_form, reason, responsible, damage, p
protect_restored_player(restored, 12)
local corpse_detached = detach_dead_form_as_corpse(old_form, source_path, reason, responsible)
local death_finished_ms = real_time_ms()
- diagnostic_event("FORM HANDOFF", string.format("old=%s restored=%s corpse_detached=%s elapsed_ms=%s",
- tostring(old_form), tostring(restored), tostring(corpse_detached),
- death_started_ms ~= nil and death_finished_ms ~= nil and string.format("%.2f", death_finished_ms-death_started_ms) or "nil"))
session = nil
pending_return_frame = nil
protected_runtime_call("form_runtime.reset", form_runtime.reset)
@@ -395,7 +381,6 @@ function form_manager.handle_tab_return(input_blocked)
pcall(ComponentSetValue2, component, "frames", 1)
end
pending_return_frame = GameGetFrameNum()
- if type(METAMORPH_CREATIVE_MENU_DIAGNOSTICS_USER_ACTION) == "function" then pcall(METAMORPH_CREATIVE_MENU_DIAGNOSTICS_USER_ACTION, "form.return_tab", "result=true") end
return true
end
diff --git a/metamorph_creative_menu/files/features/perks/locomotion_guard.lua b/metamorph_creative_menu/files/features/perks/locomotion_guard.lua
index 805cb23..b2f30a1 100644
--- a/metamorph_creative_menu/files/features/perks/locomotion_guard.lua
+++ b/metamorph_creative_menu/files/features/perks/locomotion_guard.lua
@@ -70,7 +70,7 @@ function locomotion_guard.rebind_player(old_player_entity_id, new_player_entity_
return true
end
-function locomotion_guard.debug_baseline_count()
+function locomotion_guard.baseline_count()
local count = 0
for _ in pairs(baseline_by_player) do count = count + 1 end
return count
diff --git a/metamorph_creative_menu/files/features/perks/nested_pickups.lua b/metamorph_creative_menu/files/features/perks/nested_pickups.lua
index c7e8798..6f9a63b 100644
--- a/metamorph_creative_menu/files/features/perks/nested_pickups.lua
+++ b/metamorph_creative_menu/files/features/perks/nested_pickups.lua
@@ -96,7 +96,7 @@ function nested_pickups.update()
end
end
-function nested_pickups.debug_state()
+function nested_pickups.state_snapshot()
local scopes, children = 0, 0
for _ in pairs(scopes_by_player) do scopes = scopes + 1 end
for _, rows in pairs(children_by_parent_transaction) do children = children + #(rows or {}) end
diff --git a/metamorph_creative_menu/files/features/perks/root_companions.lua b/metamorph_creative_menu/files/features/perks/root_companions.lua
index 183213b..b845d4d 100644
--- a/metamorph_creative_menu/files/features/perks/root_companions.lua
+++ b/metamorph_creative_menu/files/features/perks/root_companions.lua
@@ -248,7 +248,7 @@ function root_companions.owned_counts()
return result
end
-function root_companions.debug()
+function root_companions.ownership_summary()
local rows = {}
for perk_id in pairs(ROOT_COMPANION_SPECS) do
local owned_roots = owned_roots_by_perk[perk_id] or {}
diff --git a/metamorph_creative_menu/files/features/perks/service.lua b/metamorph_creative_menu/files/features/perks/service.lua
index f1a4650..60fcb2e 100644
--- a/metamorph_creative_menu/files/features/perks/service.lua
+++ b/metamorph_creative_menu/files/features/perks/service.lua
@@ -425,23 +425,23 @@ function perk_service.apply(player_entity_id, perk, options)
return true, "applied", tracked == true, track_reason
end
-function perk_service.root_companion_debug()
- return root_companions.debug()
+function perk_service.root_companion_state()
+ return root_companions.ownership_summary()
end
-function perk_service.debug_ownership_state()
+function perk_service.ownership_state()
local global_owners, flag_owners = 0, 0
- if type(transactions.debug_active_global_owners) == "function" then
- global_owners, flag_owners = transactions.debug_active_global_owners()
+ if type(transactions.active_global_owner_counts) == "function" then
+ global_owners, flag_owners = transactions.active_global_owner_counts()
end
return {
transactions = type(transactions.active_count) == "function" and transactions.active_count() or 0,
- mutations = type(transactions.debug_active_mutations) == "function" and transactions.debug_active_mutations() or 0,
+ mutations = type(transactions.active_mutation_count) == "function" and transactions.active_mutation_count() or 0,
global_owners = tonumber(global_owners) or 0,
run_flag_owners = tonumber(flag_owners) or 0,
- cleanup = type(transactions.debug_cleanup_state) == "function" and transactions.debug_cleanup_state() or {pending=0,failed=0},
- nested = type(nested_pickups.debug_state) == "function" and nested_pickups.debug_state() or {scopes=0,children=0},
- locomotion_baselines = type(locomotion_guard.debug_baseline_count)=="function" and locomotion_guard.debug_baseline_count() or 0,
+ cleanup = type(transactions.cleanup_state) == "function" and transactions.cleanup_state() or {pending=0,failed=0},
+ nested = type(nested_pickups.state_snapshot) == "function" and nested_pickups.state_snapshot() or {scopes=0,children=0},
+ locomotion_baselines = type(locomotion_guard.baseline_count)=="function" and locomotion_guard.baseline_count() or 0,
}
end
diff --git a/metamorph_creative_menu/files/features/perks/transactions.lua b/metamorph_creative_menu/files/features/perks/transactions.lua
index efd52db..d9ba30b 100644
--- a/metamorph_creative_menu/files/features/perks/transactions.lua
+++ b/metamorph_creative_menu/files/features/perks/transactions.lua
@@ -11,8 +11,8 @@ function perk_transactions.update()
pending_cleanup.update()
end
-function perk_transactions.debug_cleanup_state()
- return pending_cleanup.debug_state()
+function perk_transactions.cleanup_state()
+ return pending_cleanup.state_snapshot()
end
function perk_transactions.start_capture(token, environment)
@@ -428,13 +428,13 @@ end
function perk_transactions.clear() history = {} end
-function perk_transactions.debug_active_mutations()
- return mutation_journal.debug_active_properties()
+function perk_transactions.active_mutation_count()
+ return mutation_journal.active_property_count()
end
-function perk_transactions.debug_active_global_owners()
- if type(global_journal.debug_active_owners) ~= "function" then return 0, 0 end
- return global_journal.debug_active_owners()
+function perk_transactions.active_global_owner_counts()
+ if type(global_journal.active_owner_counts) ~= "function" then return 0, 0 end
+ return global_journal.active_owner_counts()
end
METAMORPH_CREATIVE_MENU_PERK_TRANSACTIONS = perk_transactions
diff --git a/metamorph_creative_menu/files/features/perks/transactions/global_journal.lua b/metamorph_creative_menu/files/features/perks/transactions/global_journal.lua
index 8821429..05230d9 100644
--- a/metamorph_creative_menu/files/features/perks/transactions/global_journal.lua
+++ b/metamorph_creative_menu/files/features/perks/transactions/global_journal.lua
@@ -308,7 +308,7 @@ function global_journal.discard_delta(delta)
end
end
-function global_journal.debug_active_owners()
+function global_journal.active_owner_counts()
local globals, flags = 0, 0
for _ in pairs(global_owners) do globals = globals + 1 end
for _ in pairs(run_flag_owners) do flags = flags + 1 end
diff --git a/metamorph_creative_menu/files/features/perks/transactions/mutation_journal.lua b/metamorph_creative_menu/files/features/perks/transactions/mutation_journal.lua
index f6905f6..6bc8572 100644
--- a/metamorph_creative_menu/files/features/perks/transactions/mutation_journal.lua
+++ b/metamorph_creative_menu/files/features/perks/transactions/mutation_journal.lua
@@ -560,7 +560,7 @@ function mutation_journal.rebuild_ownership(deltas)
end
end
-function mutation_journal.debug_active_properties()
+function mutation_journal.active_property_count()
local count = 0
for _ in pairs(property_owners) do count = count + 1 end
return count
diff --git a/metamorph_creative_menu/files/features/perks/transactions/pending_cleanup.lua b/metamorph_creative_menu/files/features/perks/transactions/pending_cleanup.lua
index e607839..51feff5 100644
--- a/metamorph_creative_menu/files/features/perks/transactions/pending_cleanup.lua
+++ b/metamorph_creative_menu/files/features/perks/transactions/pending_cleanup.lua
@@ -131,7 +131,7 @@ function pending_cleanup.update()
end
end
-function pending_cleanup.debug_state()
+function pending_cleanup.state_snapshot()
local pending_count, failed_count = 0, 0
local pending_items, failed_items = {}, {}
for key, record in pairs(pending) do
diff --git a/metamorph_creative_menu/files/features/possession/keybinds.lua b/metamorph_creative_menu/files/features/possession/keybinds.lua
index a969480..2a7fcf2 100644
--- a/metamorph_creative_menu/files/features/possession/keybinds.lua
+++ b/metamorph_creative_menu/files/features/possession/keybinds.lua
@@ -56,7 +56,6 @@ function possession_keybinds.update()
if not input_guard.actions_allowed() or inventory_open() then return false end
if not binding_just_down(binding) then return false end
local success, reason = possession.possess_under_cursor(player_locator.get())
- if type(METAMORPH_CREATIVE_MENU_DIAGNOSTICS_USER_ACTION) == "function" then pcall(METAMORPH_CREATIVE_MENU_DIAGNOSTICS_USER_ACTION, "possession", "binding="..tostring(binding.name).." result="..tostring(success).." reason="..tostring(reason)) end
if not success then
if reason == "no_target" then GamePrint(GameTextGetTranslatedOrNot("$mcm_possess_no_target"))
else GamePrint(GameTextGetTranslatedOrNot("$mcm_possess_failed") .. ": " .. tostring(reason or "")) end
diff --git a/metamorph_creative_menu/files/features/weather/service.lua b/metamorph_creative_menu/files/features/weather/service.lua
index d0c5e37..2810ab4 100644
--- a/metamorph_creative_menu/files/features/weather/service.lua
+++ b/metamorph_creative_menu/files/features/weather/service.lua
@@ -287,7 +287,7 @@ function weather_service.update()
if not state.remote then weather_sync.publish(state, world_component, false) end
end
-function weather_service.debug_state()
+function weather_service.state_snapshot()
local component = world_component()
local result = {
active = state.active == true,
diff --git a/metamorph_creative_menu/files/features/world_rules/physics.lua b/metamorph_creative_menu/files/features/world_rules/physics.lua
index 5ea6bd6..56117fd 100644
--- a/metamorph_creative_menu/files/features/world_rules/physics.lua
+++ b/metamorph_creative_menu/files/features/world_rules/physics.lua
@@ -528,7 +528,7 @@ function physics_adapter.has_persisted_local_recovery()
return recovery.has("player_gravity")
end
-function physics_adapter.debug_local_gravity(player, factor)
+function physics_adapter.local_gravity_state(player, factor)
local rows = {}
if player == nil or player == 0 or not EntityGetIsAlive(player) then return {player=0, factor=factor, rows=rows} end
for _, pair in ipairs({{"CharacterDataComponent","gravity"},{"CharacterPlatformingComponent","pixel_gravity"}}) do
diff --git a/metamorph_creative_menu/files/features/world_rules/service.lua b/metamorph_creative_menu/files/features/world_rules/service.lua
index 192c357..d6bd8ab 100644
--- a/metamorph_creative_menu/files/features/world_rules/service.lua
+++ b/metamorph_creative_menu/files/features/world_rules/service.lua
@@ -71,9 +71,6 @@ local function recover_stale_saved_overrides()
restore_loaded_gold_lifetimes_if_disabled()
-- Startup recovery removes stale state from a previous Lua/save session. It is not
-- a user edit and must not race a live EW host snapshot during peer reconnect.
- if type(METAMORPH_CREATIVE_MENU_DIAGNOSTICS_CAPTURE) == "function" then
- pcall(METAMORPH_CREATIVE_MENU_DIAGNOSTICS_CAPTURE, "world_rules.startup_recovery", "restored_stale_saved_overrides=true")
- end
return true
end
@@ -257,9 +254,6 @@ local function apply_network_snapshot(choices)
end
end
state.last_physics_scan = -100000
- if #failures > 0 and type(METAMORPH_CREATIVE_MENU_DIAGNOSTICS_CAPTURE) == "function" then
- pcall(METAMORPH_CREATIVE_MENU_DIAGNOSTICS_CAPTURE, "world_rules.snapshot_apply", table.concat(failures, ","))
- end
end
update_network_sync = function(frame)
@@ -273,8 +267,8 @@ end
function world_rule_service.gravity_factor() return physics_factor("physics_gravity") end
-function world_rule_service.local_gravity_debug()
- return physics_adapter.debug_local_gravity(player_locator.get(), world_rule_service.gravity_factor())
+function world_rule_service.local_gravity_state()
+ return physics_adapter.local_gravity_state(player_locator.get(), world_rule_service.gravity_factor())
end
function world_rule_service.step(rule, direction)
diff --git a/metamorph_creative_menu/files/integrations/ew/bootstrap.lua b/metamorph_creative_menu/files/integrations/ew/bootstrap.lua
index ea1d340..885415b 100644
--- a/metamorph_creative_menu/files/integrations/ew/bootstrap.lua
+++ b/metamorph_creative_menu/files/integrations/ew/bootstrap.lua
@@ -13,10 +13,6 @@ if not perk_guard_ok then
GlobalsSetValue("mcm_peer_perk_runtime_guard_v1", "failed:" .. tostring(perk_guard_reason))
end
local world_rules = dofile("mods/metamorph_creative_menu/files/integrations/ew/bridge/world_rules.lua")
-local dev_mode = tonumber(dofile("mods/metamorph_creative_menu/dev_mode.lua")) == 1
-local qa = dofile(dev_mode
- and "mods/metamorph_creative_menu/files/integrations/ew/bridge/qa.lua"
- or "mods/metamorph_creative_menu/files/integrations/ew/bridge/qa_reserved.lua")
local companion = dofile("mods/metamorph_creative_menu/files/integrations/ew/bridge/companion.lua")
local forms = dofile("mods/metamorph_creative_menu/files/integrations/ew/bridge/forms.lua")
local perks = dofile("mods/metamorph_creative_menu/files/integrations/ew/bridge/perks.lua")
@@ -24,11 +20,11 @@ local weather = dofile("mods/metamorph_creative_menu/files/integrations/ew/bridg
local possession = dofile("mods/metamorph_creative_menu/files/integrations/ew/bridge/possession.lua")
local items = dofile("mods/metamorph_creative_menu/files/integrations/ew/bridge/items.lua")
--- v3 RPC slots: 1 apply_rules, 2 request_rules, 3 QA telemetry, 4 companion,
--- 5 form pose, 6 reserved perk, 7 apply weather, 8 request weather,
--- 9 possession retirement, 10 reserved old light-form protocol.
+-- v3 RPC slots are positional. Slot 3 is reserved to preserve wire compatibility.
world_rules.register(rpc, common)
-qa.register(rpc, common)
+rpc.opts_reliable()
+rpc.opts_everywhere()
+function rpc.reserved_protocol_slot_3(...) end
companion.register(rpc, common)
forms.register_pose(rpc, common)
perks.register(rpc, common)
@@ -38,15 +34,9 @@ forms.register_reserved(rpc, common)
items.init(common)
local function publish_identity()
- local sent, received = forms.metrics()
GlobalsSetValue("mcm_world_rules_rpc_ready_v1", "1")
GlobalsSetValue("mcm_world_rules_rpc_my_id_v1", common.clean(ctx.my_id))
GlobalsSetValue("mcm_world_rules_rpc_host_id_v1", common.clean(ctx.host_id))
- GlobalsSetValue("mcm_form_pose_sent_v1", tostring(sent))
- GlobalsSetValue("mcm_form_pose_received_v1", tostring(received))
- GlobalsSetValue("mcm_form_sync_mode_v1", "native_full_entity_pose_v1")
- GlobalsSetValue("mcm_light_form_serialize_v1", "disabled_native")
- GlobalsSetValue("mcm_light_form_deserialize_v1", "disabled_native")
end
function ew_bootstrap.on_world_update()
@@ -58,7 +48,6 @@ function ew_bootstrap.on_world_update()
perks.update()
weather.update()
possession.update()
- qa.update(frame)
forms.update(frame)
end
diff --git a/metamorph_creative_menu/files/integrations/ew/bridge/forms.lua b/metamorph_creative_menu/files/integrations/ew/bridge/forms.lua
index 8f1c6a8..411eb7f 100644
--- a/metamorph_creative_menu/files/integrations/ew/bridge/forms.lua
+++ b/metamorph_creative_menu/files/integrations/ew/bridge/forms.lua
@@ -4,7 +4,6 @@ local last_remote_pose_frame = {}
local last_pose_send_frame = -100000
local last_remote_root_frame = {}
local remote_articulated_prepared = {}
-local form_pose_sent, form_pose_received = 0, 0
local function finite_number(value)
@@ -168,7 +167,6 @@ function forms_bridge.register_pose(shared_rpc, shared_common)
rotation, scale_x, scale_y = tonumber(rotation) or 0, tonumber(scale_x) or 1, tonumber(scale_y) or 1
if not finite_number(x) or not finite_number(y) or not finite_number(rotation) then return end
last_remote_pose_frame[sender] = frame
- form_pose_received = form_pose_received + 1
local kind = tonumber(motion_kind) or 0
local articulated = kind == 3 or kind == 5
@@ -253,11 +251,9 @@ local function send_form_pose(frame)
if ok and type(value) == "table" then phys_info = value end
end
end
- form_pose_sent = form_pose_sent + 1
rpc.sync_form_pose(frame, x, y, rotation or 0, scale_x or 1, scale_y or 1,
phys_info, motion_kind, motion_x, motion_y, motion_speed)
end
function forms_bridge.update(frame) send_form_pose(frame) end
-function forms_bridge.metrics() return form_pose_sent, form_pose_received end
return forms_bridge
diff --git a/metamorph_creative_menu/files/integrations/ew/resilience_patches.lua b/metamorph_creative_menu/files/integrations/ew/resilience_patches.lua
index 03ffa72..bb41564 100644
--- a/metamorph_creative_menu/files/integrations/ew/resilience_patches.lua
+++ b/metamorph_creative_menu/files/integrations/ew/resilience_patches.lua
@@ -31,9 +31,6 @@ local function CrossCall(name, ...)
local mcm_signature = tostring(name) .. "|" .. tostring(a)
if not mcm_crosscall_error_seen[mcm_signature] then
mcm_crosscall_error_seen[mcm_signature] = true
- if type(METAMORPH_CREATIVE_MENU_DIAGNOSTICS_CAPTURE) == "function" then
- pcall(METAMORPH_CREATIVE_MENU_DIAGNOSTICS_CAPTURE, "ew.crosscall." .. tostring(name), tostring(a))
- end
print("[Metamorph: Creative Menu] unexpected EW CrossCall failure " .. tostring(name) .. ": " .. tostring(a))
end
return nil
diff --git a/metamorph_creative_menu/files/integrations/ew/world_items.lua b/metamorph_creative_menu/files/integrations/ew/world_items.lua
index 680efb7..dcb28fd 100644
--- a/metamorph_creative_menu/files/integrations/ew/world_items.lua
+++ b/metamorph_creative_menu/files/integrations/ew/world_items.lua
@@ -43,9 +43,6 @@ function world_items.notify_world_item(entity)
if type(CrossCall) == "function" then
local crosscall_succeeded, crosscall_error = pcall(CrossCall, "ew_thrown", entity)
if crosscall_succeeded then return true, "direct" end
- if type(METAMORPH_CREATIVE_MENU_DIAGNOSTICS_CAPTURE) == "function" then
- pcall(METAMORPH_CREATIVE_MENU_DIAGNOSTICS_CAPTURE, "item.ew_thrown", tostring(crosscall_error))
- end
end
outbox_sequence = math.max(outbox_sequence, tonumber(GlobalsGetValue(OUTBOX_SEQUENCE_KEY, "0")) or 0) + 1
diff --git a/metamorph_creative_menu/files/integrations/ew/world_rules_sync.lua b/metamorph_creative_menu/files/integrations/ew/world_rules_sync.lua
index 602d361..76d0488 100644
--- a/metamorph_creative_menu/files/integrations/ew/world_rules_sync.lua
+++ b/metamorph_creative_menu/files/integrations/ew/world_rules_sync.lua
@@ -35,9 +35,6 @@ local function log_network_error(code, details)
local signature = tostring(code) .. "\31" .. tostring(details or "")
if last_network_error.signature == signature and frame - last_network_error.frame < 600 then return end
last_network_error.signature, last_network_error.frame = signature, frame
- if type(METAMORPH_CREATIVE_MENU_DIAGNOSTICS_CAPTURE) == "function" then
- pcall(METAMORPH_CREATIVE_MENU_DIAGNOSTICS_CAPTURE, "world_rules." .. tostring(code), tostring(details or ""))
- end
print("[Metamorph: Creative Menu] EW world-rules: " .. tostring(code) .. " - " .. tostring(details or ""))
end
diff --git a/metamorph_creative_menu/files/qa/baselines.lua b/metamorph_creative_menu/files/qa/baselines.lua
index a67698d..e4f3c83 100644
--- a/metamorph_creative_menu/files/qa/baselines.lua
+++ b/metamorph_creative_menu/files/qa/baselines.lua
@@ -166,8 +166,8 @@ local function perk_guard_snapshot(p)
local ok_owned, owned_counts=pcall(perk_root_companions.owned_counts)
if ok_owned and type(owned_counts)=="table" then result.owned_roots=owned_counts end
end
- if type(perk_service.debug_ownership_state)=="function" then
- local ok_state, ownership=pcall(perk_service.debug_ownership_state)
+ if type(perk_service.ownership_state)=="function" then
+ local ok_state, ownership=pcall(perk_service.ownership_state)
if ok_state and type(ownership)=="table" then result.ownership=ownership end
end
if type(EntityGetWithTag)=="function" then
diff --git a/metamorph_creative_menu/files/qa/runner.lua b/metamorph_creative_menu/files/qa/runner.lua
index 686fed6..ede5957 100644
--- a/metamorph_creative_menu/files/qa/runner.lua
+++ b/metamorph_creative_menu/files/qa/runner.lua
@@ -258,8 +258,8 @@ local function gravity_rule()
end
local function gravity_debug_ok(expected_factor)
- if type(world_rules.local_gravity_debug) ~= "function" then return false, "debug_api_missing" end
- local ok, info = pcall(world_rules.local_gravity_debug)
+ if type(world_rules.local_gravity_state) ~= "function" then return false, "debug_api_missing" end
+ local ok, info = pcall(world_rules.local_gravity_state)
if not ok or type(info) ~= "table" then return false, "debug_failed" end
local rows = info.rows or {}
if #rows == 0 then return false, "no_player_gravity_components" end
@@ -462,8 +462,8 @@ local function update_state()
local locked=weather.is_locked()
local weather_ok=state.pending_ok and locked
local extra=""
- if state.pending_name=="clear" and type(weather.debug_state)=="function" then
- local ok_state,dbg=pcall(weather.debug_state)
+ if state.pending_name=="clear" and type(weather.state_snapshot)=="function" then
+ local ok_state,dbg=pcall(weather.state_snapshot)
local rainfall=ok_state and type(dbg)=="table" and tonumber(dbg.rainfall) or nil
local rain=ok_state and type(dbg)=="table" and tonumber(dbg.rain) or nil
local target=ok_state and type(dbg)=="table" and tonumber(dbg.rain_target) or nil
diff --git a/metamorph_creative_menu/files/ui/menu_controller.lua b/metamorph_creative_menu/files/ui/menu_controller.lua
index 01f83b9..0cd84fa 100644
--- a/metamorph_creative_menu/files/ui/menu_controller.lua
+++ b/metamorph_creative_menu/files/ui/menu_controller.lua
@@ -3,7 +3,6 @@ if type(METAMORPH_CREATIVE_MENU_MENU_CONTROLLER) == "table" then return METAMORP
local menu_controller = {}
local ui = dofile("mods/metamorph_creative_menu/files/ui/runtime.lua")
-local audit = ui.audit
local player_locator = dofile("mods/metamorph_creative_menu/files/platform/noita/player_locator.lua")
local input_guard = dofile("mods/metamorph_creative_menu/files/platform/noita/input_guard.lua")
local keycodes = dofile("mods/metamorph_creative_menu/files/platform/noita/keycodes.lua")
@@ -37,7 +36,7 @@ local function update_emergency_open(player)
if not input_guard.actions_allowed() then return end
local key = keycodes.resolve("Key_TAB", "KEY_TAB"); if key == nil then return end
local ok, pressed = pcall(InputIsKeyJustDown, key)
- if ok and pressed == true then emergency_open = not emergency_open; audit("menu.emergency_toggle", "open="..tostring(emergency_open)) end
+ if ok and pressed == true then emergency_open = not emergency_open end
end
@@ -82,7 +81,7 @@ local function draw_tab_bar(width)
local title = ui.tr(tab.key, tab.fallback)
local clicked = ui.tile(0, 0, ui.EMPTY_SLOT, tab.icon, tab.fallback_icon or ui.EMPTY_SLOT,
title, "", active_tab == index, { target_size=18, max_scale=2.5, padding=1 })
- if clicked then active_tab = index; audit("menu.tab", "id="..tostring(tab.id)) end
+ if clicked then active_tab = index end
end
GuiLayoutEnd(ui.gui())
end
@@ -141,9 +140,6 @@ function menu_controller.draw()
local signature = tostring(err)
if tab_error_signatures[tab.id] ~= signature then
tab_error_signatures[tab.id] = signature
- if type(METAMORPH_CREATIVE_MENU_DIAGNOSTICS_CAPTURE) == "function" then
- pcall(METAMORPH_CREATIVE_MENU_DIAGNOSTICS_CAPTURE, "ui." .. tostring(tab.id), signature)
- end
print("[Metamorph: Creative Menu] UI tab '" .. tostring(tab.id) .. "' failed: " .. signature)
end
else
diff --git a/metamorph_creative_menu/files/ui/runtime.lua b/metamorph_creative_menu/files/ui/runtime.lua
index 5c8d9a0..5ed0e36 100644
--- a/metamorph_creative_menu/files/ui/runtime.lua
+++ b/metamorph_creative_menu/files/ui/runtime.lua
@@ -41,11 +41,6 @@ function ui_runtime.tr(key, fallback)
return value
end
-function ui_runtime.audit(action, details)
- if type(METAMORPH_CREATIVE_MENU_DIAGNOSTICS_USER_ACTION) == "function" then
- pcall(METAMORPH_CREATIVE_MENU_DIAGNOSTICS_USER_ACTION, action, details)
- end
-end
function ui_runtime.actions_allowed() return input_guard.actions_allowed() end
function ui_runtime.mark_hovered(value) if value == true then hovered = true end end
diff --git a/metamorph_creative_menu/files/ui/tabs/creatures.lua b/metamorph_creative_menu/files/ui/tabs/creatures.lua
index 37889e1..ba0998c 100644
--- a/metamorph_creative_menu/files/ui/tabs/creatures.lua
+++ b/metamorph_creative_menu/files/ui/tabs/creatures.lua
@@ -1,8 +1,5 @@
local creatures_tab = {}
-local DEV_MODE = METAMORPH_CREATIVE_MENU_DEV_MODE == true
-
local ui = dofile("mods/metamorph_creative_menu/files/ui/runtime.lua")
-local audit = ui.audit
local form_manager = dofile("mods/metamorph_creative_menu/files/features/forms/manager.lua")
local entity_catalog = dofile("mods/metamorph_creative_menu/files/features/creatures/ui_catalog.lua")
local player_avatar = dofile("mods/metamorph_creative_menu/files/features/companion/player_avatar.lua")
@@ -11,8 +8,6 @@ local creature_service, catalog, filters, filtered = nil, nil, nil, {}
local selected_filter = 1
local icon_cache = {}
local search = ""
-local review_mode = "play"
-local review_marks = {}
local search_cache_key = nil
local search_cache_value = nil
local warmup_cursor = 1
@@ -132,27 +127,8 @@ local function presentation(entry)
local text = tostring(entry.display_description or "")
if text ~= "" then text = text .. "\n" end
text = text .. tostring(entry.path or "")
- if review_mode == "play" then
- text = text .. "\n" .. ui.tr("$mcm_left_spawn", "LMB: spawn nearby")
- .. "\n" .. ui.tr("$mcm_right_transform", "RMB: transform")
- else
- local mode_key = "$mcm_mobs_mode_log_retest"
- local mode_fallback = "LOG RETEST"
- if review_mode == "safe" then mode_key, mode_fallback = "$mcm_mobs_mode_log_safe", "LOG SAFE" end
- if review_mode == "unsafe" then mode_key, mode_fallback = "$mcm_mobs_mode_log_unsafe", "LOG UNSAFE" end
- text = text .. "\n" .. ui.tr("$mcm_mobs_review_click", "CLICK: log") .. " " .. ui.tr(mode_key, mode_fallback)
- if creature_service ~= nil and type(creature_service.compatibility_status) == "function" then
- local checked, status, reason = pcall(creature_service.compatibility_status, entry.path)
- if checked then text = text .. "\nAUTO: " .. tostring(status or "unknown") .. " / " .. tostring(reason or "unknown") end
- end
- local marked = review_marks[tostring(entry.path)]
- if marked ~= nil then
- local marked_key, marked_fallback = "$mcm_mobs_mode_log_retest", "LOG RETEST"
- if marked == "safe" then marked_key, marked_fallback = "$mcm_mobs_mode_log_safe", "LOG SAFE" end
- if marked == "unsafe" then marked_key, marked_fallback = "$mcm_mobs_mode_log_unsafe", "LOG UNSAFE" end
- text = text .. "\n" .. ui.tr("$mcm_mobs_marked", "MARKED") .. ": " .. ui.tr(marked_key, marked_fallback)
- end
- end
+ .. "\n" .. ui.tr("$mcm_left_spawn", "LMB: spawn nearby")
+ .. "\n" .. ui.tr("$mcm_right_transform", "RMB: transform")
return { description = text }
end
@@ -170,7 +146,6 @@ local function transform_creature(player, creature)
local checked, status, reason = pcall(creature_service.compatibility_status, creature.path)
if checked and status == "unsafe" then
GamePrint(ui.tr("$mcm_creature_poly_failed", "Could not transform") .. ": " .. tostring(creature.display_name or creature.path))
- audit("creature.transform.blocked", "path="..tostring(creature.path).." reason="..tostring(reason or "known_unsafe"))
return false, tostring(reason or "known_unsafe")
end
end
@@ -188,27 +163,9 @@ local function transform_creature(player, creature)
local ok, canonical = pcall(creature_service.canonical_transform_path, creature.path)
if ok and type(canonical) == "string" and canonical ~= "" then target = canonical end
end
- -- Spawn identity and transform identity are intentionally separate for confirmed
- -- crash-prone placement wrappers: LMB keeps the authored biome XML, while player
- -- polymorph may use its same-species base body. requested_target preserves the
- -- exact menu/G identity for diagnostics and UI.
if form_manager.exact_effect_path_for_target(target) == nil then
pcall(form_manager.prepare_exact_effect_paths, {target})
end
- -- Persist intent before entering native polymorph code. If Noita hard-crashes during
- -- transformation, the diagnostics log still has the exact requested/canonical path
- -- and the non-destructive compatibility preflight verdict from before the attempt.
- local compatibility_status, compatibility_reason = "unknown", "unavailable"
- if creature_service ~= nil and type(creature_service.compatibility_status) == "function" then
- local checked, status, reason = pcall(creature_service.compatibility_status, creature.path)
- if checked then
- compatibility_status = tostring(status or "unknown")
- compatibility_reason = tostring(reason or "unknown")
- end
- end
- audit("creature.transform.begin", "path="..tostring(creature.path).." target="..tostring(target)
- .." transform_mode="..transform_mode.." transform_reason="..transform_reason
- .." compatibility="..compatibility_status.." compatibility_reason="..compatibility_reason)
local ok, reason = form_manager.transform_creature(player, target, nil, true, {
requested_target=creature.path,
compatibility_mode=transform_mode,
@@ -243,21 +200,7 @@ function creatures_tab.draw(player, panel_width, screen_height)
end
GuiLayoutEnd(ui.gui())
end
- if DEV_MODE then
- GuiLayoutBeginHorizontal(ui.gui(), 0, 0, true)
- if ui.button(0, 0, ui.tr("$mcm_mobs_mode_play", "PLAY"), review_mode == "play") then review_mode = "play" end
- if ui.button(0, 0, ui.tr("$mcm_mobs_mode_log_safe", "LOG SAFE"), review_mode == "safe") then review_mode = "safe" end
- if ui.button(0, 0, ui.tr("$mcm_mobs_mode_log_unsafe", "LOG UNSAFE"), review_mode == "unsafe") then review_mode = "unsafe" end
- if ui.button(0, 0, ui.tr("$mcm_mobs_mode_log_retest", "LOG RETEST"), review_mode == "retest") then review_mode = "retest" end
- GuiLayoutEnd(ui.gui())
- else
- review_mode = "play"
- end
- if review_mode == "play" then
- ui.white_text(0, 0, ui.tr("$mcm_creature_controls", "LMB: SPAWN RMB: TRANSFORM TAB: HUMAN"))
- else
- ui.white_text(0, 0, ui.tr("$mcm_mobs_review_help", "REVIEW: click a mob to log its exact path; no transform/spawn occurs"))
- end
+ ui.white_text(0, 0, ui.tr("$mcm_creature_controls", "LMB: SPAWN RMB: TRANSFORM TAB: HUMAN"))
search = ui.search_input(search, math.max(88, panel_width - 54), 64, "creatures")
local columns = ui.columns(panel_width)
@@ -279,25 +222,10 @@ function creatures_tab.draw(player, panel_width, screen_height)
ui.EMPTY_SLOT, icon(entry, false), CREATURE_ICON_FALLBACK, entry.display_name, p.description, false,
{target_size=18, max_scale=18, padding=0})
if hovered and icon_cache[tostring(entry.path)] == nil then icon(entry, true) end
- if review_mode ~= "play" and (clicked or right) then
- review_marks[tostring(entry.path)] = review_mode
- local compatibility_status, compatibility_reason = "unknown", "unavailable"
- if creature_service ~= nil and type(creature_service.compatibility_status) == "function" then
- local checked, status, reason = pcall(creature_service.compatibility_status, entry.path)
- if checked then compatibility_status, compatibility_reason = tostring(status or "unknown"), tostring(reason or "unknown") end
- end
- audit("creature.review", "verdict="..tostring(review_mode).." path="..tostring(entry.path).." id="..tostring(entry.id or "")
- .." name="..tostring(entry.display_name or "").." auto="..compatibility_status.." auto_reason="..compatibility_reason)
- local label = ui.tr("$mcm_mobs_mode_log_retest", "LOG RETEST")
- if review_mode == "safe" then label = ui.tr("$mcm_mobs_mode_log_safe", "LOG SAFE") end
- if review_mode == "unsafe" then label = ui.tr("$mcm_mobs_mode_log_unsafe", "LOG UNSAFE") end
- GamePrint(ui.tr("$mcm_mobs_review_logged", "MOBS REVIEW") .. " " .. label .. ": " .. tostring(entry.path))
- elseif clicked then
- local ok=spawn(player, entry)
- audit("creature.spawn", "path="..tostring(entry.path).." result="..tostring(ok))
+ if clicked then
+ spawn(player, entry)
elseif right then
- local ok, reason=transform(player, entry)
- audit("creature.transform", "path="..tostring(entry.path).." result="..tostring(ok).." reason="..tostring(reason))
+ transform(player, entry)
end
end
GuiEndScrollContainer(ui.gui())
diff --git a/metamorph_creative_menu/files/ui/tabs/effects.lua b/metamorph_creative_menu/files/ui/tabs/effects.lua
index 81b4254..dbfcba7 100644
--- a/metamorph_creative_menu/files/ui/tabs/effects.lua
+++ b/metamorph_creative_menu/files/ui/tabs/effects.lua
@@ -1,7 +1,6 @@
local effects_tab = {}
local ui = dofile("mods/metamorph_creative_menu/files/ui/runtime.lua")
-local audit = ui.audit
local effect_service = dofile("mods/metamorph_creative_menu/files/features/effects/service.lua")
local catalog = nil
@@ -65,7 +64,6 @@ function effects_tab.draw(player, panel_width, screen_height)
if ui.button(0,0," + ") then duration_index=math.min(#DURATIONS,duration_index+1) end
if ui.button(0,0,ui.tr("$mcm_effect_remove_all","REMOVE ALL")) then
local removed=effect_service.remove_all(player)
- audit("effect.remove_all", "removed="..tostring(removed))
end
GuiLayoutEnd(ui.gui())
search = ui.search_input(search, math.max(88, panel_width - 54), 64, "effects")
@@ -93,10 +91,8 @@ function effects_tab.draw(player, panel_width, screen_height)
if clicked then
local frames=entry.kind=="status" and nil or d.frames
local ok,reason=effect_service.add(player,entry,frames)
- audit("effect.add", "id="..tostring(entry.id or entry.path).." result="..tostring(ok).." reason="..tostring(reason))
elseif right then
local removed=effect_service.remove(player,entry)
- audit("effect.remove", "id="..tostring(entry.id or entry.path).." removed="..tostring(removed))
end
end
end
diff --git a/metamorph_creative_menu/files/ui/tabs/items.lua b/metamorph_creative_menu/files/ui/tabs/items.lua
index cae6058..e6b66ed 100644
--- a/metamorph_creative_menu/files/ui/tabs/items.lua
+++ b/metamorph_creative_menu/files/ui/tabs/items.lua
@@ -1,7 +1,6 @@
local items_tab = {}
local ui = dofile("mods/metamorph_creative_menu/files/ui/runtime.lua")
-local audit = ui.audit
local item_service = dofile("mods/metamorph_creative_menu/files/features/items/service.lua")
local item_catalog = dofile("mods/metamorph_creative_menu/files/features/items/ui_catalog.lua")
local liquid_preview = dofile("mods/metamorph_creative_menu/files/features/items/liquid_preview.lua")
@@ -132,7 +131,6 @@ function items_tab.draw(player, panel_width, screen_height)
if is_liquid then
if clicked or right then
local ok, reason, entity = item_service.spawn_filled_flask(player, entry.id, right)
- audit(right and "item.take_liquid" or "item.spawn_liquid", "id="..tostring(entry.id).." result="..tostring(ok).." reason="..tostring(reason))
if not ok and reason == "full" then
GamePrint(ui.tr("$mcm_inventory_full_spawned", "Inventory full — spawned nearby") .. ": " .. entry.display_name)
elseif not ok then
@@ -142,7 +140,6 @@ function items_tab.draw(player, panel_width, screen_height)
else
if clicked then
local entity, reason = item_service.spawn_near(player, entry.path)
- audit("item.spawn", "path="..tostring(entry.path).." entity="..tostring(entity).." reason="..tostring(reason))
if entity ~= 0 then
GamePrint(ui.tr("$mcm_created_nearby", "Spawned nearby") .. ": " .. entry.display_name)
else
@@ -150,7 +147,6 @@ function items_tab.draw(player, panel_width, screen_height)
end
elseif right then
local ok, reason, entity, spawned_nearby = item_service.give(player, entry.path, entry.category ~= "WANDS")
- audit("item.take", "path="..tostring(entry.path).." result="..tostring(ok).." reason="..tostring(reason).." entity="..tostring(entity))
if ok then
GamePrint(ui.tr("$mcm_received", "Received") .. ": " .. entry.display_name)
elseif reason == "full" then
diff --git a/metamorph_creative_menu/files/ui/tabs/perks.lua b/metamorph_creative_menu/files/ui/tabs/perks.lua
index afa481a..c6d4882 100644
--- a/metamorph_creative_menu/files/ui/tabs/perks.lua
+++ b/metamorph_creative_menu/files/ui/tabs/perks.lua
@@ -1,7 +1,6 @@
local perks_tab = {}
local ui = dofile("mods/metamorph_creative_menu/files/ui/runtime.lua")
-local audit = ui.audit
local perk_service = dofile("mods/metamorph_creative_menu/files/features/perks/service.lua")
local perk_catalog = dofile("mods/metamorph_creative_menu/files/features/perks/catalog.lua")
@@ -131,15 +130,13 @@ function perks_tab.draw(player, panel_width, screen_height)
if remove_mode then
if clicked and count > 0 then
local ok, reason = perk_service.remove_one(player, perk.data)
- audit("perk.remove_one", "id="..tostring(perk.id).." result="..tostring(ok).." reason="..tostring(reason).." scope=peer_local")
if not ok then GamePrint(ui.tr("$mcm_perk_remove_failed", "Could not safely remove perk") .. ": " .. perk.name) end
elseif right and count > 0 then
local removed, reason = perk_service.remove_all(player, perk.data)
- audit("perk.remove_all", "id="..tostring(perk.id).." removed="..tostring(removed).." reason="..tostring(reason).." scope=peer_local")
if removed == 0 then GamePrint(ui.tr("$mcm_perk_remove_failed", "Could not safely remove perk") .. ": " .. perk.name) end
end
else
- if clicked then local ok=apply_or_spawn(player, perk, false); audit("perk.spawn", "id="..tostring(perk.id).." result="..tostring(ok)) elseif right then local ok=apply_or_spawn(player, perk, true); audit("perk.take", "id="..tostring(perk.id).." result="..tostring(ok)) end
+ if clicked then apply_or_spawn(player, perk, false) elseif right then apply_or_spawn(player, perk, true) end
end
end
GuiEndScrollContainer(ui.gui())
diff --git a/metamorph_creative_menu/files/ui/tabs/spells.lua b/metamorph_creative_menu/files/ui/tabs/spells.lua
index e94dad3..11d56e6 100644
--- a/metamorph_creative_menu/files/ui/tabs/spells.lua
+++ b/metamorph_creative_menu/files/ui/tabs/spells.lua
@@ -1,7 +1,6 @@
local spells_tab = {}
local ui = dofile("mods/metamorph_creative_menu/files/ui/runtime.lua")
-local audit = ui.audit
local spell_service = dofile("mods/metamorph_creative_menu/files/features/spells/service.lua")
local spell_catalog = dofile("mods/metamorph_creative_menu/files/features/spells/catalog.lua")
@@ -120,11 +119,9 @@ function spells_tab.draw(player, panel_width, screen_height)
end
if ui.button(0, 0, ui.tr("$mcm_delete", "DELETE")) and slots[selected_slot] ~= nil then
local ok = spell_service.remove(player, wand, slots[selected_slot], entries, false)
- audit("spell.delete", "slot="..tostring(selected_slot+1).." result="..tostring(ok))
end
if ui.button(0, 0, ui.tr("$mcm_drop", "DROP")) and slots[selected_slot] ~= nil then
local ok = spell_service.remove(player, wand, slots[selected_slot], entries, true)
- audit("spell.drop", "slot="..tostring(selected_slot+1).." result="..tostring(ok))
end
GuiLayoutEnd(ui.gui())
@@ -141,7 +138,6 @@ function spells_tab.draw(player, panel_width, screen_height)
if clicked then selected_slot = slot elseif right and entry ~= nil then
selected_slot = slot
local ok = spell_service.remove(player, wand, entry, entries, true)
- audit("spell.drop", "slot="..tostring(slot+1).." result="..tostring(ok))
end
end
GuiLayoutEnd(ui.gui())
@@ -174,7 +170,6 @@ function spells_tab.draw(player, panel_width, screen_height)
if picked ~= nil then
local ok = spell_service.replace(player, wand, selected_slot, picked.id, slots[selected_slot], entries)
- audit("spell.replace", "slot="..tostring(selected_slot+1).." action="..tostring(picked.id).." result="..tostring(ok))
end
end
diff --git a/metamorph_creative_menu/files/ui/tabs/weather.lua b/metamorph_creative_menu/files/ui/tabs/weather.lua
index 3bb0055..a357f48 100644
--- a/metamorph_creative_menu/files/ui/tabs/weather.lua
+++ b/metamorph_creative_menu/files/ui/tabs/weather.lua
@@ -1,24 +1,13 @@
local weather_tab = {}
local ui = dofile("mods/metamorph_creative_menu/files/ui/runtime.lua")
-local audit = ui.audit
local weather_service = dofile("mods/metamorph_creative_menu/files/features/weather/service.lua")
local advanced=false
local function time(name)
- local ok,reason=weather_service.set_time_preset(name)
- audit("weather.time", "preset="..tostring(name).." result="..tostring(ok).." reason="..tostring(reason))
+ weather_service.set_time_preset(name)
end
local function preset(name)
- local ok,reason=weather_service.apply_preset(name)
- local detail=""
- if type(weather_service.debug_state)=="function" then
- local good,state=pcall(weather_service.debug_state)
- if good and type(state)=="table" then
- detail=" rainfall="..tostring(state.rainfall).." rain="..tostring(state.rain).." rain_target="..tostring(state.rain_target)
- .." last_emit="..tostring(state.last_rain_emit_frame).." stop_guard_until="..tostring(state.rain_stop_guard_until)
- end
- end
- audit("weather.preset", "preset="..tostring(name).." result="..tostring(ok).." reason="..tostring(reason)..detail)
+ weather_service.apply_preset(name)
end
local function tile(icon,key,fallback,fn)
local clicked=ui.tile(0,0,ui.EMPTY_SLOT,icon,ui.EMPTY_SLOT,ui.tr(key,fallback),"",false,{target_size=18,max_scale=2.5})
@@ -45,12 +34,12 @@ function weather_tab.draw(_, panel_width, screen_height)
GuiLayoutEnd(ui.gui())
GuiLayoutBeginHorizontal(ui.gui(),0,3,true)
if ui.button(0,0,ui.tr("$mcm_weather_advanced","ADVANCED")) then advanced=true end
- if weather_service.is_locked() and ui.button(0,0,ui.tr("$mcm_weather_release","RELEASE")) then local ok=weather_service.release(); audit("weather.release", "result="..tostring(ok)) end
+ if weather_service.is_locked() and ui.button(0,0,ui.tr("$mcm_weather_release","RELEASE")) then weather_service.release() end
GuiLayoutEnd(ui.gui())
else
GuiLayoutBeginHorizontal(ui.gui(),0,0,true)
if ui.button(0,0,ui.tr("$mcm_weather_back","BACK")) then advanced=false end
- if weather_service.is_locked() and ui.button(0,0,ui.tr("$mcm_weather_release","RELEASE")) then local ok=weather_service.release(); audit("weather.release", "result="..tostring(ok)) end
+ if weather_service.is_locked() and ui.button(0,0,ui.tr("$mcm_weather_release","RELEASE")) then weather_service.release() end
GuiLayoutEnd(ui.gui())
for _,field in ipairs(weather_service.fields()) do
local value=weather_service.get(field)
diff --git a/metamorph_creative_menu/files/ui/tabs/world_rules.lua b/metamorph_creative_menu/files/ui/tabs/world_rules.lua
index b4cc1e7..402d8f4 100644
--- a/metamorph_creative_menu/files/ui/tabs/world_rules.lua
+++ b/metamorph_creative_menu/files/ui/tabs/world_rules.lua
@@ -1,6 +1,5 @@
local world_rules_tab = {}
local ui = dofile("mods/metamorph_creative_menu/files/ui/runtime.lua")
-local audit = ui.audit
local world_rule_service = dofile("mods/metamorph_creative_menu/files/features/world_rules/service.lua")
local search = ""
@@ -35,10 +34,6 @@ local function run_rule_action(action_name, fn, ...)
local call_ok, result, reason = pcall(fn, ...)
if not call_ok then
last_action_error = tostring(result)
- audit(action_name, "result=false reason=exception:" .. last_action_error)
- if type(METAMORPH_CREATIVE_MENU_DIAGNOSTICS_CAPTURE) == "function" then
- pcall(METAMORPH_CREATIVE_MENU_DIAGNOSTICS_CAPTURE, "ui.rules.action", action_name .. ":" .. last_action_error)
- end
return false, "exception"
end
if result == true then
@@ -56,7 +51,6 @@ function world_rules_tab.draw(_, panel_width, screen_height)
ui.white_text(0, 1, ui.tr("$mcm_rules_title", "WORLD RULES"))
if world_rule_service.has_overrides() and ui.button(0, 0, ui.tr("$mcm_rules_reset", "RESET")) and can_edit then
local ok, reason = run_rule_action("rules.reset", world_rule_service.reset)
- audit("rules.reset", "result="..tostring(ok).." reason="..tostring(reason))
end
GuiLayoutEnd(ui.gui())
if not can_edit then ui.white_text(0, 0, ui.tr("$mcm_rules_unavailable", "World-rule editing unavailable")) end
@@ -87,7 +81,6 @@ function world_rules_tab.draw(_, panel_width, screen_height)
if can_edit and (clicked or right) then
local ok, reason
ok, reason = run_rule_action("rule.step", world_rule_service.step, rule, right and -1 or 1)
- audit("rule.step", "id="..tostring(rule.id).." direction="..tostring(right and -1 or 1).." result="..tostring(ok).." reason="..tostring(reason))
end
end
end
diff --git a/metamorph_creative_menu/init.lua b/metamorph_creative_menu/init.lua
index 9be7c29..1367e22 100644
--- a/metamorph_creative_menu/init.lua
+++ b/metamorph_creative_menu/init.lua
@@ -1,15 +1,9 @@
dofile_once("data/scripts/lib/utilities.lua")
-local dev_mode_value = dofile("mods/metamorph_creative_menu/dev_mode.lua")
-local DEV_MODE = tonumber(dev_mode_value) == 1
-METAMORPH_CREATIVE_MENU_DEV_MODE = DEV_MODE
-
-- init.lua is intentionally only the lifecycle/composition root. Feature state and
-- behavior live in services, form adapters and UI tab modules under files/.
local localization = dofile("mods/metamorph_creative_menu/files/platform/noita/localization.lua")
local input_guard = dofile("mods/metamorph_creative_menu/files/platform/noita/input_guard.lua")
-local diagnostics = DEV_MODE and dofile("mods/metamorph_creative_menu/files/diagnostics/service.lua") or nil
-local qa_controller = DEV_MODE and dofile("mods/metamorph_creative_menu/files/qa/controller.lua") or nil
local form_manager = dofile("mods/metamorph_creative_menu/files/features/forms/manager.lua")
local weather = dofile("mods/metamorph_creative_menu/files/features/weather/service.lua")
local world_rules = dofile("mods/metamorph_creative_menu/files/features/world_rules/service.lua")
@@ -40,34 +34,21 @@ function OnModPreInit()
end
function OnModPostInit()
- local seed_fallbacks, biome_fallbacks, crosscall_guards = ew_resilience.post_init()
- if (tonumber(seed_fallbacks) or 0) > 0 or (tonumber(biome_fallbacks) or 0) > 0
- or (tonumber(crosscall_guards) or 0) > 0
- then
- print("[" .. MOD_NAME .. "] EW resilience: seed=" .. tostring(seed_fallbacks)
- .. ", biome=" .. tostring(biome_fallbacks) .. ", crosscall=" .. tostring(crosscall_guards))
- end
+ ew_resilience.post_init()
-- Noita resolves polymorph effect entity paths early enough that publishing the
-- generated XML only from a menu click is not reliable on every runtime path.
-- Prewarm the exact effect wrappers after all mods finish VFS setup; transform_creature()
-- keeps its lazy publisher as a fallback for dynamically-added creature paths.
local ok_forms, prepared = pcall(form_manager.prepare_exact_effect_paths_from_catalog)
if not ok_forms then
- if type(METAMORPH_CREATIVE_MENU_DIAGNOSTICS_CAPTURE) == "function" then
- pcall(METAMORPH_CREATIVE_MENU_DIAGNOSTICS_CAPTURE, "init.polymorph_prewarm", tostring(prepared))
- end
print("[" .. MOD_NAME .. "] polymorph prewarm failed: " .. tostring(prepared))
- elseif tonumber(prepared) ~= nil then
- print("[" .. MOD_NAME .. "] polymorph effects prepared: " .. tostring(prepared))
end
end
function OnWorldPreUpdate()
input_guard.update()
- if diagnostics ~= nil then diagnostics.update() end
effect_service.update()
- if qa_controller ~= nil then qa_controller.update() end
weather.update()
world_rules.update()
ew_perk_sync.update()
diff --git a/metamorph_creative_menu/mod.xml b/metamorph_creative_menu/mod.xml
index 78a53ba..f1d6b3b 100644
--- a/metamorph_creative_menu/mod.xml
+++ b/metamorph_creative_menu/mod.xml
@@ -1,7 +1,6 @@
diff --git a/metamorph_creative_menu/tests/architecture_contract.py b/metamorph_creative_menu/tests/architecture_contract.py
index a54f2fc..3bd0932 100644
--- a/metamorph_creative_menu/tests/architecture_contract.py
+++ b/metamorph_creative_menu/tests/architecture_contract.py
@@ -142,10 +142,17 @@ def visit(node):
all_runtime_sources.append((path, read(path)))
allowed_root_lua = {'item_registry.lua', 'creature_registry.lua'}
+developer_only_modules = {
+ 'files/features/creatures/diagnostics.lua',
+ 'files/integrations/ew/bridge/qa.lua',
+ 'files/integrations/ew/bridge/qa_reserved.lua',
+}
for path in sorted((root / 'files').rglob('*.lua')):
relative = path.relative_to(root).as_posix()
if path.parent == root / 'files' and path.name in allowed_root_lua:
continue
+ if relative.startswith('files/diagnostics/') or relative.startswith('files/qa/') or relative in developer_only_modules:
+ continue
runtime_path = 'mods/metamorph_creative_menu/' + relative
referenced = any(runtime_path in source for source_path, source in all_runtime_sources if source_path != path)
if not referenced:
diff --git a/metamorph_creative_menu/tests/dev_mode_runtime_gate_mock.lua b/metamorph_creative_menu/tests/dev_mode_runtime_gate_mock.lua
index 08b530a..d1c3f04 100644
--- a/metamorph_creative_menu/tests/dev_mode_runtime_gate_mock.lua
+++ b/metamorph_creative_menu/tests/dev_mode_runtime_gate_mock.lua
@@ -1,50 +1,7 @@
local root=assert(arg[1],"root required")
-local native_dofile=dofile
-local packaged_mode=native_dofile(root.."/dev_mode.lua")
-assert(tonumber(packaged_mode)==0,"Steam package dev_mode.lua must default to 0")
-
-local function run(mode)
- local loaded={}
- local calls={diagnostics=0,qa=0}
- local stub={}
- stub["mods/metamorph_creative_menu/files/platform/noita/localization.lua"]={register=function() end}
- stub["mods/metamorph_creative_menu/files/platform/noita/input_guard.lua"]={update=function() end,blocked=function() return false end}
- stub["mods/metamorph_creative_menu/files/diagnostics/service.lua"]={update=function() calls.diagnostics=calls.diagnostics+1 end}
- stub["mods/metamorph_creative_menu/files/qa/controller.lua"]={update=function() calls.qa=calls.qa+1 end}
- stub["mods/metamorph_creative_menu/files/features/forms/manager.lua"]={current_player=function() return 1 end,update=function() end,draw_form_health=function() end,post_update=function() end,handle_tab_return=function() return false end,prepare_exact_effect_paths_from_catalog=function() return 0 end}
- stub["mods/metamorph_creative_menu/files/features/weather/service.lua"]={update=function() end}
- stub["mods/metamorph_creative_menu/files/features/world_rules/service.lua"]={update=function() end,post_update=function() end}
- stub["mods/metamorph_creative_menu/files/ui/menu_controller.lua"]={draw=function() end,post_update=function() end}
- stub["mods/metamorph_creative_menu/files/features/possession/keybinds.lua"]={update=function() end}
- stub["mods/metamorph_creative_menu/files/integrations/ew/resilience.lua"]={pre_init=function() end,post_init=function() return 0,0,0 end}
- stub["mods/metamorph_creative_menu/files/integrations/ew/perk_sync.lua"]={update=function() end}
- stub["mods/metamorph_creative_menu/files/features/perks/service.lua"]={update=function() end}
- stub["mods/metamorph_creative_menu/files/features/effects/service.lua"]={update=function() end}
- stub["mods/metamorph_creative_menu/files/features/companion/player_avatar.lua"]={update=function() end}
- local old_dofile,old_once=dofile,dofile_once
- dofile_once=function() end
- dofile=function(path)
- loaded[path]=(loaded[path] or 0)+1
- if path=="mods/metamorph_creative_menu/dev_mode.lua" then return mode end
- if stub[path] then return stub[path] end
- error("unexpected init dependency: "..tostring(path))
- end
- ModLuaFileAppend=function() end; ModIsEnabled=function() return false end; ModDoesFileExist=function() return false end
- print=function() end
- METAMORPH_CREATIVE_MENU_DEV_MODE=nil
- assert(loadfile(root.."/init.lua"))()
- OnWorldPreUpdate()
- dofile,dofile_once=old_dofile,old_once
- return loaded,calls,METAMORPH_CREATIVE_MENU_DEV_MODE
-end
-
-local release_loaded,release_calls,release_flag=run(0)
-assert(release_flag==false,"dev_mode=0 did not expose release flag")
-assert(release_loaded["mods/metamorph_creative_menu/files/diagnostics/service.lua"]==nil,"release mode loaded diagnostics service")
-assert(release_loaded["mods/metamorph_creative_menu/files/qa/controller.lua"]==nil,"release mode loaded Z QA controller")
-assert(release_calls.diagnostics==0 and release_calls.qa==0,"release mode executed diagnostics/QA update")
-local dev_loaded,dev_calls,dev_flag=run(1)
-assert(dev_flag==true,"dev_mode=1 did not expose developer flag")
-assert(dev_loaded["mods/metamorph_creative_menu/files/diagnostics/service.lua"]==1 and dev_loaded["mods/metamorph_creative_menu/files/qa/controller.lua"]==1,"developer mode did not load diagnostics/QA")
-assert(dev_calls.diagnostics==1 and dev_calls.qa==1,"developer mode did not execute diagnostics/QA update")
-io.write("dev_mode_runtime_gate=PASS default=0 release_unloaded=true developer_loaded=true\n")
+local text=assert(io.open(root.."/init.lua","rb")):read("*a")
+assert(not string.find(text,"dev_mode.lua",1,true),"Workshop init still loads dev_mode.lua")
+assert(not string.find(text,"files/diagnostics/service.lua",1,true),"Workshop init still loads diagnostics service")
+assert(not string.find(text,"files/qa/controller.lua",1,true),"Workshop init still loads QA controller")
+assert(not string.find(text,"METAMORPH_CREATIVE_MENU_DEV_MODE",1,true),"Workshop init still exposes dev-mode global")
+io.write("workshop_runtime_composition=PASS dev_mode_unloaded=true qa_unloaded=true diagnostics_unloaded=true\n")
diff --git a/metamorph_creative_menu/tests/diagnostics_scanner_load_mock.lua b/metamorph_creative_menu/tests/diagnostics_scanner_load_mock.lua
index 2ae26ad..b41ca66 100644
--- a/metamorph_creative_menu/tests/diagnostics_scanner_load_mock.lua
+++ b/metamorph_creative_menu/tests/diagnostics_scanner_load_mock.lua
@@ -61,7 +61,7 @@ local module_stubs = {
can_edit=function() return true, "ok" end,
is_locked=function() return false end,
fields=function() return {} end,
- debug_state=function() return {} end,
+ state_snapshot=function() return {} end,
},
["mods/metamorph_creative_menu/files/ui/menu_controller.lua"] = {active_tab=function() return "SPELLS" end, is_hovered=function() return false end},
["mods/metamorph_creative_menu/files/features/possession/keybinds.lua"] = {possess_key_name=function() return "Key_g" end},
diff --git a/metamorph_creative_menu/tests/ew_dev_mode_bridge_gate_mock.lua b/metamorph_creative_menu/tests/ew_dev_mode_bridge_gate_mock.lua
index 87d78a3..fbac395 100644
--- a/metamorph_creative_menu/tests/ew_dev_mode_bridge_gate_mock.lua
+++ b/metamorph_creative_menu/tests/ew_dev_mode_bridge_gate_mock.lua
@@ -1,50 +1,7 @@
local root=assert(arg[1],"root required")
-local native_dofile=dofile
-
-local function run(mode)
- local loaded={}
- local registrations={}
- local rpc={opts_reliable=function() end,opts_everywhere=function() end}
- local modules={}
- local function bridge(name)
- return {register=function() registrations[#registrations+1]=name end,update=function() end}
- end
- modules["mods/metamorph_creative_menu/files/integrations/ew/bridge/protocol.lua"]={NAMESPACE="test"}
- modules["mods/metamorph_creative_menu/files/integrations/ew/bridge/common.lua"]={clean=tostring,report_error=function() end}
- modules["mods/metamorph_creative_menu/files/integrations/ew/perk_runtime_guard.lua"]={install=function() return true,"ok" end}
- modules["mods/metamorph_creative_menu/files/integrations/ew/bridge/world_rules.lua"]=bridge("world_rules")
- modules["mods/metamorph_creative_menu/files/integrations/ew/bridge/qa.lua"]=bridge("qa_full")
- modules["mods/metamorph_creative_menu/files/integrations/ew/bridge/qa_reserved.lua"]=bridge("qa_reserved")
- modules["mods/metamorph_creative_menu/files/integrations/ew/bridge/companion.lua"]=bridge("companion")
- modules["mods/metamorph_creative_menu/files/integrations/ew/bridge/perks.lua"]=bridge("perks")
- modules["mods/metamorph_creative_menu/files/integrations/ew/bridge/weather.lua"]=bridge("weather")
- modules["mods/metamorph_creative_menu/files/integrations/ew/bridge/possession.lua"]=bridge("possession")
- modules["mods/metamorph_creative_menu/files/integrations/ew/bridge/items.lua"]={init=function() end,update=function() end}
- modules["mods/metamorph_creative_menu/files/integrations/ew/bridge/forms.lua"]={register_pose=function() registrations[#registrations+1]="forms_pose" end,register_reserved=function() registrations[#registrations+1]="forms_reserved" end,update=function() end,metrics=function() return 0,0 end}
- local old_dofile,old_once=dofile,dofile_once
- dofile_once=function(path)
- if path=="mods/quant.ew/files/api/ew_api.lua" then return {new_rpc_namespace=function() return rpc end} end
- error("unexpected dofile_once "..tostring(path))
- end
- dofile=function(path)
- loaded[path]=(loaded[path] or 0)+1
- if path=="mods/metamorph_creative_menu/dev_mode.lua" then return mode end
- if modules[path] then return modules[path] end
- error("unexpected EW bootstrap dependency: "..tostring(path))
- end
- GlobalsSetValue=function() end; GameGetFrameNum=function() return 10 end
- ctx={my_id="me",host_id="host"}
- local module=assert(loadfile(root.."/files/integrations/ew/bootstrap.lua"))()
- dofile,dofile_once=old_dofile,old_once
- return loaded,registrations,module
-end
-
-local release_loaded,release_reg=run(0)
-assert(release_loaded["mods/metamorph_creative_menu/files/integrations/ew/bridge/qa.lua"]==nil,"release EW bootstrap loaded full QA telemetry bridge")
-assert(release_loaded["mods/metamorph_creative_menu/files/integrations/ew/bridge/qa_reserved.lua"]==1,"release EW bootstrap did not preserve reserved QA slot")
-assert(release_reg[2]=="qa_reserved","release QA slot moved from RPC position 3 registration group")
-local dev_loaded,dev_reg=run(1)
-assert(dev_loaded["mods/metamorph_creative_menu/files/integrations/ew/bridge/qa.lua"]==1,"developer EW bootstrap did not load full QA bridge")
-assert(dev_loaded["mods/metamorph_creative_menu/files/integrations/ew/bridge/qa_reserved.lua"]==nil,"developer EW bootstrap loaded release placeholder")
-assert(dev_reg[2]=="qa_full","developer QA registration order changed")
-io.write("ew_dev_mode_bridge_gate=PASS release_qa_unloaded=true reserved_slot=true developer_qa=true\n")
+local text=assert(io.open(root.."/files/integrations/ew/bootstrap.lua","rb")):read("*a")
+assert(not string.find(text,"dev_mode.lua",1,true),"EW bootstrap still loads dev_mode.lua")
+assert(not string.find(text,"bridge/qa.lua",1,true),"EW bootstrap still loads full QA bridge")
+assert(not string.find(text,"bridge/qa_reserved.lua",1,true),"EW bootstrap still loads QA placeholder module")
+assert(string.find(text,"function rpc.reserved_protocol_slot_3",1,true),"EW bootstrap no longer preserves reserved RPC slot 3")
+io.write("workshop_ew_composition=PASS qa_unloaded=true reserved_slot_3=true\n")
diff --git a/metamorph_creative_menu/tests/ew_protocol_mock.lua b/metamorph_creative_menu/tests/ew_protocol_mock.lua
index 2ea3b24..5707f56 100644
--- a/metamorph_creative_menu/tests/ew_protocol_mock.lua
+++ b/metamorph_creative_menu/tests/ew_protocol_mock.lua
@@ -1,6 +1,6 @@
local root=assert(arg[1])
local expected={
- "apply_world_rules","request_world_rules","sync_qa_state","request_player_companion",
+ "apply_world_rules","request_world_rules","reserved_protocol_slot_3","request_player_companion",
"sync_form_pose","remove_global_perk","apply_weather_state","request_weather_state",
"retire_possession_target","announce_light_form_protocol"
}
diff --git a/metamorph_creative_menu/tests/gravity_mock.lua b/metamorph_creative_menu/tests/gravity_mock.lua
index 4aa3741..6e0933d 100644
--- a/metamorph_creative_menu/tests/gravity_mock.lua
+++ b/metamorph_creative_menu/tests/gravity_mock.lua
@@ -57,7 +57,7 @@ assert(math.abs(comps[101].gravity - (-400))<1e-6,"player gravity="..tostring(co
comps[102].pixel_gravity=-700
api.post_update()
assert(math.abs(comps[102].pixel_gravity - (-1400))<1e-6,"post-update reassert="..tostring(comps[102].pixel_gravity))
-local dbg=api.local_gravity_debug(); assert(dbg.factor==-4)
+local dbg=api.local_gravity_state(); assert(dbg.factor==-4)
local found=false; for _,r in ipairs(dbg.rows) do if r.field=="pixel_gravity" then found=true; assert(r.native==350); assert(r.expected==-1400); assert(r.current==-1400) end end; assert(found)
-- RESET returns ownership snapshot (perk-modified 0), not the clean scale used for creative multiplication.
frame=3
diff --git a/metamorph_creative_menu/tests/license_contract.py b/metamorph_creative_menu/tests/license_contract.py
new file mode 100644
index 0000000..94c1284
--- /dev/null
+++ b/metamorph_creative_menu/tests/license_contract.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python3
+"""Verify that redistributable MCM packages carry the project license and attribution."""
+
+from __future__ import annotations
+
+import sys
+from pathlib import Path
+
+
+def main() -> int:
+ root = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else Path(__file__).resolve().parent.parent
+ license_path = root / "LICENSE.txt"
+ notice_path = root / "NOTICE.txt"
+
+ missing = [path.name for path in (license_path, notice_path) if not path.is_file()]
+ if missing:
+ raise SystemExit("license_contract=FAIL missing=" + ",".join(missing))
+
+ license_text = license_path.read_text(encoding="utf-8")
+ notice_text = notice_path.read_text(encoding="utf-8")
+
+ required_license_terms = (
+ "Metamorph Creative Menu Attribution License 1.0",
+ "commercial or non-commercial purpose",
+ "Original developer: zerodancing",
+ "https://github.com/zerodancing/Metamorph-Creative-Menu",
+ )
+ for text in required_license_terms:
+ if text not in license_text:
+ raise SystemExit(f"license_contract=FAIL LICENSE.txt missing {text!r}")
+
+ required_notice_terms = (
+ "Original developer: zerodancing",
+ "https://github.com/zerodancing",
+ "https://github.com/zerodancing/Metamorph-Creative-Menu",
+ )
+ for text in required_notice_terms:
+ if text not in notice_text:
+ raise SystemExit(f"license_contract=FAIL NOTICE.txt missing {text!r}")
+
+ print("license_contract=PASS attribution=zerodancing commercial_use=true")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/metamorph_creative_menu/tests/menu_error_isolation_mock.lua b/metamorph_creative_menu/tests/menu_error_isolation_mock.lua
index b5ba22c..1baed79 100644
--- a/metamorph_creative_menu/tests/menu_error_isolation_mock.lua
+++ b/metamorph_creative_menu/tests/menu_error_isolation_mock.lua
@@ -2,7 +2,7 @@ local root = assert(arg[1], "root required")
local native_dofile = dofile
local tile_calls = 0
local error_labels = 0
-local diagnostic_calls = 0
+local error_prints = 0
local tab_draw_calls = 0
local ui = {
@@ -56,8 +56,7 @@ function GuiLayoutBeginHorizontal() end
function GuiLayoutEnd() end
function GuiBeginAutoBox() end
function InputIsMouseButtonJustDown() return false end
-function print() end
-METAMORPH_CREATIVE_MENU_DIAGNOSTICS_CAPTURE=function() diagnostic_calls=diagnostic_calls+1 end
+function print(message) if string.find(tostring(message), "UI tab", 1, true) then error_prints=error_prints+1 end end
METAMORPH_CREATIVE_MENU_MENU_CONTROLLER=nil
local controller = assert(native_dofile(root.."/files/ui/menu_controller.lua"))
@@ -67,6 +66,6 @@ assert(ok1 and ok2, "tab runtime error escaped menu controller: "..tostring(err1
assert(tab_draw_calls == 2, "active tab was not attempted on both frames")
assert(tile_calls == 14, "top tab bar stopped rendering after a tab error")
assert(error_labels == 2, "menu did not show bounded tab error fallback")
-assert(diagnostic_calls == 1, "same persistent tab error should be reported once, not every frame")
+assert(error_prints == 1, "same persistent tab error should be printed once, not every frame")
print("menu_error_isolation=PASS header_survives=true error_contained=true")
diff --git a/metamorph_creative_menu/tests/perk_mutation_journal_mock.lua b/metamorph_creative_menu/tests/perk_mutation_journal_mock.lua
index 8d23d7d..a41e08d 100644
--- a/metamorph_creative_menu/tests/perk_mutation_journal_mock.lua
+++ b/metamorph_creative_menu/tests/perk_mutation_journal_mock.lua
@@ -104,7 +104,7 @@ assert(ComponentHasTag(component,"baseline_component") and not ComponentHasTag(c
journal.revert_delta(d2)
assert(meta[component].run_velocity==100,"stacked speed baseline not restored")
assert(math.abs(objects[component].damage_multipliers.explosion-1.0)<1e-9,"stacked object baseline not restored")
-assert(journal.debug_active_properties()==0,"property ownership leaked")
+assert(journal.active_property_count()==0,"property ownership leaked")
-- Scalar rollback must keep ownership when the setter silently fails readback, then
-- succeed on retry instead of forgetting a mutation that still exists in the player.
@@ -118,9 +118,9 @@ block_gravity_restore=true
local failed,failed_reason=journal.revert_delta(d3)
assert(failed==false and string.find(tostring(failed_reason),"property_restore",1,true),"silent scalar restore failure reported success")
assert(values[component].pixel_gravity==50,"fixture unexpectedly restored blocked scalar")
-assert(journal.debug_active_properties()==1,"failed scalar restore discarded ownership")
+assert(journal.active_property_count()==1,"failed scalar restore discarded ownership")
block_gravity_restore=false
local retried,retry_reason=journal.revert_delta(d3)
assert(retried==true,retry_reason)
-assert(values[component].pixel_gravity==350 and journal.debug_active_properties()==0,"scalar retry did not restore baseline and release ownership")
+assert(values[component].pixel_gravity==350 and journal.active_property_count()==0,"scalar retry did not restore baseline and release ownership")
print("perk_mutation_journal=PASS meta=true object=true overlap=true created_root=true component=true reparent=true tags=true scalar_retry=true")
diff --git a/metamorph_creative_menu/tests/perk_owned_residue_failure_mock.lua b/metamorph_creative_menu/tests/perk_owned_residue_failure_mock.lua
index a4f964b..e35fd2b 100644
--- a/metamorph_creative_menu/tests/perk_owned_residue_failure_mock.lua
+++ b/metamorph_creative_menu/tests/perk_owned_residue_failure_mock.lua
@@ -32,7 +32,7 @@ local delta={transaction_id=token.transaction_id}; journal.attach_delta(delta,to
local ok,reason=journal.revert_delta(delta)
assert(ok==true,'deferred EntityKill blocked rollback: '..tostring(reason))
assert(values[component].pixel_gravity==350,'scalar rollback did not commit on first removal')
-local state=pending.debug_state(); assert(state.pending==1 and state.failed==0,'deferred object not queued')
+local state=pending.state_snapshot(); assert(state.pending==1 and state.failed==0,'deferred object not queued')
frame=frame+1; for id in pairs(pending_kill) do alive[id]=false end; pending.update()
-state=pending.debug_state(); assert(state.pending==0 and state.failed==0,'completed deletion remained residue')
+state=pending.state_snapshot(); assert(state.pending==0 and state.failed==0,'completed deletion remained residue')
print('perk_owned_residue_failure=PASS deferred_delete_requires_one_user_action=true')
diff --git a/metamorph_creative_menu/tests/perk_pending_cleanup_timeout_mock.lua b/metamorph_creative_menu/tests/perk_pending_cleanup_timeout_mock.lua
index c582996..5032862 100644
--- a/metamorph_creative_menu/tests/perk_pending_cleanup_timeout_mock.lua
+++ b/metamorph_creative_menu/tests/perk_pending_cleanup_timeout_mock.lua
@@ -7,6 +7,6 @@ function EntityKill(_) end
local pending=assert(dofile(root..'/files/features/perks/transactions/pending_cleanup.lua'))
assert(select(1,pending.retire_entity(101,'stuck-test'))==true)
for _=1,31 do frame=frame+1; pending.update() end
-local state=pending.debug_state(); assert(state.pending==0 and state.failed==1,'true residue not detected')
+local state=pending.state_snapshot(); assert(state.pending==0 and state.failed==1,'true residue not detected')
assert(string.find(table.concat(state.failed_items,','),'entity:101',1,true),'exact id missing')
print('perk_pending_cleanup_timeout=PASS persistent_residue_detected_async=true')
diff --git a/metamorph_creative_menu/tests/perk_spawn_mock.lua b/metamorph_creative_menu/tests/perk_spawn_mock.lua
index 2ada791..5f3a864 100644
--- a/metamorph_creative_menu/tests/perk_spawn_mock.lua
+++ b/metamorph_creative_menu/tests/perk_spawn_mock.lua
@@ -6,8 +6,8 @@ local stubs = {
["mods/metamorph_creative_menu/files/features/perks/inverse_registry.lua"]={has=function() return false end},
["mods/metamorph_creative_menu/files/features/perks/transactions.lua"]={has=function() return false end},
["mods/metamorph_creative_menu/files/features/perks/root_companions.lua"]={supports=function() return false end, update=function() end, debug=function() return {} end},
- ["mods/metamorph_creative_menu/files/features/perks/nested_pickups.lua"]={update=function() end,debug_state=function() return {scopes=0,children=0} end},
- ["mods/metamorph_creative_menu/files/features/perks/locomotion_guard.lua"]={capture_if_idle=function() end,repair_if_idle=function() end,debug_baseline_count=function() return 0 end},
+ ["mods/metamorph_creative_menu/files/features/perks/nested_pickups.lua"]={update=function() end,state_snapshot=function() return {scopes=0,children=0} end},
+ ["mods/metamorph_creative_menu/files/features/perks/locomotion_guard.lua"]={capture_if_idle=function() end,repair_if_idle=function() end,baseline_count=function() return 0 end},
["mods/metamorph_creative_menu/files/features/perks/presentation.lua"]={update=function() end},
}
dofile=function(path)
diff --git a/metamorph_creative_menu/tests/qa_baselines_scope_mock.lua b/metamorph_creative_menu/tests/qa_baselines_scope_mock.lua
index 6f9ccd1..b3179cd 100644
--- a/metamorph_creative_menu/tests/qa_baselines_scope_mock.lua
+++ b/metamorph_creative_menu/tests/qa_baselines_scope_mock.lua
@@ -20,7 +20,7 @@ local root_companions_stub = {
}
local service_calls = 0
local perk_service_stub = {
- debug_ownership_state = function()
+ ownership_state = function()
service_calls = service_calls + 1
return { transactions = 0, mutations = 0, global_owners = 0, run_flag_owners = 0, cleanup = {pending=0, failed=0} }
end,
diff --git a/metamorph_creative_menu/tests/weather_state_sync_mock.lua b/metamorph_creative_menu/tests/weather_state_sync_mock.lua
index 3682f9c..975651d 100644
--- a/metamorph_creative_menu/tests/weather_state_sync_mock.lua
+++ b/metamorph_creative_menu/tests/weather_state_sync_mock.lua
@@ -72,7 +72,7 @@ globals.mcm_weather_remote_snapshot_v1 = storm_snapshot
frame = frame + 1
weather.update()
assert(weather.is_locked() == true, "remote weather snapshot was not adopted")
-local state = weather.debug_state()
+local state = weather.state_snapshot()
assert(state.fog == 0.5 and state.wind_speed == 48, "remote storm snapshot did not converge world fields")
-- A peer taking over a remote active lock must retain its local pre-override time_dt.
diff --git a/metamorph_creative_menu/tests/world_rules_click_deferred_scan_mock.lua b/metamorph_creative_menu/tests/world_rules_click_deferred_scan_mock.lua
index 32f60d1..2ccbe81 100644
--- a/metamorph_creative_menu/tests/world_rules_click_deferred_scan_mock.lua
+++ b/metamorph_creative_menu/tests/world_rules_click_deferred_scan_mock.lua
@@ -15,7 +15,7 @@ local physics={
restore_rule=function() return true,"ok" end,
reset_all=function() return true end,
has_overrides=function() return false end,
- debug_local_gravity=function() return {} end,
+ local_gravity_state=function() return {} end,
has_persisted_local_recovery=function() return false end,
recover_persisted_local=function() return true end,
}
diff --git a/metamorph_creative_menu/tests/world_rules_lifecycle_mock.lua b/metamorph_creative_menu/tests/world_rules_lifecycle_mock.lua
index 98a93ae..3411700 100644
--- a/metamorph_creative_menu/tests/world_rules_lifecycle_mock.lua
+++ b/metamorph_creative_menu/tests/world_rules_lifecycle_mock.lua
@@ -25,7 +25,7 @@ local physics = {
restore_rule=function() physics_value=nil; return true,"ok" end,
reset_all=function() reset_calls.physics=reset_calls.physics+1; physics_value=nil; return true end,
has_overrides=function() return physics_value ~= nil end,
- debug_local_gravity=function() return {} end,
+ local_gravity_state=function() return {} end,
}
local stain = {
supported=function() return true end, apply=function() end, cleanup_stale=function() end,
diff --git a/metamorph_creative_menu/tests/world_rules_restart_recovery_mock.lua b/metamorph_creative_menu/tests/world_rules_restart_recovery_mock.lua
index cd939be..4e6cc65 100644
--- a/metamorph_creative_menu/tests/world_rules_restart_recovery_mock.lua
+++ b/metamorph_creative_menu/tests/world_rules_restart_recovery_mock.lua
@@ -122,6 +122,6 @@ assert(service.choice_index(service.rules()[1])==1 and service.choice_index(serv
assert(service.has_overrides()==false,"restart recovery left in-memory rule ownership")
assert(dirty_calls==0,"startup recovery was incorrectly published as a user Rules edit")
assert(network_updates==1,"normal network sync did not continue after recovery")
-assert(diagnostics==1,"restart recovery was not visible to diagnostics")
+assert(diagnostics==0,"restart recovery unexpectedly called diagnostics from player runtime")
io.write("world_rules_restart_recovery=PASS relations=0 pixel_gravity=350 recovery_clean=true\n")
diff --git a/metamorph_creative_menu/tests/world_rules_ui_action_isolation_mock.lua b/metamorph_creative_menu/tests/world_rules_ui_action_isolation_mock.lua
index 4bd62e1..c784b70 100644
--- a/metamorph_creative_menu/tests/world_rules_ui_action_isolation_mock.lua
+++ b/metamorph_creative_menu/tests/world_rules_ui_action_isolation_mock.lua
@@ -4,7 +4,6 @@ local tile_calls=0
local white_labels={}
local step_calls=0
local click_once=true
-local diagnostics=0
local service={
can_edit=function() return true,"singleplayer" end,
has_overrides=function() return false end,
@@ -51,9 +50,6 @@ function GuiLayoutEnd() end
function GuiBeginScrollContainer() end
function GuiEndScrollContainer() end
function GuiGetPreviousWidgetInfo() return false,false,false end
-METAMORPH_CREATIVE_MENU_DIAGNOSTICS_CAPTURE=function(kind,details)
- if kind=="ui.rules.action" and string.find(details,"intentional Rules mutation failure",1,true) then diagnostics=diagnostics+1 end
-end
local tab=assert(native_dofile(root.."/files/ui/tabs/world_rules.lua"))
local ok1,err1=pcall(tab.draw,nil,220,180)
@@ -61,7 +57,6 @@ local ok2,err2=pcall(tab.draw,nil,220,180)
assert(ok1 and ok2,"Rules action exception escaped tab draw: "..tostring(err1 or err2))
assert(step_calls==1,"probe did not execute exactly one failing Rules mutation")
assert(tile_calls==2,"Rules grid stopped drawing after a mutation error")
-assert(diagnostics==1,"Rules mutation exception was not reported to diagnostics")
local saw_error=false
for _,text in ipairs(white_labels) do if string.find(text,"RULE ERROR:",1,true) then saw_error=true end end
assert(saw_error,"Rules tab did not surface bounded mutation failure")
diff --git a/metamorph_creative_menu/workshop.xml b/metamorph_creative_menu/workshop.xml
new file mode 100644
index 0000000..5c11d74
--- /dev/null
+++ b/metamorph_creative_menu/workshop.xml
@@ -0,0 +1,8 @@
+
+
diff --git a/metamorph_creative_menu/workshop_preview_image.png b/metamorph_creative_menu/workshop_preview_image.png
new file mode 100644
index 0000000..28f5e91
Binary files /dev/null and b/metamorph_creative_menu/workshop_preview_image.png differ