aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--core.3058597bin0 -> 11264000 bytes
-rw-r--r--core.3061219bin0 -> 11268096 bytes
-rw-r--r--core.3063669bin0 -> 11268096 bytes
-rw-r--r--core.3068673bin0 -> 11268096 bytes
-rw-r--r--core.3072453bin0 -> 11268096 bytes
-rw-r--r--core.3073976bin0 -> 11268096 bytes
-rw-r--r--core.3076382bin0 -> 11268096 bytes
-rw-r--r--core.3080116bin0 -> 11268096 bytes
-rw-r--r--core.3081720bin0 -> 11268096 bytes
-rwxr-xr-xgbctl18
-rwxr-xr-xtools/gbhud.py185
11 files changed, 202 insertions, 1 deletions
diff --git a/core.3058597 b/core.3058597
new file mode 100644
index 0000000..8e1fd6b
--- /dev/null
+++ b/core.3058597
Binary files differ
diff --git a/core.3061219 b/core.3061219
new file mode 100644
index 0000000..9066b5d
--- /dev/null
+++ b/core.3061219
Binary files differ
diff --git a/core.3063669 b/core.3063669
new file mode 100644
index 0000000..63cdb39
--- /dev/null
+++ b/core.3063669
Binary files differ
diff --git a/core.3068673 b/core.3068673
new file mode 100644
index 0000000..f652f89
--- /dev/null
+++ b/core.3068673
Binary files differ
diff --git a/core.3072453 b/core.3072453
new file mode 100644
index 0000000..e72709c
--- /dev/null
+++ b/core.3072453
Binary files differ
diff --git a/core.3073976 b/core.3073976
new file mode 100644
index 0000000..04ca0da
--- /dev/null
+++ b/core.3073976
Binary files differ
diff --git a/core.3076382 b/core.3076382
new file mode 100644
index 0000000..5f9535b
--- /dev/null
+++ b/core.3076382
Binary files differ
diff --git a/core.3080116 b/core.3080116
new file mode 100644
index 0000000..a569f7e
--- /dev/null
+++ b/core.3080116
Binary files differ
diff --git a/core.3081720 b/core.3081720
new file mode 100644
index 0000000..0699dd4
--- /dev/null
+++ b/core.3081720
Binary files differ
diff --git a/gbctl b/gbctl
index 7eb521c..4c3d4f9 100755
--- a/gbctl
+++ b/gbctl
@@ -284,6 +284,22 @@ def cmd_screen(argv):
r = subprocess.run(args, capture_output=True, text=True)
sys.stdout.write(r.stdout)
+def cmd_hud(argv):
+ # spawn a tmux pane running the live debug HUD, bound to the resolved socket
+ sock_path = _resolve_sock(argv)
+ if "TMUX" not in os.environ:
+ die("not inside tmux; gbctl hud needs a tmux session")
+ hud = os.path.join(HERE, "tools", "gbhud.py")
+ split = "-h"
+ if "--split" in argv:
+ i = argv.index("--split"); split = {"h":"-h","v":"-v"}.get(argv[i+1],"-h")
+ cmd = "exec python3 %s --sock %s" % (_shquote(hud), _shquote(sock_path))
+ r = subprocess.run(["tmux", "split-window", split, "-P", "-F", "#{pane_id}", cmd],
+ capture_output=True, text=True)
+ if r.returncode != 0:
+ die("tmux spawn failed: %s" % r.stderr.strip())
+ print("hud pane=%s sock=%s" % (r.stdout.strip(), sock_path))
+
def cmd_monitor(argv):
secs = float(argv[0]) if argv and _isnum(argv[0]) else None
if secs is not None:
@@ -319,7 +335,7 @@ def _isnum(x):
# main dispatch
# ---------------------------------------------------------------------------
LIFECYCLE = {"spawn": cmd_spawn, "list": cmd_list, "stop": cmd_stop,
- "screen": cmd_screen, "monitor": cmd_monitor}
+ "screen": cmd_screen, "monitor": cmd_monitor, "hud": cmd_hud}
# convenience aliases -> raw protocol verbs
ALIASES = {"press": None, "tap": None, "hold": None, "buttons": None}
diff --git a/tools/gbhud.py b/tools/gbhud.py
new file mode 100755
index 0000000..f9f2c68
--- /dev/null
+++ b/tools/gbhud.py
@@ -0,0 +1,185 @@
+#!/usr/bin/env python3
+"""gbhud.py [--sock PATH] [--sym PATH] — live debug HUD for the gbc emulator
+running Pokemon Yellow. Reads WRAM over the control socket, resolves symbols
+from pokeyellow.sym, parses game state, and pretty-prints it, refreshing ~5Hz.
+
+Meant to run in its own tmux pane (see `gbctl hud`)."""
+import socket, sys, os, time, glob, re
+
+REGDIR = os.path.join(os.environ.get("XDG_RUNTIME_DIR", "/tmp"), "gbctl")
+SYM_DEFAULT = os.path.expanduser("~/dev/pokeyellow/pokeyellow.sym")
+MAPCONST = os.path.expanduser("~/dev/pokeyellow/constants/map_constants.asm")
+
+# ---- symbol + map-name tables ----
+def load_syms(path):
+ syms = {}
+ try:
+ for line in open(path):
+ m = re.match(r"\s*[0-9A-Fa-f]+:([0-9A-Fa-f]+)\s+(\S+)", line)
+ if m:
+ syms[m.group(2)] = int(m.group(1), 16)
+ except OSError:
+ pass
+ return syms
+
+def load_maps(path):
+ maps, idx = {}, 0
+ try:
+ for line in open(path):
+ m = re.match(r"\s*map_const\s+(\w+)", line)
+ if m:
+ maps[idx] = m.group(1).replace("_", " ").title()
+ idx += 1
+ except OSError:
+ pass
+ return maps
+
+def gb_decode(bs):
+ o = ""
+ for b in bs:
+ if b == 0x50: break
+ if 0x80 <= b <= 0x99: o += chr(ord('A') + b - 0x80)
+ elif 0xA0 <= b <= 0xB9: o += chr(ord('a') + b - 0xA0)
+ elif 0xF6 <= b <= 0xFF: o += "0123456789"[b - 0xF6]
+ elif b == 0x7F: o += " "
+ else: o += "?"
+ return o
+
+# ---- socket ----
+def resolve_sock(arg):
+ if arg: return arg
+ if os.environ.get("GBCTL_SOCK"): return os.environ["GBCTL_SOCK"]
+ live = []
+ for p in glob.glob(os.path.join(REGDIR, "*.json")):
+ import json
+ try:
+ d = json.load(open(p))
+ if os.path.exists(d.get("sock", "")): live.append(d["sock"])
+ except Exception: pass
+ return live[0] if len(live) == 1 else None
+
+class Mem:
+ """Batched WRAM reader over the control socket."""
+ def __init__(self, sock):
+ self.s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ self.s.settimeout(2); self.s.connect(sock)
+ self._line() # greeting
+ self.blocks = {}
+ def _line(self):
+ b = b""
+ while b"\n" not in b: b += self.s.recv(65536)
+ return b.split(b"\n", 1)[0].decode()
+ def read(self, addr, n):
+ self.s.sendall(f"read 0x{addr:04X} {n}\n".encode())
+ h = self._line().split()[-1]
+ return bytes(int(h[i*2:i*2+2], 16) for i in range(n))
+ def refresh(self):
+ self.blocks = {
+ 0xC100: self.read(0xC100, 0x20),
+ 0xCD48: self.read(0xCD48, 0x90),
+ 0xD158: self.read(0xD158, 0x1A0),
+ 0xD340: self.read(0xD340, 0x60),
+ 0xD520: self.read(0xD520, 0x20),
+ 0xD720: self.read(0xD720, 0x20),
+ }
+ def b(self, addr):
+ for base, blk in self.blocks.items():
+ if base <= addr < base + len(blk): return blk[addr - base]
+ return 0
+ def w(self, addr): # little-endian word
+ return self.b(addr) | (self.b(addr+1) << 8)
+ def wbe(self, addr): # big-endian word
+ return (self.b(addr) << 8) | self.b(addr+1)
+
+# ---- rendering ----
+C = lambda n: f"\x1b[{n}m"
+RESET, DIM, BOLD = C(0), C(2), C(1)
+CY, GR, YE, RD, MG, WH = C(96), C(92), C(93), C(91), C(95), C(97)
+FACING = {0x00: "DOWN", 0x04: "UP", 0x08: "LEFT", 0x0C: "RIGHT"}
+BADGES = ["BOULDER","CASCADE","THUNDER","RAINBOW","SOUL","MARSH","VOLCANO","EARTH"]
+
+def bar(cur, mx, width=10):
+ if mx <= 0: return " " * width
+ f = max(0, min(width, round(cur / mx * width)))
+ col = GR if cur > mx*0.5 else (YE if cur > mx*0.2 else RD)
+ return col + "█"*f + DIM + "░"*(width-f) + RESET
+
+ANSI = re.compile(r"\x1b\[[0-9;]*m")
+def vlen(x): return len(ANSI.sub("", x))
+
+W = 46
+def render(m, S, MAPS):
+ def s(name): return S.get(name, 0)
+ out = []
+ def row(txt):
+ pad = (W - 3) - vlen(txt)
+ out.append(f"{CY}│{RESET} " + txt + " "*max(0, pad) + f"{CY}│{RESET}")
+ def hr(title=""):
+ if title:
+ out.append(f"{CY}├─ {BOLD}{title}{RESET}{CY} " + "─"*(W-5-len(title)) + "┤"+RESET)
+ else:
+ out.append(CY+"├"+"─"*(W-2)+"┤"+RESET)
+ out.append(CY+"┌"+"─"*(W-2)+"┐"+RESET)
+ row(f"{BOLD}{MG}SL0P DEBUG HUD{RESET} {DIM}pokemon yellow{RESET}")
+
+ mapid = m.b(s("wCurMap"))
+ x, y = m.b(s("wXCoord")), m.b(s("wYCoord"))
+ facing = FACING.get(m.b(s("wSpritePlayerStateData1FacingDirection")), "?")
+ hr("LOCATION")
+ row(f"{WH}{MAPS.get(mapid,'?')}{RESET} {DIM}(0x{mapid:02X}){RESET} tileset {m.b(s('wCurMapTileset'))}")
+ row(f"pos {YE}x={x:<2} y={y:<2}{RESET} facing {YE}{facing}{RESET}")
+
+ hr("MOVEMENT / COLLISION")
+ walk = m.b(s("wWalkCounter")); joyig = m.b(s("wJoyIgnore"))
+ front = m.b(s("wTileInFrontOfPlayer")); standing = m.b(s("wTilePlayerStandingOn"))
+ ctl = GR+"YES"+RESET if joyig == 0 and walk == 0 else RD+"NO"+RESET
+ row(f"walkCounter={walk} joyIgnore=0x{joyig:02X} canMove={ctl}")
+ row(f"tile-standing={CY}0x{standing:02X}{RESET} tile-front={CY}0x{front:02X}{RESET}")
+
+ hr("PLAYER")
+ name = gb_decode(m.blocks[0xD158][0:11])
+ money = "".join(f"{m.b(0xD346+i):02X}" for i in range(3))
+ badges = m.b(s("wObtainedBadges"))
+ bset = [BADGES[i] for i in range(8) if badges & (1<<i)]
+ row(f"{WH}{name}{RESET} {DIM}id 0x{m.wbe(s('wPlayerID')):04X}{RESET} {YE}\u00a5{money}{RESET}")
+ row(f"badges {len(bset)}/8 {DIM}{','.join(bset) or '-'}{RESET}")
+
+ cnt = m.b(s("wPartyCount"))
+ hr(f"PARTY ({cnt})")
+ mon1 = s("wPartyMon1"); nicks = s("wPartyMonNicks")
+ for i in range(min(cnt, 6)):
+ base = mon1 + i*44
+ lvl = m.b(base+0x21); hp = m.wbe(base+1); mhp = m.wbe(base+0x22)
+ nick = gb_decode(m.blocks[0xD158][nicks-0xD158 + i*11 : nicks-0xD158 + i*11 + 11])
+ row(f"{WH}{nick:<10}{RESET} Lv{lvl:>3} {bar(hp,mhp)} {hp:>3}/{mhp:<3}")
+ out.append(CY+"└"+"─"*(W-2)+"┘"+RESET)
+ return "\n".join(out)
+
+def main():
+ argv = sys.argv[1:]
+ sock = None; sym = SYM_DEFAULT
+ if "--sock" in argv: sock = argv[argv.index("--sock")+1]
+ if "--sym" in argv: sym = argv[argv.index("--sym")+1]
+ sock = resolve_sock(sock)
+ if not sock: print("no live emulator (run gbctl spawn)"); return
+ S = load_syms(sym); MAPS = load_maps(MAPCONST)
+ sys.stdout.write("\x1b[?25l") # hide cursor
+ try:
+ m = None
+ while True:
+ try:
+ if m is None: m = Mem(sock)
+ m.refresh()
+ sys.stdout.write("\x1b[H\x1b[2J" + render(m, S, MAPS) + "\n")
+ sys.stdout.flush()
+ except (OSError, ConnectionError):
+ m = None
+ sys.stdout.write("\x1b[H\x1b[2J waiting for emulator...\n"); sys.stdout.flush()
+ time.sleep(0.2)
+ except KeyboardInterrupt:
+ pass
+ finally:
+ sys.stdout.write("\x1b[?25h\n")
+
+if __name__ == "__main__":
+ main()