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-runtime-audit.yml b/.github/workflows/workshop-runtime-audit.yml
new file mode 100644
index 0000000..e6ad0dc
--- /dev/null
+++ b/.github/workflows/workshop-runtime-audit.yml
@@ -0,0 +1,96 @@
+name: Workshop runtime residue audit
+
+on:
+ push:
+ branches: [workshop]
+ paths:
+ - '.github/workflows/workshop-runtime-audit.yml'
+
+permissions:
+ contents: read
+
+jobs:
+ audit:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7
+
+ - name: Scan exact Steam upload set
+ id: scan
+ shell: python
+ run: |
+ from pathlib import Path
+ import os
+ import re
+ import xml.etree.ElementTree as ET
+
+ root = Path('metamorph_creative_menu')
+ 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', '')))
+ text_suffixes = {'.lua', '.xml', '.txt', '.csv', '.json', '.frag', '.vert'}
+ 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 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 identifier', re.compile(r'\b(?:debug_[A-Za-z0-9_]+|[A-Za-z0-9_]+_debug|[A-Za-z0-9_]+\.debug(?:_[A-Za-z0-9_]+)?)\b')),
+ ('diagnostic identifier', re.compile(r'\b(?:diagnostic|diagnostics|DIAGNOSTICS|DIAGNOSTIC)\b')),
+ ('dev mode', re.compile(r'\b(?:dev_mode|DEV_MODE|METAMORPH_CREATIVE_MENU_DEV_MODE)\b')),
+ ('profiling or metrics', re.compile(r'\b(?:profiling|profile_metrics|metrics_enabled|set_(?:profiling|metrics)_enabled)\b', re.I)),
+ ('runtime print', re.compile(r'\bprint\s*\(')),
+ ('unfinished marker', re.compile(r'\b(?:TODO|FIXME|HACK)\b', re.I)),
+ ]
+
+ findings = []
+ scanned = 0
+ for path in sorted(root.rglob('*')):
+ if not path.is_file() or path.suffix.lower() not in text_suffixes:
+ continue
+ rel = path.relative_to(root).as_posix()
+ if rel in excluded_files or any(rel == d or rel.startswith(d + '/') for d in excluded_dirs):
+ continue
+ scanned += 1
+ try:
+ text = path.read_text(encoding='utf-8')
+ except UnicodeDecodeError:
+ continue
+ for lineno, line in enumerate(text.splitlines(), 1):
+ for label, pattern in rules:
+ if pattern.search(line):
+ findings.append(f'{rel}:{lineno}: {label}: {line.strip()[:240]}')
+
+ status = 'FAIL' if findings else 'PASS'
+ report = '\n'.join([f'WORKSHOP_RUNTIME_AUDIT={status} files={scanned} findings={len(findings)}', *findings]) + '\n'
+ Path('workshop-runtime-audit.txt').write_text(report, encoding='utf-8')
+ print(report, end='')
+ with open(os.environ['GITHUB_OUTPUT'], 'a', encoding='utf-8') as output:
+ output.write(f'findings={len(findings)}\n')
+
+ - name: Upload audit report
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: workshop-runtime-audit
+ path: workshop-runtime-audit.txt
+ if-no-files-found: error
+ retention-days: 1
+
+ - name: Upload source snapshot
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: workshop-source-snapshot
+ path: metamorph_creative_menu
+ if-no-files-found: error
+ retention-days: 1
+
+ - name: Fail on runtime residue
+ if: steps.scan.outputs.findings != '0'
+ shell: bash
+ run: exit 1
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/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/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/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/workshop.xml b/metamorph_creative_menu/workshop.xml
new file mode 100644
index 0000000..cf832b6
--- /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