romdevtools 0.85.0 → 0.87.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,165 @@
1
+ pico-8 cartridge // http://www.pico-8.com
2
+ version 18
3
+ __lua__
4
+ -- star sweeper — a shmup scaffold
5
+ -- genre example for romdev/pico8. real sprites (see __gfx__), looping
6
+ -- music (__music__), sfx, title/play/gameover states, projectile pool,
7
+ -- wave spawner, aabb collision, score + hi-score, thruster animation.
8
+ -- fork it and reshape one thing at a time.
9
+
10
+ -- sprite ids in __gfx__:
11
+ -- 1 ship, 2 ship-thrust, 3 shot, 4 foe-a, 5 foe-b, 6 explosion
12
+
13
+ function _init()
14
+ hi=0
15
+ make_stars()
16
+ state="title"
17
+ boot=0
18
+ reset_game()
19
+
20
+ music(0)
21
+ end
22
+
23
+ function make_stars()
24
+ stars={}
25
+ for i=1,40 do add(stars,{x=rnd(128),y=rnd(128),s=rnd(2)+.4}) end
26
+ end
27
+
28
+ function reset_game()
29
+ player={x=60,y=100,cool=0,inv=0}
30
+ shots={}
31
+ foes={}
32
+ boom={}
33
+ score=0
34
+ lives=3
35
+ spawn=0
36
+ t=0
37
+ end
38
+
39
+ function _update()
40
+ t+=1
41
+ for s in all(stars) do s.y+=s.s if s.y>128 then s.y=0 s.x=rnd(128) end end
42
+
43
+ if state=="title" then
44
+ boot+=1
45
+ if boot>10 and (btnp(4) or btnp(5)) then state="play" reset_game() music(-1) end
46
+ return
47
+ end
48
+ if state=="over" then
49
+ if btnp(4) or btnp(5) then state="title" end
50
+ return
51
+ end
52
+
53
+ -- move
54
+ local mv=false
55
+ if btn(0) then player.x-=2 mv=true end
56
+ if btn(1) then player.x+=2 mv=true end
57
+ if btn(2) then player.y-=2 end
58
+ if btn(3) then player.y+=2 end
59
+ player.x=mid(4,player.x,116)
60
+ player.y=mid(4,player.y,120)
61
+ player.inv=max(0,player.inv-1)
62
+
63
+ -- fire
64
+ player.cool=max(0,player.cool-1)
65
+ if (btn(4) or btn(5)) and player.cool==0 then
66
+ add(shots,{x=player.x+3,y=player.y-2})
67
+ player.cool=7 sfx(3)
68
+ end
69
+ for s in all(shots) do s.y-=4 if s.y<-4 then del(shots,s) end end
70
+
71
+ -- spawn foes (ramping)
72
+ spawn-=1
73
+ if spawn<=0 then
74
+ add(foes,{x=rnd(104)+12,y=-8,vy=rnd(1)+1,vx=rnd(1)-.5,ty=flr(rnd(2))})
75
+ spawn=max(10,38-flr(t/90))
76
+ end
77
+ for f in all(foes) do
78
+ f.y+=f.vy f.x+=f.vx
79
+ if f.x<8 or f.x>116 then f.vx=-f.vx end
80
+ if f.y>134 then del(foes,f) end
81
+ if player.inv==0 and abs(f.x-player.x)<6 and abs(f.y-player.y)<6 then
82
+ del(foes,f) hurt()
83
+ end
84
+ end
85
+
86
+ -- shot vs foe
87
+ for s in all(shots) do
88
+ for f in all(foes) do
89
+ if abs(f.x-s.x)<5 and abs(f.y-s.y)<5 then
90
+ del(shots,s) del(foes,f)
91
+ add(boom,{x=f.x,y=f.y,f=0})
92
+ score+=10 sfx(4)
93
+ break
94
+ end
95
+ end
96
+ end
97
+ for b in all(boom) do b.f+=1 if b.f>8 then del(boom,b) end end
98
+ end
99
+
100
+ function hurt()
101
+ lives-=1 sfx(5)
102
+ add(boom,{x=player.x,y=player.y,f=0})
103
+ player.inv=60
104
+ if lives<=0 then
105
+ hi=max(hi,score) state="over"
106
+ end
107
+ end
108
+
109
+ function _draw()
110
+ cls(0)
111
+ for s in all(stars) do pset(s.x,s.y,s.s>1.6 and 7 or (s.s>1 and 6 or 5)) end
112
+
113
+ if state=="title" then
114
+ print("star sweeper",34,44,12)
115
+ print("★",26,44,10) print("★",96,44,10)
116
+ spr(1,60,62)
117
+ print("z/x start",40,84,7)
118
+ print("hi "..hi,50,98,6)
119
+ print("arrows move · z/x fire",18,116,5)
120
+ return
121
+ end
122
+
123
+ for f in all(foes) do spr(f.ty==0 and 4 or 5,f.x-4,f.y-4) end
124
+ for s in all(shots) do spr(3,s.x-4,s.y-4) end
125
+ for b in all(boom) do spr(6,b.x-4,b.y-4) end
126
+ -- ship (blink when invulnerable)
127
+ if not (player.inv>0 and player.inv%4<2) then
128
+ spr((t%8<4) and 2 or 1,player.x-4,player.y-4)
129
+ end
130
+
131
+ rectfill(0,0,127,7,1)
132
+ print("score "..score,2,1,7)
133
+ print("hi "..hi,54,1,6)
134
+ for i=1,lives do spr(1,96+i*8,0) end
135
+
136
+ if state=="over" then
137
+ rectfill(28,52,100,80,0) rect(28,52,100,80,8)
138
+ print("game over",44,56,8)
139
+ print("score "..score,42,66,7)
140
+ print("z/x title",40,74,6)
141
+ end
142
+ end
143
+
144
+ __gfx__
145
+ 00000000000c000000000c00000aa000000880000088880000000000000000000000000000000000000000000000000000000000000000000000000000000000
146
+ 0000000000ccc00000ccc0000aaaa000008888000899998000000000000000000000000000000000000000000000000000000000000000000000000000000000
147
+ 0070070000ccc00000ccc0000a00a000088888000988889000000000000000000000000000000000000000000000000000000000000000000000000000000000
148
+ 000770000cccccc00cc7cc00aa00aa008899988089aaaa9800000000000000000000000000000000000000000000000000000000000000000000000000000000
149
+ 000770000c7cc7c00c7cc7c0a0a00a0a0899998008aaaa8000000000000000000000000000000000000000000000000000000000000000000000000000000000
150
+ 0070070000c00c0000cccc00a000000a088888000899998000000000000000000000000000000000000000000000000000000000000000000000000000000000
151
+ 00000000000000000090900000a00a00008888000089980000000000000000000000000000000000000000000000000000000000000000000000000000000000
152
+ 00000000000000000090000000000000000880000008800000000000000000000000000000000000000000000000000000000000000000000000000000000000
153
+ __sfx__
154
+ 010c00000c6530e6530c6530000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
155
+ 010c00001805518055180551805518055180551305513055130551305511055110550e0550e0550e0550e0550c0550c0550c0550c055000000000000000000000000000000000000000000000000000000000000
156
+ 011c00002465524655216551d65500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
157
+ 000200003065528655236551e6551a655166551265510655000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
158
+ 00030000186550f6550c6550a65508655066550465502655006550065500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
159
+ 010500000c6530a653086530565303653016530000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
160
+ 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
161
+ 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
162
+ 0012000024250242502b2502b2502d2502d2502b2500000029250292502825028250262502625024250000002b2502b2502925029250282502825026250000002b2502b250292502925028250282502625000000
163
+ 00120000182300000018230000001d230000001d230000001a230000001a23000000182300000018230000001d230000001d230000001a230000001a230000001d230000001d2300000018230000001823000000
164
+ __music__
165
+ 03 08094040
@@ -0,0 +1,140 @@
1
+ pico-8 cartridge // http://www.pico-8.com
2
+ version 18
3
+ __lua__
4
+ -- rally volley — a 2-player paddle sports scaffold
5
+ -- genre example for romdev/pico8. paddle + ball sprites (see __gfx__),
6
+ -- looping music, bounce sfx, angle-off-paddle physics, speed-up on rally,
7
+ -- 1p-vs-cpu or 2p couch mode, first to 5. fork it.
8
+
9
+ function _init()
10
+ state="title"
11
+ boot=0
12
+ ai=true
13
+ reset_match()
14
+
15
+ music(0)
16
+ end
17
+
18
+ function reset_match()
19
+ lp={y=64,score=0} rp={y=64,score=0}
20
+ rally=0
21
+ serve(pick_dir())
22
+ end
23
+
24
+ function pick_dir() return (rnd(1)<.5) and -1 or 1 end
25
+
26
+ function serve(dir)
27
+ ball={x=64,y=64,dx=dir*1.4,dy=(rnd(2)-1)*1.1}
28
+ wait=36
29
+ end
30
+
31
+ function _update()
32
+ if state=="title" then
33
+ boot+=1
34
+ if btnp(2) or btnp(3) then ai=not ai sfx(3) end
35
+ if boot>10 and (btnp(4) or btnp(5)) then state="play" reset_match() music(-1) end
36
+ return
37
+ end
38
+ if state=="over" then
39
+ if btnp(4) or btnp(5) then state="title" end
40
+ return
41
+ end
42
+
43
+ if btn(2) then lp.y-=2 end
44
+ if btn(3) then lp.y+=2 end
45
+ lp.y=mid(12,lp.y,116)
46
+
47
+ if ai then
48
+ if ball.dx>0 then rp.y+=mid(-2,(ball.y-rp.y)*.11,2) end
49
+ else
50
+ if btn(4) then rp.y-=2 end
51
+ if btn(5) then rp.y+=2 end
52
+ end
53
+ rp.y=mid(12,rp.y,116)
54
+
55
+ if wait>0 then wait-=1 return end
56
+
57
+ ball.x+=ball.dx ball.y+=ball.dy
58
+ if ball.y<4 or ball.y>123 then ball.dy=-ball.dy sfx(0) end
59
+
60
+ if ball.dx<0 and ball.x<9 and abs(ball.y-lp.y)<11 then
61
+ ball.dx=abs(ball.dx)+.12 ball.dy+=(ball.y-lp.y)*.07 rally+=1 sfx(1)
62
+ end
63
+ if ball.dx>0 and ball.x>119 and abs(ball.y-rp.y)<11 then
64
+ ball.dx=-abs(ball.dx)-.12 ball.dy+=(ball.y-rp.y)*.07 rally+=1 sfx(1)
65
+ end
66
+ ball.dy=mid(-3,ball.dy,3)
67
+
68
+ if ball.x<0 then rp.score+=1 point() end
69
+ if ball.x>128 then lp.score+=1 point() end
70
+ end
71
+
72
+ function point()
73
+ sfx(2) rally=0
74
+ if lp.score>=5 or rp.score>=5 then
75
+ winner=lp.score>rp.score and "left" or "right"
76
+ state="over"
77
+ else
78
+ serve(pick_dir())
79
+ end
80
+ end
81
+
82
+ -- a tall paddle bar (10 tall, 3 wide) with a lighter core so it reads clearly
83
+ function paddle(x,y,col)
84
+ rectfill(x,y-9,x+2,y+9,col)
85
+ rectfill(x+1,y-8,x+1,y+8,7) -- white core stripe
86
+ end
87
+
88
+ function _draw()
89
+ cls(0)
90
+ -- dashed center net
91
+ for y=2,124,8 do rectfill(63,y,64,y+4,5) end
92
+
93
+ if state=="title" then
94
+ print("rally volley",38,36,12)
95
+ paddle(24,64,12) paddle(102,64,8)
96
+ rectfill(62,62,65,65,7) -- ball
97
+ print("mode: "..(ai and "1p vs cpu" or "2 player"),34,80,7)
98
+ print("up/down toggle",30,90,6)
99
+ print("z/x start",40,102,7)
100
+ return
101
+ end
102
+
103
+ paddle(4,lp.y,12) -- left paddle (blue, player 1)
104
+ paddle(121,rp.y,8) -- right paddle (red, cpu/p2)
105
+ rectfill(ball.x-1,ball.y-1,ball.x+1,ball.y+1,7) -- ball
106
+
107
+ rectfill(0,0,127,8,0)
108
+ print(lp.score,40,2,12)
109
+ print(rp.score,84,2,8)
110
+ if rally>2 then print("rally "..rally,52,2,10) end
111
+
112
+ if state=="over" then
113
+ rectfill(24,52,104,78,0) rect(24,52,104,78,11)
114
+ print(winner.." wins!",38,58,11)
115
+ print(lp.score.." - "..rp.score,52,68,7)
116
+ end
117
+ end
118
+
119
+ __gfx__
120
+ 0000000000cccc000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
121
+ 000000000cccccc0000cc00000cc0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
122
+ 000000000c7777c000cccc00cccc0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
123
+ 000000000c7777c000cccc00cccc0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
124
+ 000000000c7777c000cccc00cccc0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
125
+ 000000000cccccc000cccc00cccc0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
126
+ 0000000000cccc00000cc00000cc0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
127
+ 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
128
+ __sfx__
129
+ 010800001505015050000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
130
+ 010400001f05524055000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
131
+ 011000000c05508055050550000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
132
+ 010400001c0551c055000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
133
+ 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
134
+ 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
135
+ 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
136
+ 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
137
+ 00100000183501c3501d3501f35000000183501c3501d3501f35000000183501c3501d3501f3501c350183501c3501a350000001a3501c3501f3501f3501c3501f3501d3501c3501a35018350000000000000000
138
+ 00100000182300000000000000001f230000000000000000182300000000000000001f230000000000000000182300000000000000001d2300000000000000001f23000000000000000018230000000000000000
139
+ __music__
140
+ 03 08094040
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "romdevtools",
3
- "version": "0.85.0",
3
+ "version": "0.87.1",
4
4
  "description": "Tool server giving coding agents full control of homebrew ROM development AND reverse-engineering/romhacking across 17 retro platforms (NES, SNES, GB, Genesis, Atari, C64, PC Engine, MSX, PlayStation, N64, Dreamcast, ...) via WASM toolchains + emulator cores. Use over plain HTTP, as an Agent Skill, or as an MCP server.",
5
5
  "type": "module",
6
6
  "main": "src/mcp/server.js",
@@ -13,8 +13,7 @@
13
13
  "postinstall": "node src/install/postinstall.mjs",
14
14
  "smoke": "node --enable-source-maps src/cli/smoke.js",
15
15
  "mcp": "node --enable-source-maps src/mcp/server.js",
16
- "test": "node --test 'src/**/*.test.js' 'test/**/*.test.js'",
17
- "test:jsgame": "node --experimental-vm-modules --test-force-exit --test test/jsgame-host.test.js"
16
+ "test": "node --test 'src/**/*.test.js' 'test/**/*.test.js'"
18
17
  },
19
18
  "keywords": [
20
19
  "mcp",
@@ -56,10 +55,11 @@
56
55
  "romdev-audio-resampler": "0.1.0",
57
56
  "romdev-core-beetle-psx-hw": "0.2.0",
58
57
  "romdev-core-bluemsx": "0.7.0",
58
+ "romdev-core-fake08": "0.1.0",
59
59
  "romdev-core-fceumm": "0.11.0",
60
60
  "romdev-core-flycast": "0.2.0",
61
61
  "romdev-core-gambatte": "0.10.0",
62
- "romdev-core-gametank": "0.1.0",
62
+ "romdev-core-gametank": "0.2.0",
63
63
  "romdev-core-geargrafx": "0.8.0",
64
64
  "romdev-core-gpgx": "0.13.0",
65
65
  "romdev-core-handy": "0.8.0",
@@ -111,5 +111,8 @@
111
111
  "homepage": "https://github.com/monteslu/romdev#readme",
112
112
  "bugs": {
113
113
  "url": "https://github.com/monteslu/romdev/issues"
114
+ },
115
+ "devDependencies": {
116
+ "rawr": "^1.0.1"
114
117
  }
115
118
  }
@@ -277,6 +277,98 @@ export function decodeSnesGameGenie(code) {
277
277
  return { address, value };
278
278
  }
279
279
 
280
+ // ── GameTank Game Genie ─────────────────────────────────────────────────
281
+ // A NEW format for Clyde Shaffer's open GameTank console — no prior art exists
282
+ // (nobody has made GameTank cheat codes before). The GameTank CPU (W65C02S) sees a
283
+ // flat 16-bit address space, so a code encodes a 16-bit READ address + an 8-bit
284
+ // substitute value, plus an optional 8-bit compare byte (the compare form survives
285
+ // the cart's flash bank switching, exactly like NES/GB compare codes). The device
286
+ // intercepts the CPU's bus read and substitutes the value — identical behaviour to
287
+ // a hardware Game Genie you could build for the console's open cart bus.
288
+ //
289
+ // Encoding: a distinct 16-letter wheel (so codes read as GameTank, not NES). The
290
+ // payload nibbles are laid out and lightly scrambled so a small address change moves
291
+ // several letters (the GG "feel"), then a 1-nibble checksum guards typos.
292
+ // plain: 6 payload nibbles (addr16 + val8) + 1 checksum = 7 letters, shown "XXX-XXXX"
293
+ // compare: 8 payload nibbles (addr16 + val8 + cmp8) + 1 checksum = 9 letters, "XXXX-XXXXX"
294
+ const GT_GG_LETTERS = "KLMNPQRSTVWXYZ23"; // 16 distinct glyphs, no vowels/0-1-O-I ambiguity
295
+
296
+ function gtNibblesToLetters(nibs) {
297
+ return nibs.map((x) => GT_GG_LETTERS[x & 0xF]).join("");
298
+ }
299
+ function gtLettersToNibbles(code) {
300
+ const clean = code.replace(/-/g, "").toUpperCase();
301
+ const n = [];
302
+ for (const ch of clean) {
303
+ const v = GT_GG_LETTERS.indexOf(ch);
304
+ if (v < 0) return null;
305
+ n.push(v);
306
+ }
307
+ return n;
308
+ }
309
+ // Simple nibble checksum: XOR of all payload nibbles, folded to 4 bits.
310
+ function gtChecksum(payloadNibs) {
311
+ let c = 0;
312
+ for (const x of payloadNibs) c ^= x & 0xF;
313
+ return c & 0xF;
314
+ }
315
+ // Scramble/unscramble the payload-nibble ORDER (a fixed permutation) so a code
316
+ // doesn't read as plain hex. Self-inverse pairs keep decode trivial.
317
+ const GT_PERM6 = [3, 0, 5, 1, 4, 2]; // plain: 6 nibbles
318
+ const GT_PERM8 = [5, 2, 7, 0, 4, 1, 6, 3]; // compare: 8 nibbles
319
+ function applyPerm(arr, perm) { return perm.map((i) => arr[i]); }
320
+ function invertPerm(perm) {
321
+ const inv = new Array(perm.length);
322
+ perm.forEach((p, i) => { inv[p] = i; });
323
+ return inv;
324
+ }
325
+
326
+ /** ENCODE → GameTank Game Genie. `address` 0..0xFFFF, `value` 0..0xFF, optional
327
+ * `compare` 0..0xFF. Returns the dashed letter code, or null if out of range. */
328
+ export function encodeGameTankGameGenie({ address, value, compare }) {
329
+ if (address == null || value == null) return null;
330
+ const a = address & 0xFFFF, v = value & 0xFF;
331
+ if (address < 0 || address > 0xFFFF || value < 0 || value > 0xFF) return null;
332
+
333
+ // payload nibbles, MSB-first: addr[15:12],addr[11:8],addr[7:4],addr[3:0],val[7:4],val[3:0]
334
+ let payload = [(a >> 12) & 0xF, (a >> 8) & 0xF, (a >> 4) & 0xF, a & 0xF, (v >> 4) & 0xF, v & 0xF];
335
+ let perm = GT_PERM6;
336
+ if (compare != null) {
337
+ if (compare < 0 || compare > 0xFF) return null;
338
+ const c = compare & 0xFF;
339
+ payload = payload.concat([(c >> 4) & 0xF, c & 0xF]);
340
+ perm = GT_PERM8;
341
+ }
342
+ const scrambled = applyPerm(payload, perm);
343
+ const sum = gtChecksum(payload);
344
+ const nibs = scrambled.concat([sum]);
345
+ const letters = gtNibblesToLetters(nibs);
346
+ // dash after the first 3 (plain) / 4 (compare) letters for readability
347
+ const cut = compare != null ? 4 : 3;
348
+ return letters.slice(0, cut) + "-" + letters.slice(cut);
349
+ }
350
+
351
+ /** Decode a GameTank Game Genie code → { address, value, compare? } or null. */
352
+ export function decodeGameTankGameGenie(code) {
353
+ const n = gtLettersToNibbles(code);
354
+ if (!n) return null;
355
+ let perm, payloadLen;
356
+ if (n.length === 7) { perm = GT_PERM6; payloadLen = 6; }
357
+ else if (n.length === 9) { perm = GT_PERM8; payloadLen = 8; }
358
+ else return null;
359
+
360
+ const scrambled = n.slice(0, payloadLen);
361
+ const sum = n[payloadLen];
362
+ const payload = applyPerm(scrambled, invertPerm(perm));
363
+ if (gtChecksum(payload) !== sum) return null; // typo / not a valid GameTank code
364
+
365
+ const address = ((payload[0] << 12) | (payload[1] << 8) | (payload[2] << 4) | payload[3]) & 0xFFFF;
366
+ const value = ((payload[4] << 4) | payload[5]) & 0xFF;
367
+ if (payloadLen === 6) return { address, value };
368
+ const compare = ((payload[6] << 4) | payload[7]) & 0xFF;
369
+ return { address, value, compare };
370
+ }
371
+
280
372
  // ── Game Boy GameShark ──────────────────────────────────────────────────
281
373
  // 8 hex digits "TTVVAAAA": TT = type/RAM-bank byte, VV = replacement value,
282
374
  // AAAA = the RAM address in LITTLE-ENDIAN order. (Distinct from GB Game Genie,
@@ -340,6 +432,11 @@ export function detectDevice(code, platform) {
340
432
  if (hex8 || hyphenHex) return "action-replay"; // SMS/GG Action Replay
341
433
  if (/^[0-9A-F]{3}-[0-9A-F]{3}/i.test(c)) return "game-genie";
342
434
  return "unknown";
435
+ case "gametank":
436
+ // 7-letter (XXX-XXXX) or 9-letter (XXXX-XXXXX) GameTank wheel code.
437
+ if (/^[KLMNPQRSTVWXYZ23]{3}-[KLMNPQRSTVWXYZ23]{4}$/i.test(c) ||
438
+ /^[KLMNPQRSTVWXYZ23]{4}-[KLMNPQRSTVWXYZ23]{5}$/i.test(c)) return "game-genie";
439
+ return "unknown";
343
440
  default:
344
441
  return "unknown";
345
442
  }
@@ -370,6 +467,8 @@ export function decodeCode(code, platform) {
370
467
  case "gg":
371
468
  // SMS/GG Action Replay raw-hex (addr/val, no scramble).
372
469
  return decodeProActionReplay(c, platform);
470
+ case "gametank":
471
+ return decodeGameTankGameGenie(c);
373
472
  default:
374
473
  return null;
375
474
  }
@@ -476,6 +575,7 @@ export function nativeDevicesFor(platform) {
476
575
  case "snes": return ["pro-action-replay", "game-genie"];
477
576
  case "gb": case "gbc": return ["game-genie", "gameshark"];
478
577
  case "sms": case "gg": return ["action-replay"];
578
+ case "gametank": return ["game-genie"];
479
579
  default: return ["raw"];
480
580
  }
481
581
  }
@@ -494,6 +594,7 @@ export function encodeForDevice({ address, value, compare }, platform, device) {
494
594
  else if (["genesis", "megadrive", "md"].includes(platform)) code = encodeGenesisGameGenie({ address, value });
495
595
  else if (["gb", "gbc"].includes(platform)) code = encodeGbGameGenie({ address, value, compare });
496
596
  else if (platform === "snes") code = encodeSnesGameGenie({ address, value });
597
+ else if (platform === "gametank") code = encodeGameTankGameGenie({ address, value, compare });
497
598
  return code ? { code, device: "game-genie" } : null;
498
599
  }
499
600
  case "pro-action-replay": {
@@ -198,6 +198,33 @@ export const CAPABILITIES = {
198
198
  cart: true, disasm: true, decompile: true,
199
199
  },
200
200
  },
201
+ pico8: {
202
+ // FAKE-08 (MIT) runs PICO-8 carts. PICO-8 is a Lua VM, not a real CPU — so there's
203
+ // no machine code to disassemble/decompile and no CPU register file (cpuState N/A).
204
+ // But: build=PACKAGE a .p8 cart (Lua + gfx/sfx/map sections); run/screenshot work;
205
+ // memory works — the romdev patch exposes the full 64KB PICO-8 address space as
206
+ // system_ram (sprite sheet, map, flags, music, sfx, general RAM, screen buffer).
207
+ // disasm here is target:'source' — the cart IS Lua source, so we return the Lua
208
+ // itself (the honest "understand this cart" path), not a machine-code listing.
209
+ // It renders to a 128×128 framebuffer (poke to screen memory), so the inspect-*
210
+ // tile/sprite-table tools are N/A like gametank/the disc platforms.
211
+ // tier:"fantasy" — a fantasy console (Lua VM), not a CPU emulator. Held to its OWN
212
+ // conformance, NOT the canonical-14 cross-checks (which assume CPU disasm/decompile/
213
+ // cpuState/tile inspectors every real console has). Excluded from CONTRACT_PLATFORMS
214
+ // via NEXTGEN_TIER_PLATFORMS.
215
+ tier: "fantasy",
216
+ cpuFamily: "lua", decompileQuality: "n/a",
217
+ cpus: { main: "", secondary: [] }, // Lua VM — no CPU register file
218
+ audioChips: [], // audioDebug not wired (PICO-8's synth isn't a fixed-register chip we decode)
219
+ memoryRegions: [...GENERIC_REGIONS],
220
+ renderingKind: "framebuffer", introspection: "shallow",
221
+ ops: {
222
+ build: true, run: true, screenshot: true,
223
+ inspectSprites: false, inspectPalette: false, inspectBackground: false,
224
+ renderingContext: false, cpuState: false, audioDebug: false,
225
+ cart: false, disasm: true, decompile: false,
226
+ },
227
+ },
201
228
  pce: {
202
229
  cpuFamily: "huc6280", decompileQuality: "medium",
203
230
  cpus: { main: "", secondary: [] }, // getCPUState main NOT wired for pce
@@ -316,7 +343,7 @@ export const MIPS_TIER_PLATFORMS = Object.entries(CAPABILITIES)
316
343
  * is held to its OWN conformance, not the "all 14" cross-checks; a new platform
317
344
  * starts here (analysis-first) and graduates as run/build/etc. land. */
318
345
  export const NEXTGEN_TIER_PLATFORMS = Object.entries(CAPABILITIES)
319
- .filter(([, c]) => c.tier === "mips" || c.tier === "sh")
346
+ .filter(([, c]) => c.tier === "mips" || c.tier === "sh" || c.tier === "fantasy")
320
347
  .map(([p]) => p);
321
348
 
322
349
  /** Back-compat: the analysis-only set is now empty (PS1/N64 gained run/screenshot
@@ -54,6 +54,11 @@ export const CORES = {
54
54
  pce: { platform: "pce", coreName: "geargrafx", pkg: "romdev-core-geargrafx", displayName: "PC Engine / TurboGrafx-16 (Geargrafx)", aka: "turbografx,tg16,pcengine" },
55
55
  msx: { platform: "msx", coreName: "bluemsx", pkg: "romdev-core-bluemsx", displayName: "MSX / MSX2 (blueMSX)", aka: "msx2" },
56
56
  gametank: { platform: "gametank", coreName: "gametank", pkg: "romdev-core-gametank", displayName: "GameTank (Clyde Shaffer)", aka: "gtr" },
57
+ // FAKE-08 = open-source (MIT) PICO-8 player, NOT Lexaloffle's PICO-8, no BIOS. Runs
58
+ // .p8 (Lua source) + .p8.png carts at 128×128. It's a Lua VM, not a real CPU — the
59
+ // capability descriptor marks cpuState/disasm/memory-region tools not-applicable; the
60
+ // .p8 Lua source IS the readable "disassembly".
61
+ pico8: { platform: "pico8", coreName: "fake08", pkg: "romdev-core-fake08", displayName: "PICO-8 (FAKE-08)", aka: "p8,fake08" },
57
62
  // 32-bit MIPS tier. These cores HW-render (GL): the host lazy-loads the OPTIONAL
58
63
  // webgl-node bridge only when one of these boots (hwRender:true). The other 14 are
59
64
  // software-rendered and never touch GL, so a headless user without the GPU module
@@ -212,8 +212,14 @@ export const PLATFORM_VIRTUAL_EXT = {
212
212
  atari7800: ".a78",
213
213
  pce: ".pce",
214
214
  msx: ".rom",
215
+ pico8: ".p8",
215
216
  };
216
217
  import { RETRO_DEVICE_JOYPAD, ROMDEV_PIXEL_FORMAT_RGBA8888 } from "./retroConstants.js";
218
+ import { decodeCode as decodeCheatCode } from "../cheats/gamegenie.js";
219
+
220
+ // Platforms whose core stubs retro_cheat_set but ships romdev's value-override cheat
221
+ // device (romdev_cheat_set) — cheats route through that read-substitution instead.
222
+ const CHEAT_PREFER_ROMDEV_DEVICE = new Set(["gametank"]);
217
223
 
218
224
  // C64 controller→keyboard map (the Batocera/RetroDeck model: a CONTROLLER alone
219
225
  // plays C64 — no physical keyboard needed — by mapping spare buttons/stick to
@@ -1536,7 +1542,18 @@ export class LibretroHost {
1536
1542
  * (Older bundled cores predate the cheat-export build flag.) */
1537
1543
  cheatsSupported() {
1538
1544
  const mod = this.mod;
1539
- return !!(mod && typeof mod._retro_cheat_set === "function");
1545
+ // Real cheat support = EITHER the core's retro_cheat_set actually applies codes,
1546
+ // OR romdev's value-override cheat device (romdev_cheat_set) is present. Some cores
1547
+ // (GameTank) stub retro_cheat_set but ship the romdev_cheat_* read-substitution
1548
+ // device — that's the working path for them.
1549
+ return !!(mod && (typeof mod._romdev_cheat_set === "function" || typeof mod._retro_cheat_set === "function"));
1550
+ }
1551
+
1552
+ /** True if this core applies cheats via romdev's value-override device (romdev_cheat_set)
1553
+ * rather than the libretro retro_cheat_set (which is a stub on some cores). */
1554
+ _usesRomdevCheatDevice() {
1555
+ return !!(this.mod && typeof this.mod._romdev_cheat_set === "function" &&
1556
+ CHEAT_PREFER_ROMDEV_DEVICE.has(this.status.platform));
1540
1557
  }
1541
1558
 
1542
1559
  /**
@@ -1553,6 +1570,32 @@ export class LibretroHost {
1553
1570
  */
1554
1571
  setCheat(index, code, enabled = true) {
1555
1572
  const mod = this._needMod();
1573
+
1574
+ // romdev value-override device (GameTank etc.): the core's retro_cheat_set is a
1575
+ // stub, but romdev_cheat_set installs a hardware-faithful read substitution. Decode
1576
+ // the code to {address,value,compare} and hand it to the device.
1577
+ if (this._usesRomdevCheatDevice()) {
1578
+ const decoded = decodeCheatCode(String(code), this.status.platform);
1579
+ if (!decoded || decoded.address == null || decoded.value == null) {
1580
+ throw new Error(
1581
+ `setCheat: could not decode '${code}' for ${this.status.platform}. ` +
1582
+ `Provide a GameTank Game Genie code or a raw ADDR:VAL[:COMPARE].`,
1583
+ );
1584
+ }
1585
+ mod._romdev_cheat_set(
1586
+ index >>> 0,
1587
+ decoded.address & 0xFFFF,
1588
+ decoded.value & 0xFF,
1589
+ (decoded.compare ?? 0) & 0xFF,
1590
+ decoded.compare != null ? 1 : 0,
1591
+ enabled ? 1 : 0,
1592
+ );
1593
+ if (!this._activeCheats) this._activeCheats = new Map();
1594
+ if (enabled) this._activeCheats.set(index, code);
1595
+ else this._activeCheats.delete(index);
1596
+ return;
1597
+ }
1598
+
1556
1599
  if (typeof mod._retro_cheat_set !== "function") {
1557
1600
  throw new Error(
1558
1601
  "this core build does not expose the cheat interface (retro_cheat_set). " +
@@ -18,6 +18,9 @@ const GG_ADDR_RANGE = {
18
18
  snes: [0x008000, 0xFFFFFF], // mapped ROM ($xx:8000-$xx:FFFF); the Game Genie
19
19
  // ROM device patches here. (Pro Action Replay is
20
20
  // a RAM poke — for a ROM patch we pick GG below.)
21
+ gametank: [0x0000, 0xFFFF], // flat 16-bit CPU space. GameTank's Game Genie is a
22
+ // READ substitution on the bus, so any read address is
23
+ // valid (RAM or cart ROM). A NEW format — see gamegenie.js.
21
24
  };
22
25
 
23
26
  // The native ROM-PATCH device per platform (installs a read-intercept), as
@@ -94,6 +97,7 @@ const SUPPORTED = new Set([
94
97
  const MAKE_CHEAT_PLATFORMS = [
95
98
  "nes", "gb", "gbc", "snes", "genesis", "sms", "gg",
96
99
  "atari2600", "atari7800", "lynx", "gba", "c64", "pce", "msx",
100
+ "gametank", // NEW: GameTank Game Genie (read-substitution device) — see gamegenie.js
97
101
  ];
98
102
 
99
103
  // gameCheats indexes whose codes are predominantly ENCRYPTED at the source