From 8991dbd48c609fd34eea080e9d5ca0915c191c53 Mon Sep 17 00:00:00 2001 From: hyp36rmax <82559640+hyp36rmax@users.noreply.github.com> Date: Mon, 19 Oct 2026 14:06:46 -0700 Subject: [PATCH 1/2] Record native Local VS boss execution boundaries --- .github/workflows/build.yml | 1 + docs/project/LOCAL-VS-BOSSES.md | 87 ++++++++++++++++++++++++++ tools/bosses/local_vs_audit.py | 72 +++++++++++++++++++++ tools/bosses/test_local_vs_boundary.py | 81 ++++++++++++++++++++++++ 4 files changed, 241 insertions(+) create mode 100644 docs/project/LOCAL-VS-BOSSES.md create mode 100644 tools/bosses/local_vs_audit.py create mode 100644 tools/bosses/test_local_vs_boundary.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e57b21c..18906e3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -50,6 +50,7 @@ jobs: run: | python tools/brandingtest.py python tools/experience/test_bosses.py + python tools/bosses/test_local_vs_boundary.py python tools/experience/test_repository.py python tools/experience/test_diagnostics.py python tools/experience/test_normal_lifecycle.py diff --git a/docs/project/LOCAL-VS-BOSSES.md b/docs/project/LOCAL-VS-BOSSES.md new file mode 100644 index 0000000..0158dd1 --- /dev/null +++ b/docs/project/LOCAL-VS-BOSSES.md @@ -0,0 +1,87 @@ +# Local VS boss execution boundaries + +**Implementation status: incomplete.** Local VS bosses are not enabled, and no +playable B03 candidate is available. This record and its tools establish native +boundaries that must be resolved before exposing the optional feature. + +Frozen B02: `b6518d3f3c0777335fbb2100c1b99e8304797d2b`. +Development branch: `feature/playable-bosses-local-vs`, directly from B02. +Upstream single-player implementation remains Pairomaniac and Vortyne's work; +hyp36rmax maintains the VO PATCH EX integration and expansions. + +## Native evidence + +`tools/bosses/test_local_vs_boundary.py` executes the actual linked B02 routines +in Unicorn, preserving register and stack checks. These are tests of the existing +boundary, not evidence that a Local VS extension works. + +| Boundary | Result | +|---|---| +| `selmax_a` | VERIFIED: both cursor indices allow boss positions only in mode 0. Local mode 1 and network mode 2 retain the bound of seven. | +| `zfire_a` | VERIFIED: the A player object uses either decoded turbo edge (`0xff`); a held value does not retrigger. | +| B object input through `zfire_a` | VERIFIED: turbo edges on the known B object candidates do not take the A player special-attack path. This fixture does not establish which candidate owns P2 in every runtime state. | +| Four editions | VERIFIED: linked-code fixtures run for Retail, JPRE, OEM and JP. The B object candidate checks use Retail/JPRE addresses only. No pristine OEM/JP execution or physical gameplay is claimed. | + +The game has two engine copies, each with a subject/opponent pair. An engine +copy is not a permanent human-player identity. The A opponent object is not the +B controlled object. Player role and camera state can select different members +of each pair. A safe extension must identify both engine and object ownership. + +## Candidate mapping + +`tools/bosses/local_vs_audit.py` scans recognized pristine executables without +writing them. It compares 16-instruction windows, masking relocations and branch +destinations. Every source window must match its original location. A candidate +is **NEEDS VALIDATION**, even if unique: caller reachability, operand ownership, +stack continuation and resource lifetime are not proved by opcode similarity. + +Retail and JPRE each expose 93 inspected code windows; 54 have one alternate +match. Selected findings: + +| Native boundary | Retail candidates | JPRE candidates | Status | +|---|---|---|---| +| AI tick continuation | `4c736f`, `4c7435`, `5b8c0b` | `4c4f0f`, `4c4fd5`, `5b35e3` | UNKNOWN owner; three matches each | +| Model-clone continuation | `468e79`, `4eb949`, `5773b3` | `467ebd`, `4e89cd`, `57269f` | UNKNOWN owner; three matches each | +| Z-Gradt draw entry | None in this comparison | None in this comparison | UNKNOWN; absence of a pattern match does not mean the game lacks the routine | +| Clip bank 0 continuation | `4c5798` | `4c33c0` | NEEDS VALIDATION | +| Clip bank 3 continuation | `4c57b2` | `4c33da` | NEEDS VALIDATION | + +Addresses are native virtual addresses, not file offsets. There is no assumed +constant A-to-B or cross-edition delta. + +## Resource contracts still requiring implementation + +The existing boss flag, palette owner, model clone, AI bank, temporary state, +laser lock-on, gauge state and replay sidecar are single-player state. Z-Gradt's +banking/model-copy paths are guarded by the final-stage/player conditions; they +are not a demonstrated arbitrary-arena, two-human implementation. + +The following contracts remain UNKNOWN or NEEDS VALIDATION: + +1. B initialization, draw, weapon/update and cleanup entry/exit contracts. +2. Which engine/object pair owns each human input in selection, fight and replay. +3. Per-object AI/model/palette/event storage and the lifetime of shared assets. +4. Simultaneous laser state and complete replay save/restore boundaries. +5. P2 boss camera/HUD/animation and round-result paths. + +Selection-only unlocking is UNSUITABLE: it bypasses the existing guard without +repairing these dependencies. No guessed hook, second copy of the complete boss +implementation, placeholder option or unverified generated recipe was added. + +## Remaining B03 scope + +After those adapters are verified, add one independently optional local-mode +extension requiring Playable Bosses. Mode 0 must retain B02 unlocks and behavior; +mode 2 must retain the original online behavior. Mode 1 must allow the original +eight, Jaguarandi and Z-Gradt for either player without unlock requirements. + +All eight required orientations remain NEEDS VALIDATION: Jaguarandi/standard, +standard/Jaguarandi, Z-Gradt/standard, standard/Z-Gradt, Jaguarandi/Jaguarandi, +Jaguarandi/Z-Gradt, Z-Gradt/Jaguarandi and Z-Gradt/Z-Gradt. No balancing, damage +scaling or artificial weapon restrictions are permitted. + +The existing 512 boss-free and 2,048 B02 catalog entries remain untouched. +Controller assignments, F7, movie ownership, display modes, networking, +Apply/Restore and saved progression remain unchanged. A B03 UAT ZIP must only +be assembled once the native extension and its tests exist; renaming B02 would +not provide a Local VS test candidate. diff --git a/tools/bosses/local_vs_audit.py b/tools/bosses/local_vs_audit.py new file mode 100644 index 0000000..aa2ac17 --- /dev/null +++ b/tools/bosses/local_vs_audit.py @@ -0,0 +1,72 @@ +"""Find instruction-shape candidates for native boss adapters, without patching. + +A match is not proof of player ownership or a safe hook continuation. In +particular, absolute relocations and branch destinations are masked; callers, +object lifetimes and side effects still require separate verification. +""" +import argparse +import bisect +import hashlib +import json +from pathlib import Path +import re +import sys +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import selftest +import vomap + + +def audit(path): + v = selftest.load_patcher() + digest = hashlib.md5(path.read_bytes()).hexdigest() + if digest not in v.BUILDS: + raise ValueError('Expected a recognized pristine executable') + build = v.BUILDS[digest] + exe = vomap.Exe(str(path)) + text = exe.d[exe.off(exe.text_lo):exe.off(exe.text_hi)] + relocations = sorted(exe.relocs) + symbols = {} + for name, address in build.symbols.items(): + if not name.startswith('BS_') or not isinstance(address, int): + continue + if not exe.text_lo <= address < exe.text_hi: + continue + raw = exe.d[exe.off(address):exe.off(address) + 128] + instructions = list(vomap.md.disasm(raw, address))[:16] + if len(instructions) < 6: + continue + size = instructions[-1].address + instructions[-1].size - address + mask = set() + for insn in instructions: + begin = bisect.bisect_left(relocations, insn.address) + end = bisect.bisect_left(relocations, insn.address + insn.size) + for relocation in relocations[begin:end]: + mask.update(range(relocation-address, relocation-address+4)) + if insn.group(vomap.capstone.CS_GRP_JUMP) or insn.group(vomap.capstone.CS_GRP_CALL): + if insn.imm_size: + offset = insn.address-address+insn.imm_offset + mask.update(range(offset, offset+insn.imm_size)) + pattern = b''.join(b'.' if i in mask else re.escape(bytes([raw[i]])) for i in range(size)) + matches = [exe.text_lo + match.start() for match in re.finditer(b'(?=('+pattern+b'))', text, re.S)] + assert address in matches, (build.short, name) + symbols[name] = {'source': hex(address), 'window_size': size, + 'instruction_count': len(instructions), + 'candidates': [hex(a) for a in matches if a != address]} + assert hashlib.md5(path.read_bytes()).hexdigest() == digest + return {'edition': build.short, 'original_md5': digest, + 'status': 'NEEDS VALIDATION: candidate addresses, not verified B adapters', + 'symbols': symbols} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('original', type=Path) + parser.add_argument('output', type=Path) + args = parser.parse_args() + result = audit(args.original) + args.output.write_text(json.dumps(result, indent=2)+'\n') + print(result['edition'], len(result['symbols']), 'code windows; executable unchanged') + + +if __name__ == '__main__': + main() diff --git a/tools/bosses/test_local_vs_boundary.py b/tools/bosses/test_local_vs_boundary.py new file mode 100644 index 0000000..14aa8fe --- /dev/null +++ b/tools/bosses/test_local_vs_boundary.py @@ -0,0 +1,81 @@ +"""Execute frozen boss entry points; distinguish SP guarantees from VS support. + +These are evidence fixtures for B03, not tests of an implemented VS extension. +They deliberately preserve B02's existing behavior until verified replacements +are available. No game files are patched or copied. +""" +from pathlib import Path +import struct +import sys +from unicorn import Uc, UC_ARCH_X86, UC_MODE_32 +from unicorn.x86_const import UC_X86_REG_EAX, UC_X86_REG_ECX, UC_X86_REG_EBP, UC_X86_REG_ESP, UC_X86_REG_EFLAGS +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import selftest + +v = selftest.load_patcher() +STACK = 0x10000000 +STOP = 0x11000000 +B_OBJECTS = {'retail': (0x01ef8ab0, 0x01ef9f60), + 'jpre': (0x01ef5450, 0x01ef5ad0)} + +class Fixture: + def __init__(self, build): + self.build = build + self.uc = Uc(UC_ARCH_X86, UC_MODE_32) + self.pages = set() + self.base = v.cave_va('BOSSES', build) + self.labels = v.BLOBS['BOSSES'][2] + blob = v.link('BOSSES', build) + self.write(self.base, blob) + self.write(STACK, bytes(4096)) + self.write(STOP, b'\x90') + + def write(self, address, data): + for page in range(address & ~4095, (address + len(data) + 4095) & ~4095, 4096): + if page not in self.pages: + self.uc.mem_map(page, 4096) + self.pages.add(page) + self.uc.mem_write(address, data) + + def word(self, address, value): + self.write(address, struct.pack(' Date: Mon, 19 Oct 2026 14:35:19 -0700 Subject: [PATCH 2/2] Verify native Player B initialization and logical input contracts --- docs/project/LOCAL-VS-BOSSES.md | 45 +++++++++ tools/bosses/test_player_b_native.py | 143 +++++++++++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 tools/bosses/test_player_b_native.py diff --git a/docs/project/LOCAL-VS-BOSSES.md b/docs/project/LOCAL-VS-BOSSES.md index 0158dd1..394bd8d 100644 --- a/docs/project/LOCAL-VS-BOSSES.md +++ b/docs/project/LOCAL-VS-BOSSES.md @@ -85,3 +85,48 @@ Controller assignments, F7, movie ownership, display modes, networking, Apply/Restore and saved progression remain unchanged. A B03 UAT ZIP must only be assembled once the native extension and its tests exist; renaming B02 would not provide a Local VS test candidate. + +## Player B initialization and input (B03-P02) + +The pristine engine already provides more of the required path than the first +candidate scan established. `tools/bosses/test_player_b_native.py` executes two +bounded native slices in Retail and JPRE, without changing instructions or +substituting callees. It refuses unrecognized executable hashes. + +| Contract | Retail | JPRE | Evidence | +|---|---|---|---| +| B combatant initializer | `4c74e6` | `4c5086` | VERIFIED through callback selection, before environment setup | +| Initialization continuation | `4c7695` | `4c5235` | Terrain, palettes and subsequent setup are outside the fixture | +| B machine-model table | `6070a0` | `601e30` | VERIFIED: ten entries, including both bosses | +| Jaguarandi mode-0 callback | `4c722e` | `4c4dce` | VERIFIED: same callback choice as the original eight | +| Z-Gradt mode-0 callback | `4c742c` | `4c4fcc` | VERIFIED: distinct callback and setup branch | +| Z-Gradt setup entry | `5771d1` | `5724bd` | VERIFIED entry reached; its body is not executed by this fixture | +| B logical-input decoder | `4c5772` | `4c339a` | VERIFIED through input-bank selection and initial direction decoding | + +The initializer takes ten arguments: combatant, opponent, object mode, machine +identity, opponent identity, palette side, model pointer, X, Z and angle. Its +initial slice stores identity at `+0x64`, model at `+0x6c`, opponent at `+0x74` +and an update callback at `+4`. Both native B objects accept these values. The +fixture covers all ten identities and modes 0, 1, 3 and 4, stopping before +external setup work. It does not establish that every mode is a human path. + +For mode 0, Jaguarandi uses the existing normal-machine callback. That callback +calls B's existing movement/update and model routine. Z-Gradt uses its separate +setup and callback; replacing that callback with Jaguarandi's is UNSUITABLE. +There is no evidence here that replacement player objects or duplicate combat +systems are required. + +The input slice reads bank zero for mode 0 and the alternate bank for mode 3. +This decision does not inspect machine identity. Both existing B objects were +exercised with all ten identities and distinct bank sentinels. The other B +object and the A player object remained unchanged in these bounded tests. +Machine identity alone therefore does not require a new controller decoder. +This does not prove full-match input ownership, weapon execution or camera +routing. Modes and engine copies must not be relabeled as permanent P1/P2 IDs. + +Each edition passes 80 initialization-dispatch cases and 40 input-bank cases. +These are native contract tests, not complete initialization or physical play. +No runtime hook, selection option, recipe or UAT package is introduced by P02's +current changes. Player B gameplay implementation remains incomplete. Next work +must connect Local VS selection to these existing initialization paths and +verify downstream model, weapon, collision, camera and match-lifecycle behavior. diff --git a/tools/bosses/test_player_b_native.py b/tools/bosses/test_player_b_native.py new file mode 100644 index 0000000..201d32d --- /dev/null +++ b/tools/bosses/test_player_b_native.py @@ -0,0 +1,143 @@ +"""Verify pristine B-engine initialization and logical input routing slices. + +Usage: python tools/bosses/test_player_b_native.py ORIGINAL.exe + +These tests execute existing game instructions, not an implemented Local VS +extension. Initialization stops before terrain, palette and weapon setup; +Z-Gradt stops at entry to its setup routine. No callee is replaced with a stub. +""" +import hashlib +from pathlib import Path +import struct +import sys + +from unicorn import Uc, UC_ARCH_X86, UC_MODE_32, UC_HOOK_CODE +from unicorn.x86_const import UC_X86_REG_ESP, UC_X86_REG_EIP + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from vomap import Exe + +# Addresses checked against each pristine executable, not a constant delta. +EDITION = { + 'a464b0ff32d5bab499f265e45658504e': dict( + name='Retail', init=0x4c74e6, initialized=0x4c7695, + standard=0x4c722e, cpu=0x4c7366, alternate=0x4c7308, + replay=0x4c7337, ztick=0x4c742c, zsetup=0x5771d1, + cpu_setup=0x5adc1f, models=0x6070a0, + objects=(0x1ef8ab0, 0x1ef9f60), a_object=0x1ae0c40, + input=0x4c5772, input_end=0x4c591d, + banks=(0x365198c, 0x3651994, 0x3651a0c, 0x3651a14)), + 'd19320bdc3381a48228990907910a391': dict( + name='JPRE', init=0x4c5086, initialized=0x4c5235, + standard=0x4c4dce, cpu=0x4c4f06, alternate=0x4c4ea8, + replay=0x4c4ed7, ztick=0x4c4fcc, zsetup=0x5724bd, + cpu_setup=0x5a8995, models=0x601e30, + objects=(0x1ef5450, 0x1ef5ad0), a_object=0x1adb930, + input=0x4c339a, input_end=0x4c3545, + banks=(0x364c66c, 0x364c674, 0x364c5ec, 0x364c5f4)), +} +STACK = 0x10000000 + + +class Native: + def __init__(self, exe): + self.uc = Uc(UC_ARCH_X86, UC_MODE_32) + for _, va, virtual, offset, raw in exe.secs: + address = 0x400000 + va + self.uc.mem_map(address, (max(virtual, raw) + 4095) & ~4095) + if raw: + self.uc.mem_write(address, exe.d[offset:offset + raw]) + self.uc.mem_map(STACK, 4096) + + def word(self, address, value): + self.uc.mem_write(address, struct.pack('