aboutsummaryrefslogtreecommitdiffstats
path: root/tools/stitch_overworld.py
blob: c4439b1d7b3d94f40806c1c8545e862bfffa4715 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
#!/usr/bin/env python3
"""stitch_overworld.py <sock> <out.png> — drive EXPLORE mode, DFS the map
connection graph, screenshot every reachable overworld map, and composite them
into one big overworld image positioned by connection direction.
Assumes EXPLORE is ALREADY open (on the origin map) when this starts."""
import socket, sys, time
from PIL import Image, ImageDraw

sock, out = sys.argv[1], sys.argv[2]
c = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM); c.settimeout(8); c.connect(sock); c.recv(200)
def cmd(l):
    c.sendall((l+"\n").encode()); b=b""
    while b"\n" not in b: b+=c.recv(65536)
    return b.split(b"\n",1)[0].decode()
def rd(a,n):
    o=[]
    while n>0:
        k=min(4096,n); h=cmd(f"read 0x{a:04X} {k}").split()[-1]
        o+=[int(h[i*2:i*2+2],16) for i in range(k)]; a+=k; n-=k
    return o
reg=lambda a: rd(a,1)[0]

def capture():
    LCDC=reg(0xFF40);SCY=reg(0xFF42);SCX=reg(0xFF43)
    cmd("write 0xFF4F 00"); v0=rd(0x8000,0x2000)
    cmd("write 0xFF4F 01"); v1=rd(0x8000,0x2000)
    cmd("write 0xFF4F 00")
    vb=lambda bank,addr:(v0 if bank==0 else v1)[addr-0x8000]
    bgmap=0x9C00 if (LCDC>>3)&1 else 0x9800
    uns=(LCDC>>4)&1
    ta=lambda idx:0x8000+idx*16 if uns else 0x9000+((idx^0x80)-0x80)*16
    shade=[255,170,85,0]
    fb=[[255]*160 for _ in range(144)]
    def fetch(mx,my):
        ent=bgmap+(my//8)*32+(mx//8)
        idx=vb(0,ent);attr=vb(1,ent)
        bank=(attr>>3)&1;xf=(attr>>5)&1;yf=(attr>>6)&1
        a=ta(idx); ry=(7-(my%8)) if yf else (my%8); rx=(7-(mx%8)) if xf else (mx%8)
        b1=vb(bank,a+ry*2);b2=vb(bank,a+ry*2+1)
        return shade[((b1>>(7-rx))&1)|(((b2>>(7-rx))&1)<<1)]
    for sy in range(144):
        for sx in range(160):
            if LCDC&1: fb[sy][sx]=fetch((sx+SCX)&0xFF,(sy+SCY)&0xFF)
    img=Image.new("L",(160,144))
    img.putdata([fb[y][x] for y in range(144) for x in range(160)])
    return img.convert("RGB")

def curmap(): return reg(0xD35D)
def conns():
    cn=reg(0xD36F)
    return {'N':reg(0xD370) if cn&8 else None,
            'S':reg(0xD37B) if cn&4 else None,
            'W':reg(0xD386) if cn&2 else None,
            'E':reg(0xD391) if cn&1 else None}
def press(k, settle=1.4):
    cmd("+"+k); time.sleep(0.26); cmd("-"+k); time.sleep(settle)

def move(k, src, target):
    """press k to go src->target, with retries; only re-press while still on src."""
    for _ in range(4):
        press(k)
        m=curmap()
        if m==target: return True
        if m!=src: return False   # ended somewhere unexpected; don't hammer
    return False

OFF={'N':(0,-1),'S':(0,1),'W':(-1,0),'E':(1,0)}
KEY={'N':'up','S':'down','W':'left','E':'right'}
OPP={'N':'S','S':'N','W':'E','E':'W'}

cmd("release")
shots={}   # map_id -> RGB image
grid={}    # map_id -> (col,row)
cells={}   # (col,row) -> map_id  (collision detect)
order=[]

def visit(mid,col,row):
    shots[mid]=capture(); grid[mid]=(col,row); order.append(mid)
    if (col,row) in cells and cells[(col,row)]!=mid:
        print(f"  COLLISION at ({col},{row}): 0x{cells[(col,row)]:02X} vs 0x{mid:02X}")
    cells[(col,row)]=mid
    cn=conns()
    print(f"map 0x{mid:02X} @({col},{row}) conn "+" ".join(f"{d}=0x{v:02X}" for d,v in cn.items() if v is not None))
    for d in ('N','S','W','E'):
        nb=cn[d]
        if nb is None or nb in grid: continue
        if not move(KEY[d], mid, nb):
            print(f"  !! {d} 0x{mid:02X}->0x{nb:02X} failed (on 0x{curmap():02X}); resync")
            # try to get back to mid before continuing
            if curmap()!=mid: move(KEY[OPP[d]], curmap(), mid)
            continue
        dc,dr=OFF[d]; visit(nb,col+dc,row+dr)
        if not move(KEY[OPP[d]], nb, mid):
            print(f"  !! backtrack 0x{nb:02X}->0x{mid:02X} failed (on 0x{curmap():02X})")

start=curmap()
print(f"start map=0x{start:02X}")
visit(start,0,0)

cols=[p[0] for p in grid.values()]; rows=[p[1] for p in grid.values()]
minc,maxc=min(cols),max(cols); minr,maxr=min(rows),max(rows)
W=(maxc-minc+1)*160; H=(maxr-minr+1)*144
canvas=Image.new("RGB",(W,H),(40,40,40))
dr=ImageDraw.Draw(canvas)
for mid,(col,row) in grid.items():
    x=(col-minc)*160; y=(row-minr)*144
    canvas.paste(shots[mid],(x,y))
    dr.rectangle([x,y,x+159,y+143],outline=(255,0,0))
    dr.text((x+2,y+2),f"{mid:02X}",fill=(255,0,0))
canvas.save(out)
print(f"\nstitched {len(shots)} maps -> {out}  ({W}x{H})")