aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorgbc dev <gbc@localhost>2026-07-15 11:56:21 +0200
committergbc dev <gbc@localhost>2026-07-15 11:56:21 +0200
commitb3a855234dc4b7ab3cab733cc21602485b8b3de4 (patch)
tree744616df262010c853e31ccab07260e93903537b
parentrender: no status line in sixel mode (pure graphics output only) (diff)
downloadsl0pboy-b3a855234dc4b7ab3cab733cc21602485b8b3de4.tar.gz
sl0pboy-b3a855234dc4b7ab3cab733cc21602485b8b3de4.tar.xz
sl0pboy-b3a855234dc4b7ab3cab733cc21602485b8b3de4.zip
tools: stitch_overworld.py - DFS EXPLORE's connection graph into one overworld image
-rw-r--r--tools/stitch_overworld.py111
1 files changed, 111 insertions, 0 deletions
diff --git a/tools/stitch_overworld.py b/tools/stitch_overworld.py
new file mode 100644
index 0000000..c4439b1
--- /dev/null
+++ b/tools/stitch_overworld.py
@@ -0,0 +1,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})")