Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 31 additions & 5 deletions tools/bg_binary_to_c.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@ class Portal4Point:
class BGBinaryParser:
def __init__(self, binary_path: str, output_path: str = None):
self.binary_path = binary_path
self.output_path = output_path or binary_path.replace('.bin', '.c')
self.header_path = output_path.replace('.c', '.h') if output_path else binary_path.replace('.bin', '.h')
# Swap the extension only: str.replace('.c', '.h') also hits a ".c"
# anywhere in the directory path (e.g. /home/me/.config/ -> .honfig/).
self.output_path = output_path or os.path.splitext(binary_path)[0] + '.c'
self.header_path = os.path.splitext(self.output_path)[0] + '.h'
self.binary_data = b''
self.header = None
self.room_entries = []
Expand Down Expand Up @@ -75,6 +77,15 @@ def to_file_offset(self, n64_addr: int) -> int:
return 0
return n64_addr - N64_BASE_ADDRESS

def is_valid_address(self, n64_addr: int) -> bool:
"""Whether a word looks like a pointer into this bg file.

Used to tell a table row from the data that follows it: a portal
table can end where the next word is 0x04000000 -- float geometry,
not an address.
"""
return N64_BASE_ADDRESS <= n64_addr < N64_BASE_ADDRESS + len(self.binary_data)

def read_u32(self, offset: int) -> int:
"""Read big-endian u32 at offset"""
return struct.unpack('>I', self.binary_data[offset:offset+4])[0]
Expand Down Expand Up @@ -163,11 +174,22 @@ def parse_portal_data_table(self):
room2 = self.read_u8(offset + 5)
control = self.read_u16(offset + 6)

# Check for terminator
# A null pointer is a terminator row, but NOT necessarily the last
# row: Cradle's table is two null rows, and stopping at the first
# emitted one 8-byte entry too few. Every pointer after it then sat
# 8 bytes low, so the file was the right size, compiled, and was
# wrong throughout. Consume null rows and keep going; the table
# really ends where the pointer is neither null nor a plausible
# in-range address -- Cradle's next word is 0x04000000, the start
# of float geometry data.
if portal_offset_raw == 0 and room1 == 0 and room2 == 0:
self.portal_entries.append(PortalDataEntry(0, 0, 0, 0))
offset += entry_size
nxt = self.read_u32(offset)
if nxt == 0 or self.is_valid_address(nxt):
continue
break

# Convert N64 address to file offset
portal_offset = self.to_file_offset(portal_offset_raw)

Expand Down Expand Up @@ -402,8 +424,12 @@ def generate_c_file(self):
if self.header:
portal_lines = ['struct portal_data_table_entry portal_data_table[] = {']
for i, entry in enumerate(self.portal_entries):
# Comma on every row but the last. The null row used to omit it
# unconditionally, which was fine only while a table could hold
# exactly one; Cradle has two and the pair would not compile.
last = (i == len(self.portal_entries) - 1)
if entry.portal_offset == 0:
portal_lines.append(' {0, 0, 0, 0}')
portal_lines.append(' {0, 0, 0, 0}' + ('' if last else ','))
else:
# Find which portal index to reference (the first one at this offset)
portal_idx = offset_to_portal_idx.get(entry.portal_offset, i)
Expand Down
138 changes: 138 additions & 0 deletions tools/brief_binary_to_c.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#!/usr/bin/env python3
"""GoldenEye briefing binary to C converter.

The class nobody had counted. `bg`, `chr`, `gun`, `prop`, `setup`, `stan` and
`text` were the seven tracked as needing converters; extracting Cradle's full
file set turned up an eighth, `brief`, with no converter and no mention.

The format is the smallest of the lot -- a fixed 48 bytes, all big-endian,
matching struct BriefStruct in assets/obseg/brief/Ubrief.h:

u16 brief[4] background, M, Q, Moneypenny
{ u16 text; u16 difficulty } x OBJECTIVES_MAX (10)

4*2 + 10*4 = 48, which is exactly the file size on disk.

Text ids decode through getStringID(bank, slot) = bank * 0x400 + slot
(bondconstants.h), so bank = id >> 10 and slot = id & 0x3ff. Bank names are
read from bondconstants.h at run time rather than hardcoded -- 0x2400 gives
bank 9, and the enum's index 9 is LCRAD, which is what Cradle's committed
source says.

THE DIFFICULTY FIELD IS NOT THE DIFFICULTY ENUM, and assuming it is puts every
objective one step too easy. Retail Cradle stores 0 where its committed source
reads DIFFICULTY_AGENT, but DIFFICULTY_AGENT is 1 -- the enum begins at
DIFFICULTY_MULTI, which a solo briefing can never use. Correlating every
committed brief against its own binary gives the mapping outright:

stored 0 -> DIFFICULTY_AGENT (enum 1)
stored 1 -> DIFFICULTY_SECRET (enum 2)
stored 2 -> DIFFICULTY_00 (enum 3)

so stored = enum - 1. Stored 3 -> DIFFICULTY_007 follows the pattern but does
not appear in any committed brief, so it is emitted with a comment saying so.

Usage:
python3 tools/brief_binary_to_c.py <in.bin> [out.c] [--name UbriefcradZ]
"""

import argparse
import os
import re
import struct
import sys

BRIEF_LEN = 4
OBJECTIVES_MAX = 10
SIZE = BRIEF_LEN * 2 + OBJECTIVES_MAX * 4

# stored value -> name. Derived, not assumed; see the docstring.
DIFFICULTY = {0: "DIFFICULTY_AGENT", 1: "DIFFICULTY_SECRET",
2: "DIFFICULTY_00", 3: "DIFFICULTY_007"}
UNVERIFIED = {3}

REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


def text_banks():
"""Bank names in enum order, read from bondconstants.h."""
try:
s = open(os.path.join(REPO, "src", "bondconstants.h"),
errors="replace").read()
i = s.rfind("{", 0, s.index("LCRAD,"))
j = s.index("}", i)
names = [re.sub(r"\s*(=.*|/\*.*)", "", l).strip().rstrip(",")
for l in s[i + 1:j].splitlines() if l.strip()]
return [n for n in names if n]
except (OSError, ValueError):
return []


def parse(data):
if len(data) < SIZE:
raise ValueError(f"{len(data)} bytes, expected at least {SIZE}")
brief = list(struct.unpack_from(f">{BRIEF_LEN}H", data, 0))
objs = []
for k in range(OBJECTIVES_MAX):
t, d = struct.unpack_from(">HH", data, BRIEF_LEN * 2 + k * 4)
objs.append((t, d))
return {"brief": brief, "objectives": objs}


def sid(v, banks):
if v == 0:
return "0"
bank, slot = v >> 10, v & 0x3FF
name = banks[bank] if 0 <= bank < len(banks) else None
return f"getStringID({name}, {slot})" if name else f"{v:#06x}"


def emit(r, name, banks):
L = ["/* Generated by tools/brief_binary_to_c.py. */",
'#include "Ubrief.h"', "",
f"struct BriefStruct {name} = {{",
" { /* Mission Briefing */"]
labels = ["Background", "M Briefing", "Q Branch", "Moneypenny"]
for i, v in enumerate(r["brief"]):
comma = "," if i < BRIEF_LEN - 1 else ""
L.append(f" {sid(v, banks)}{comma}".ljust(54) + f"/* {labels[i]} */")
L.append(" },")
L.append(" { /* Objectives (Text and Difficulty) */")
for i, (t, d) in enumerate(r["objectives"]):
comma = "," if i < OBJECTIVES_MAX - 1 else ""
if t == 0:
L.append(f" {{0, 0}}{comma}".ljust(47) + "/* Not assigned */")
else:
dn = DIFFICULTY.get(d, str(d))
note = " /* stored 3; unverified, no committed brief uses it */" \
if d in UNVERIFIED else ""
L.append(f" {{{sid(t, banks)}, {dn}}}{comma}{note}")
L.append(" }")
L.append("};")
return "\n".join(L) + "\n"


def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("input")
ap.add_argument("output", nargs="?")
ap.add_argument("--name", help="symbol name (default: from filename)")
a = ap.parse_args()

data = open(a.input, "rb").read()
try:
r = parse(data)
except ValueError as e:
sys.exit(f"{a.input}: {e}")
banks = text_banks()
name = a.name or os.path.basename(a.input).rsplit(".", 1)[0]
out = a.output or a.input.replace(".bin", ".c")
open(out, "w").write(emit(r, name, banks))
used = sum(1 for t, _ in r["objectives"] if t)
print(f"{os.path.basename(a.input)}: {len(data)} bytes, "
f"{used} objective(s) -> {out}")


if __name__ == "__main__":
main()
Loading