diff --git a/deploy/classic/README.md b/deploy/classic/README.md new file mode 100644 index 0000000..b97bafe --- /dev/null +++ b/deploy/classic/README.md @@ -0,0 +1,32 @@ +# Classic migration staging + +Target: CMaNGOS Classic, client 1.12.1 build 5875, with CMaNGOS Playerbots. +This directory does not implement the runtime connector or authorize a cutover. + +`stage-data.py --root /path/to/isolated/migration` observes an existing +`azerothai-classic-build` container, verifies the transferred MPQs against +`client-manifest.json`, and runs the upstream extractor with two worker threads. +The manifest maps relative MPQ paths to `size` and `sha256`. Checksums verify +transfer integrity against the supplied client manifest, not publisher authenticity. +The Docker builder image must be named `azerothai-classic-builder:local`; the +installed extractor must be under `run/bin/tools`. The staging directory and +container files belong to UID/GID 1000. Containers have a two-core/four-GiB limit. + +Progress and failures are written to `prerequisites-status.json`. A successful +extraction is only a prerequisite: database import, the Classic control adapter, +the 50-character roster, account protection, and gameplay validation remain. +The script never starts the new realm, changes the old realm, or deletes bots. + +Migration safeguards: + +- Preserve the verified Wrath backup and the active-50 identity export. +- Maintain 25 Alliance and 25 Horde. Use valid Vanilla race/class combinations. +- Start new Vanilla characters at level 1; do not directly import Wrath inventory, + talent, spell, or quest tables. Blood Elves, Draenei, Death Knights, Alliance + Shamans, and Horde Paladins need explicit compatible replacements. +- Retain personalities and historical memories; reset live observations, GUIDs, + action queues, and progress baselines when binding the new characters. +- Protect human/GM accounts. Inactive managed profiles are cleanup candidates, + not proof that corresponding game characters can safely be deleted. +- Test native bot login, progress, resurrection, safety rules, read-only visitor + access, and current observations before switching the live dashboard or client. diff --git a/deploy/classic/stage-data.py b/deploy/classic/stage-data.py new file mode 100644 index 0000000..d31f552 --- /dev/null +++ b/deploy/classic/stage-data.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Finish isolated Classic prerequisites. Never starts a realm or edits live data.""" +import argparse +import hashlib +import json +import pathlib +import shutil +import subprocess +import time + +parser = argparse.ArgumentParser() +parser.add_argument('--root', required=True) +args = parser.parse_args() +root = pathlib.Path(args.root).resolve() + +def status(stage, **extra): + value = {'at': time.time(), 'stage': stage, 'cutover': False, **extra} + temporary = root / 'prerequisites-status.tmp' + temporary.write_text(json.dumps(value, indent=2)) + temporary.replace(root / 'prerequisites-status.json') + print(stage, flush=True) + +def run(*command): + return subprocess.check_output(command, text=True).strip() + +try: + status('waiting for Classic compilation and client transfer') + deadline = time.monotonic() + 6 * 3600 + while True: + state = json.loads(run('docker', 'inspect', 'azerothai-classic-build'))[0]['State'] + if not state['Running']: + if state['ExitCode'] != 0: + raise RuntimeError('Classic build failed; inspect docker logs azerothai-classic-build') + break + if time.monotonic() > deadline: + raise RuntimeError('Classic build exceeded six-hour observation window') + time.sleep(30) + manifest = json.loads((root / 'client-manifest.json').read_text()) + while not all((root / 'client' / f).exists() and (root / 'client' / f).stat().st_size == v['size'] for f, v in manifest.items()): + if time.monotonic() > deadline: + raise RuntimeError('Client transfer incomplete after six hours') + time.sleep(30) + status('verifying transferred client archives') + for filename, expected in manifest.items(): + with (root / 'client' / filename).open('rb') as stream: + actual = hashlib.file_digest(stream, 'sha256').hexdigest() + if actual != expected['sha256']: + raise RuntimeError('Client archive checksum mismatch: ' + filename) + if shutil.disk_usage(root).free < 8 * 1024**3: + raise RuntimeError('Less than 8 GiB free for extracted Vanilla data') + output = root / 'extracted'; output.mkdir(exist_ok=True) + status('extracting Vanilla maps and navigation data; realm not switched') + with (root / 'data-extraction.log').open('w') as log: + subprocess.run([ + 'docker', 'run', '--name', 'azerothai-classic-extract', '--cpus', '2', '--memory', '4g', + '--user', '1000:1000', '-i', '-v', str(root) + ':/work', '-w', '/work/run/bin/tools', + 'azerothai-classic-builder:local', 'bash', './ExtractResources.sh', '/work/client', '/work/extracted' + ], input='y\n2\n\nn\nn\n\n', text=True, stdout=log, stderr=subprocess.STDOUT, check=True) + counts = {name: sum(1 for f in (output / name).rglob('*') if f.is_file()) for name in ('dbc', 'maps', 'vmaps', 'mmaps')} + if not all(counts.values()): + raise RuntimeError('An extracted data directory is empty: ' + str(counts)) + status('prerequisites extracted; database, connector, roster and gameplay validation remain', files=counts) +except Exception as error: + status('blocked', error=str(error)) + raise