mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-28 23:00:21 -04:00
596 lines
21 KiB
Python
596 lines
21 KiB
Python
#!/usr/bin/env python3
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
REPO = HERE.parent
|
|
DEFAULT_ROM = REPO.parent / "pokeemerald" / "pokeemerald.gba"
|
|
DEFAULT_SAV = REPO / ".claude" / "skills" / "pygba-headless" / "assets" / "pokemon-emerald-all-shiny-fixed.sav"
|
|
ROM_SHA1 = "f3ae088181bf583e55daf962a92bb46f4f1d07b7"
|
|
|
|
KEYS = {"A": 0, "B": 1, "select": 2, "start": 3, "right": 4, "left": 5, "up": 6, "down": 7, "R": 8, "L": 9}
|
|
|
|
# pokeemerald/pokeemerald.map
|
|
SYM = {
|
|
"gMain": 0x030022C0,
|
|
"gSaveBlock1Ptr": 0x03005D8C,
|
|
"gSaveBlock2Ptr": 0x03005D90,
|
|
"gPokemonStoragePtr": 0x03005D94,
|
|
"gSaveCounter": 0x03006200,
|
|
"gPlayerParty": 0x020244EC,
|
|
"gPlayerPartyCount": 0x020244E9,
|
|
"gTasks": 0x03005E00,
|
|
"sGlobalScriptContext": 0x03000E40,
|
|
"sStartMenuCursorPos": 0x0203760E,
|
|
"sNumStartMenuActions": 0x0203760F,
|
|
"sCurrentStartMenuActions": 0x02037610,
|
|
"CB2_Overworld": 0x08085E5C,
|
|
"CB2_MainMenu": 0x0802F6B0,
|
|
"CB2_NamingScreen": 0x080E4F58,
|
|
"BattleMainCB2": 0x08038420,
|
|
"gBattleTypeFlags": 0x02022FEC,
|
|
"Task_HandleTruckSequence": 0x080FB36C,
|
|
"Task_DisplayMainMenu": 0x0802FBA4,
|
|
}
|
|
MENU_ACTION_SAVE = 5
|
|
SB1_SIZE, SB2_SIZE, STORAGE_SIZE = 0x3D88, 0xF2C, 0x83D0
|
|
|
|
|
|
def lua_consts(kind):
|
|
text = (REPO / "src/core/game3/constants/emerald" / (kind + ".lua")).read_text()
|
|
out = {}
|
|
for name, val in re.findall(r"\b([A-Z][A-Z0-9_]+) = (-?0x[0-9A-Fa-f]+|-?\d+),", text):
|
|
out.setdefault(name, int(val, 0))
|
|
return out
|
|
|
|
|
|
class Emu:
|
|
def __init__(self, rom, save_bytes=None, video=False):
|
|
import mgba.log
|
|
mgba.log.silence()
|
|
import mgba.core
|
|
from mgba._pylib import ffi
|
|
self.ffi = ffi
|
|
self.dir = Path(tempfile.mkdtemp())
|
|
path = self.dir / "rom.gba"
|
|
path.write_bytes(Path(rom).read_bytes())
|
|
self.sav = self.dir / "rom.sav"
|
|
self.sav.write_bytes(save_bytes if save_bytes is not None else b"\xff" * 0x20000)
|
|
self.core = mgba.core.load_path(str(path))
|
|
self.fb = None
|
|
if video:
|
|
import mgba.image
|
|
self.fb = mgba.image.Image(*self.core.desired_video_dimensions())
|
|
self.core.set_video_buffer(self.fb)
|
|
self.core.autoload_save()
|
|
self.core.reset()
|
|
self.frame = 0
|
|
|
|
def run(self, n=1, keys=()):
|
|
for k in keys:
|
|
self.core.add_keys(KEYS[k])
|
|
for _ in range(n):
|
|
self.core.run_frame()
|
|
self.frame += 1
|
|
for k in keys:
|
|
self.core.clear_keys(KEYS[k])
|
|
|
|
def press(self, key, hold=2, after=6):
|
|
self.run(hold, (key,))
|
|
self.run(after)
|
|
|
|
def region(self, addr):
|
|
mc = self.core.memory.u8._core
|
|
size = self.ffi.new("size_t *")
|
|
ptr = self.ffi.cast("uint8_t *", mc.getMemoryBlock(mc, addr >> 24, size))
|
|
return self.ffi.buffer(ptr, size[0])
|
|
|
|
def read(self, addr, n):
|
|
buf = self.region(addr)
|
|
a = addr & (len(buf) - 1)
|
|
return bytes(buf[a:a + n])
|
|
|
|
def write(self, addr, data):
|
|
buf = self.region(addr)
|
|
a = addr & (len(buf) - 1)
|
|
buf[a:a + len(data)] = data
|
|
|
|
def u8(self, a):
|
|
return self.read(a, 1)[0]
|
|
|
|
def u32(self, a):
|
|
return int.from_bytes(self.read(a, 4), "little")
|
|
|
|
def cb2(self):
|
|
return self.u32(SYM["gMain"] + 4) & ~1
|
|
|
|
def tasks(self):
|
|
out = []
|
|
for i in range(16):
|
|
b = self.read(SYM["gTasks"] + i * 40, 40)
|
|
if b[4]:
|
|
out.append(int.from_bytes(b[0:4], "little") & ~1)
|
|
return out
|
|
|
|
def blocks(self):
|
|
sb1 = self.read(self.u32(SYM["gSaveBlock1Ptr"]), SB1_SIZE)
|
|
sb2 = self.read(self.u32(SYM["gSaveBlock2Ptr"]), SB2_SIZE)
|
|
st = self.read(self.u32(SYM["gPokemonStoragePtr"]), STORAGE_SIZE)
|
|
return sb1, sb2, st
|
|
|
|
def flash(self):
|
|
self.run(120)
|
|
data = self.sav.read_bytes()
|
|
if not 0x20000 <= len(data) <= 0x2003F:
|
|
raise SystemExit("flash file is %d bytes" % len(data))
|
|
return data
|
|
|
|
def shot(self, path):
|
|
self.fb.to_pil().convert("RGB").save(path)
|
|
|
|
def wait_cb2(self, target, limit, tap=None, every=30):
|
|
for i in range(limit):
|
|
if self.cb2() == target:
|
|
return True
|
|
if tap and i % every == 0:
|
|
self.press(tap, 2, 0)
|
|
else:
|
|
self.run(1)
|
|
return self.cb2() == target
|
|
|
|
|
|
def to_main_menu(e, limit=2000):
|
|
return e.wait_cb2(SYM["CB2_MainMenu"], limit, tap="A", every=40)
|
|
|
|
|
|
def continue_game(e):
|
|
if not to_main_menu(e):
|
|
raise SystemExit("main menu not reached")
|
|
e.run(90)
|
|
if not e.wait_cb2(SYM["CB2_Overworld"], 1500, tap="A", every=40):
|
|
raise SystemExit("overworld not reached after CONTINUE")
|
|
e.run(240)
|
|
|
|
|
|
def new_game(e):
|
|
for _ in range(9000):
|
|
cb = e.cb2()
|
|
if cb == SYM["CB2_NamingScreen"]:
|
|
e.run(60)
|
|
e.press("A", 2, 30)
|
|
e.press("start", 2, 30)
|
|
e.press("A", 2, 60)
|
|
continue
|
|
if cb == SYM["CB2_Overworld"]:
|
|
break
|
|
if e.frame % 30 == 0:
|
|
e.press("A", 2, 0)
|
|
else:
|
|
e.run(1)
|
|
if e.cb2() != SYM["CB2_Overworld"]:
|
|
raise SystemExit("new game never reached the truck")
|
|
for _ in range(6000):
|
|
if SYM["Task_HandleTruckSequence"] not in e.tasks():
|
|
break
|
|
e.run(1)
|
|
e.run(600)
|
|
|
|
|
|
def settle(e, limit=3000):
|
|
quiet = 0
|
|
for _ in range(limit):
|
|
if e.u8(SYM["sGlobalScriptContext"] + 1) == 0 and e.cb2() == SYM["CB2_Overworld"]:
|
|
quiet += 1
|
|
if quiet >= 60:
|
|
return True
|
|
e.run(1)
|
|
else:
|
|
quiet = 0
|
|
e.press("B", 2, 10)
|
|
return False
|
|
|
|
|
|
def save_game(e):
|
|
before = e.u32(SYM["gSaveCounter"])
|
|
n = 0
|
|
for _ in range(5):
|
|
settle(e)
|
|
e.press("start", 2, 40)
|
|
n = e.u8(SYM["sNumStartMenuActions"])
|
|
if n:
|
|
break
|
|
e.press("B", 2, 30)
|
|
acts = list(e.read(SYM["sCurrentStartMenuActions"], n))
|
|
if MENU_ACTION_SAVE not in acts:
|
|
raise SystemExit("no SAVE in the start menu: %r" % acts)
|
|
cur = e.u8(SYM["sStartMenuCursorPos"])
|
|
for _ in range((acts.index(MENU_ACTION_SAVE) - cur) % n):
|
|
e.press("down", 2, 8)
|
|
e.press("A", 2, 10)
|
|
snap = None
|
|
for i in range(3000):
|
|
if e.u32(SYM["gSaveCounter"]) != before:
|
|
snap = e.blocks()
|
|
break
|
|
if i % 25 == 0:
|
|
e.press("A", 2, 0)
|
|
else:
|
|
e.run(1)
|
|
if snap is None:
|
|
raise SystemExit("the save never completed")
|
|
e.run(90)
|
|
for _ in range(4):
|
|
e.press("B", 2, 20)
|
|
e.run(120)
|
|
return snap
|
|
|
|
|
|
def walk_into_battle(e, steps=3000, seed=7):
|
|
import random
|
|
rng = random.Random(seed)
|
|
d = "left"
|
|
for i in range(steps):
|
|
if rng.random() < 0.3:
|
|
d = rng.choice(["left", "right", "up", "down"])
|
|
e.run(16, (d,))
|
|
if e.cb2() != SYM["CB2_Overworld"]:
|
|
return e.wait_cb2(SYM["BattleMainCB2"], 1200)
|
|
if i % 10 == 0:
|
|
e.press("B", 2, 4)
|
|
return False
|
|
|
|
|
|
def finish_battle(e):
|
|
trainer = e.u32(SYM["gBattleTypeFlags"]) & 8
|
|
for _ in range(200):
|
|
if e.cb2() == SYM["CB2_Overworld"]:
|
|
break
|
|
if trainer:
|
|
e.press("A", 2, 20)
|
|
else:
|
|
e.run(60)
|
|
e.press("right", 2, 6)
|
|
e.press("down", 2, 6)
|
|
e.press("A", 2, 30)
|
|
e.press("B", 2, 20)
|
|
if not e.wait_cb2(SYM["CB2_Overworld"], 4000, tap="B", every=30):
|
|
raise SystemExit("still in battle")
|
|
e.run(300)
|
|
|
|
|
|
def u16(b, o):
|
|
return int.from_bytes(b[o:o + 2], "little")
|
|
|
|
|
|
def u32(b, o):
|
|
return int.from_bytes(b[o:o + 4], "little")
|
|
|
|
|
|
def s16(b, o):
|
|
v = u16(b, o)
|
|
return v - 65536 if v >= 32768 else v
|
|
|
|
|
|
def s8(b, o):
|
|
return b[o] - 256 if b[o] >= 128 else b[o]
|
|
|
|
|
|
# pokeemerald/src/pokemon.c:2863
|
|
ORDER = [
|
|
(0, 1, 2, 3), (0, 1, 3, 2), (0, 2, 1, 3), (0, 3, 1, 2), (0, 2, 3, 1), (0, 3, 2, 1),
|
|
(1, 0, 2, 3), (1, 0, 3, 2), (2, 0, 1, 3), (3, 0, 1, 2), (2, 0, 3, 1), (3, 0, 2, 1),
|
|
(1, 2, 0, 3), (1, 3, 0, 2), (2, 1, 0, 3), (3, 1, 0, 2), (2, 3, 0, 1), (3, 2, 0, 1),
|
|
(1, 2, 3, 0), (1, 3, 2, 0), (2, 1, 3, 0), (3, 1, 2, 0), (2, 3, 1, 0), (3, 2, 1, 0),
|
|
]
|
|
|
|
|
|
def box_mon(raw):
|
|
if not any(raw):
|
|
return None
|
|
pid, otid = u32(raw, 0), u32(raw, 4)
|
|
key = pid ^ otid
|
|
sec = b"".join((u32(raw, 32 + i * 4) ^ key).to_bytes(4, "little") for i in range(12))
|
|
g = ORDER[pid % 24][0] * 12
|
|
a = ORDER[pid % 24][1] * 12
|
|
m = ORDER[pid % 24][3] * 12
|
|
return {
|
|
"pid": pid, "otid": otid, "species": u16(sec, g), "heldItem": u16(sec, g + 2), "exp": u32(sec, g + 4),
|
|
"moves": [u16(sec, a + i * 2) for i in range(4)], "ivWord": u32(sec, m + 4), "ribbons": u32(sec, m + 8),
|
|
"nickRaw": raw[8:18].hex(), "otRaw": raw[20:27].hex(), "flags": raw[19],
|
|
}
|
|
|
|
|
|
def items(b, off, count, key):
|
|
out = []
|
|
for i in range(count):
|
|
iid, q = u16(b, off + i * 4), u16(b, off + i * 4 + 2)
|
|
if key is not None:
|
|
q ^= key & 0xFFFF
|
|
if iid:
|
|
out.append([iid, q])
|
|
return out
|
|
|
|
|
|
def bitlist(b, off, n):
|
|
return [i for i in range(n * 8) if b[off + i // 8] >> (i % 8) & 1]
|
|
|
|
|
|
def time_of(b, o):
|
|
return [s16(b, o), s8(b, o + 2), s8(b, o + 3), s8(b, o + 4)]
|
|
|
|
|
|
def oracle(sb1, sb2, st):
|
|
key = u32(sb2, 0xAC)
|
|
party = []
|
|
for i in range(min(sb1[0x234], 6)):
|
|
o = 0x238 + i * 100
|
|
mon = box_mon(sb1[o:o + 80])
|
|
mon.update({"level": sb1[o + 84], "hp": u16(sb1, o + 86), "maxHp": u16(sb1, o + 88), "mail": sb1[o + 85]})
|
|
party.append(mon)
|
|
boxes = 0
|
|
firstbox = None
|
|
for i in range(14 * 30):
|
|
raw = st[4 + i * 80:4 + i * 80 + 80]
|
|
if any(raw) and raw[19] & 2:
|
|
boxes += 1
|
|
if firstbox is None:
|
|
firstbox = dict(box_mon(raw), slot=i)
|
|
trees = {}
|
|
for i in range(128):
|
|
t = sb1[0x169C + i * 8:0x169C + i * 8 + 8]
|
|
if any(t):
|
|
trees[i] = [t[0], t[1] & 0x7F, u16(t, 2), t[4]]
|
|
blocks = [list(sb1[0x848 + i * 8:0x848 + i * 8 + 7]) for i in range(40) if any(sb1[0x848 + i * 8:0x848 + i * 8 + 8])]
|
|
stats = {}
|
|
for i in range(64):
|
|
v = u32(sb1, 0x159C + i * 4) ^ key
|
|
if v:
|
|
stats[i] = v
|
|
rv = {}
|
|
for i in range(256):
|
|
v = u16(sb1, 0x139C + i * 2)
|
|
if v:
|
|
rv[0x4000 + i] = v
|
|
dc = 0x3030
|
|
return {
|
|
"nameRaw": sb2[0:8].hex(), "gender": sb2[8], "tid": u16(sb2, 10), "sid": u16(sb2, 12),
|
|
"playTime": [u16(sb2, 14), sb2[16], sb2[17]], "key": key, "warpFlags": sb2[9],
|
|
"options": u16(sb2, 0x14), "buttonMode": sb2[0x13], "nationalMagic": sb2[0x1A],
|
|
"money": u32(sb1, 0x490) ^ key, "coins": (u16(sb1, 0x494) ^ key) & 0xFFFF, "berryPowder": u32(sb2, 0x1F4) ^ key,
|
|
"registeredItem": u16(sb1, 0x496),
|
|
"pos": [s16(sb1, 0), s16(sb1, 2)], "location": [s8(sb1, 4), s8(sb1, 5), s8(sb1, 6), s16(sb1, 8), s16(sb1, 10)],
|
|
"heal": [s8(sb1, 0x1C), s8(sb1, 0x1D), s8(sb1, 0x1E), s16(sb1, 0x20), s16(sb1, 0x22)],
|
|
"weather": sb1[0x2E], "weatherCycleStage": sb1[0x2F],
|
|
"party": party, "boxCount": boxes, "firstBoxMon": firstbox, "currentBox": st[0],
|
|
"pockets": {
|
|
"ITEMS": items(sb1, 0x560, 30, key), "KEY_ITEMS": items(sb1, 0x5D8, 30, key),
|
|
"POKE_BALLS": items(sb1, 0x650, 16, key), "TM_CASE": items(sb1, 0x690, 64, key),
|
|
"BERRY_POUCH": items(sb1, 0x790, 46, key),
|
|
},
|
|
"pcItems": items(sb1, 0x498, 50, None),
|
|
"flags": bitlist(sb1, 0x1270, 300), "vars": rv, "gameStats": stats,
|
|
"dexOwned": bitlist(sb2, 0x28, 52), "dexSeen": bitlist(sb2, 0x5C, 52),
|
|
"daycare": {"offspringPersonality": u32(sb1, dc + 0x118), "stepCounter": sb1[dc + 0x11C],
|
|
"species": [(box_mon(sb1[dc + i * 0x8C:dc + i * 0x8C + 80]) or {}).get("species", 0) for i in range(2)],
|
|
"steps": [u32(sb1, dc + i * 0x8C + 0x88) for i in range(2)]},
|
|
"berryTrees": trees, "pokeblocks": blocks,
|
|
"localTimeOffset": time_of(sb2, 0x98), "lastBerryTreeUpdate": time_of(sb2, 0xA0),
|
|
"roamer": {"species": u16(sb1, 0x31DC + 8), "level": sb1[0x31DC + 12], "active": sb1[0x31DC + 19],
|
|
"personality": u32(sb1, 0x31DC + 4), "ivs": u32(sb1, 0x31DC)},
|
|
"mailItems": [u16(sb1, 0x2BE0 + i * 36 + 0x20) for i in range(16)],
|
|
"trainerRematchStepCounter": u16(sb1, 0x9C8),
|
|
"rematches": {i: sb1[0x9CA + i] for i in range(100) if sb1[0x9CA + i]},
|
|
"secretBaseId": sb1[0x1A9C], "giftRibbons": list(sb1[0x31A8:0x31A8 + 11]),
|
|
"decorInventory": list(sb1[0x2734:0x2734 + 150]), "playerRoomDecorations": list(sb1[0x271C:0x271C + 12]),
|
|
"outbreakSpecies": u16(sb1, 0x2B90), "dewfordWords": [u16(sb1, 0x2E68 + i * 8 + 4) for i in range(5)],
|
|
"linkRecord0": sb1[0x3150:0x3150 + 16].hex(), "contestWinnerSpecies": [u16(sb1, 0x2E90 + i * 32 + 8) for i in range(13)],
|
|
"battlePoints": u16(sb2, 0x64C + 0x86C),
|
|
}
|
|
|
|
|
|
def rle(data):
|
|
out, i, n = [], 0, len(data)
|
|
while i < n:
|
|
b = data[i]
|
|
if b in (0, 0xFF):
|
|
j = i
|
|
while j < n and data[j] == b:
|
|
j += 1
|
|
if j - i >= 6:
|
|
out.append(("Z" if b == 0 else "F") + "%x" % (j - i))
|
|
i = j
|
|
continue
|
|
j = i
|
|
while j < n:
|
|
if data[j] in (0, 0xFF):
|
|
k = j
|
|
while k < n and data[k] == data[j]:
|
|
k += 1
|
|
if k - j >= 6:
|
|
break
|
|
j = k
|
|
else:
|
|
j += 1
|
|
out.append(data[i:j].hex())
|
|
i = j
|
|
return " ".join(out)
|
|
|
|
|
|
def lua(v, ind=""):
|
|
if v is None:
|
|
return "nil"
|
|
if isinstance(v, bool):
|
|
return "true" if v else "false"
|
|
if isinstance(v, (int, float)):
|
|
return str(v)
|
|
if isinstance(v, str):
|
|
return json.dumps(v)
|
|
if isinstance(v, list):
|
|
if all(isinstance(x, (int, bool)) or x is None for x in v):
|
|
return "{ " + ", ".join(lua(x) for x in v) + " }"
|
|
inner = ind + " "
|
|
return "{\n" + "".join(inner + lua(x, inner) + ",\n" for x in v) + ind + "}"
|
|
if isinstance(v, dict):
|
|
inner = ind + " "
|
|
parts = []
|
|
for k in sorted(v, key=lambda x: (isinstance(x, str), x)):
|
|
key = ("[%d]" % k) if isinstance(k, int) else (k if re.match(r"^[A-Za-z_]\w*$", k) else "[%s]" % json.dumps(k))
|
|
parts.append(inner + key + " = " + lua(v[k], inner) + ",\n")
|
|
return "{\n" + "".join(parts) + ind + "}"
|
|
raise TypeError(type(v))
|
|
|
|
|
|
def doctor(e, C, donor):
|
|
sb1a, sb2a = e.u32(SYM["gSaveBlock1Ptr"]), e.u32(SYM["gSaveBlock2Ptr"])
|
|
sb1, sb2, _ = e.blocks()
|
|
key = u32(sb2, 0xAC)
|
|
|
|
# pokeemerald/src/load_save.c:286
|
|
def rekey32(block, base, off):
|
|
e.write(base + off, (u32(block, off) ^ key).to_bytes(4, "little"))
|
|
|
|
rekey32(sb1, sb1a, 0x490)
|
|
e.write(sb1a + 0x494, ((u16(sb1, 0x494) ^ key) & 0xFFFF).to_bytes(2, "little"))
|
|
for i in range(64):
|
|
rekey32(sb1, sb1a, 0x159C + i * 4)
|
|
rekey32(sb2, sb2a, 0x1F4)
|
|
for off, cnt in ((0x560, 30), (0x5D8, 30), (0x650, 16), (0x690, 64), (0x790, 46)):
|
|
for i in range(cnt):
|
|
o = off + i * 4 + 2
|
|
e.write(sb1a + o, ((u16(sb1, o) ^ key) & 0xFFFF).to_bytes(2, "little"))
|
|
e.write(sb2a + 0xAC, b"\0\0\0\0")
|
|
e.write(SYM["gPlayerParty"], donor)
|
|
e.write(SYM["gPlayerPartyCount"], bytes([1]))
|
|
potion = C["items"]["ITEM_POTION"]
|
|
for i in range(50):
|
|
e.write(sb1a + 0x498 + i * 4, potion.to_bytes(2, "little") + (i + 1).to_bytes(2, "little"))
|
|
dc = 0x3030
|
|
e.write(sb1a + dc, donor[:80])
|
|
e.write(sb1a + dc + 0x88, (4321).to_bytes(4, "little"))
|
|
e.write(sb1a + dc + 0x118, (0xDEADBEEF).to_bytes(4, "little"))
|
|
e.write(sb1a + dc + 0x11C, bytes([77]))
|
|
e.write(sb1a + 0x169C + 7 * 8, bytes([5, 3, 0x2C, 0x01, 4, 0x12, 0, 0]))
|
|
e.write(sb1a + 0x848, bytes([1, 20, 0, 5, 0, 0, 10, 0]))
|
|
flags = sb1[0x1270 + 0x950 // 8] | (1 << (0x950 % 8))
|
|
e.write(sb1a + 0x1270 + 0x950 // 8, bytes([flags]))
|
|
e.write(sb1a + 0x139C + 0xFF * 2, (1234).to_bytes(2, "little"))
|
|
e.write(sb1a + 0x9C8, (123).to_bytes(2, "little"))
|
|
e.write(sb1a + 0x9CA + 3, bytes([2]))
|
|
e.write(sb1a + 0x31A8, bytes([1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 5]))
|
|
e.write(sb2a + 0x98, (-2 & 0xFFFF).to_bytes(2, "little") + bytes([5, 30, 7]))
|
|
mail = 0x2BE0 + 6 * 36
|
|
e.write(sb1a + mail, b"".join(w.to_bytes(2, "little") for w in (0x0A0B, 0x0402, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF)))
|
|
e.write(sb1a + mail + 0x12, bytes([0xCC, 0xC3, 0xD0, 0xBB, 0xC6, 0x00, 0xFF, 0xFF]))
|
|
e.write(sb1a + mail + 0x1A, (0x12345678).to_bytes(4, "little") + (25).to_bytes(2, "little") + (121).to_bytes(2, "little"))
|
|
e.write(sb1a + 0x3150, bytes([0xCC, 0xC3, 0xD0, 0xBB, 0xC6, 0xFF, 0, 0]) + (4242).to_bytes(2, "little")
|
|
+ (3).to_bytes(2, "little") + (1).to_bytes(2, "little") + (0).to_bytes(2, "little"))
|
|
e.write(sb1a + 0x31DC, (0x3FFFFFFF).to_bytes(4, "little") + (0xCAFEBABE).to_bytes(4, "little")
|
|
+ (C["species"]["SPECIES_LATIOS"]).to_bytes(2, "little") + (100).to_bytes(2, "little")
|
|
+ bytes([40, 0, 1, 2, 3, 4, 5, 1]))
|
|
|
|
|
|
def capture(rom, bundled):
|
|
C = {"items": lua_consts("items"), "flags": lua_consts("flags"), "species": lua_consts("species")}
|
|
out = {"images": {}, "oracle": {}, "meta": {}}
|
|
bundled_bytes = Path(bundled).read_bytes()
|
|
|
|
e = Emu(rom, bundled_bytes)
|
|
continue_game(e)
|
|
shiny = e.blocks()
|
|
donor = e.read(SYM["gPlayerParty"], 100)
|
|
out["oracle"]["em_shiny"] = oracle(*shiny)
|
|
out["meta"]["em_shiny"] = {"sha1": hashlib.sha1(bundled_bytes).hexdigest(), "size": len(bundled_bytes),
|
|
"ramAfterContinue": True, "olderSlotOf": "em_battle"}
|
|
|
|
e = Emu(rom, bundled_bytes)
|
|
continue_game(e)
|
|
if not walk_into_battle(e):
|
|
raise SystemExit("no wild battle on the bundled save's route")
|
|
finish_battle(e)
|
|
snap = save_game(e)
|
|
out["images"]["em_battle"] = e.flash()
|
|
out["oracle"]["em_battle"] = oracle(*snap)
|
|
|
|
e = Emu(rom, None)
|
|
new_game(e)
|
|
snap = save_game(e)
|
|
fresh = e.flash()
|
|
out["images"]["em_fresh"] = fresh
|
|
out["oracle"]["em_fresh"] = oracle(*snap)
|
|
|
|
e = Emu(rom, fresh[:0x20000])
|
|
continue_game(e)
|
|
doctor(e, C, donor)
|
|
snap = save_game(e)
|
|
out["images"]["em_doctored"] = e.flash()
|
|
out["oracle"]["em_doctored"] = oracle(*snap)
|
|
|
|
for name, img in out["images"].items():
|
|
out["meta"][name] = {"sha1": hashlib.sha1(img).hexdigest(), "size": len(img), "ramAfterContinue": False}
|
|
return out
|
|
|
|
|
|
def write_fixture(data, path):
|
|
lines = ["-- tools/make_emerald_save_fixture.py (PyGBA, pokeemerald.gba sha1 " + ROM_SHA1 + ")", "return {", " images = {"]
|
|
for name in sorted(data["images"]):
|
|
lines.append(" %s = %s," % (name, json.dumps(rle(data["images"][name]))))
|
|
lines.append(" },")
|
|
lines.append(" meta = " + lua(data["meta"], " ") + ",")
|
|
lines.append(" oracle = " + lua(data["oracle"], " ") + ",")
|
|
lines.append("}")
|
|
Path(path).write_text("\n".join(lines) + "\n")
|
|
|
|
|
|
def continue_check(rom, sav, shot):
|
|
C = lua_consts("flags")
|
|
e = Emu(rom, Path(sav).read_bytes(), video=True)
|
|
if not to_main_menu(e):
|
|
raise SystemExit("main menu not reached")
|
|
e.run(150)
|
|
if shot:
|
|
e.shot(shot)
|
|
sb1, sb2, _ = e.blocks()
|
|
badges = [i + 1 for i in range(8) if (lambda f: sb1[0x1270 + f // 8] >> (f % 8) & 1)(C["FLAG_BADGE0%d_GET" % (i + 1)])]
|
|
info = {"name": sb2[0:8].hex(), "playTime": [u16(sb2, 14), sb2[16], sb2[17]], "badges": badges,
|
|
"dexOwned": len(bitlist(sb2, 0x28, 52)), "tid": u16(sb2, 10)}
|
|
continue_game_from_menu(e)
|
|
sb1, sb2, _ = e.blocks()
|
|
info["overworld"] = {"location": [s8(sb1, 4), s8(sb1, 5)], "pos": [s16(sb1, 0), s16(sb1, 2)], "party": sb1[0x234]}
|
|
if shot:
|
|
e.shot(shot.replace(".png", "_field.png"))
|
|
print(json.dumps(info))
|
|
|
|
|
|
def continue_game_from_menu(e):
|
|
if not e.wait_cb2(SYM["CB2_Overworld"], 1500, tap="A", every=40):
|
|
raise SystemExit("overworld not reached after CONTINUE")
|
|
e.run(240)
|
|
|
|
|
|
def main():
|
|
p = argparse.ArgumentParser()
|
|
p.add_argument("mode", choices=["fixtures", "continue"])
|
|
p.add_argument("sav", nargs="?")
|
|
p.add_argument("--rom", default=str(DEFAULT_ROM))
|
|
p.add_argument("--bundled", default=str(DEFAULT_SAV))
|
|
p.add_argument("--out", default=str(REPO / "tests/fixture_data/gen3_saves_emerald.lua"))
|
|
p.add_argument("--shot")
|
|
a = p.parse_args()
|
|
if hashlib.sha1(Path(a.rom).read_bytes()).hexdigest() != ROM_SHA1:
|
|
raise SystemExit("not the byte-exact pokeemerald.gba")
|
|
if a.mode == "fixtures":
|
|
write_fixture(capture(a.rom, a.bundled), a.out)
|
|
print("wrote", a.out)
|
|
else:
|
|
continue_check(a.rom, a.sav, a.shot)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|