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.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +22 -0
- package/README.md +5 -2
- package/examples/README.md +1 -0
- package/examples/pico8/templates/platformer.p8 +168 -0
- package/examples/pico8/templates/puzzle.p8 +240 -0
- package/examples/pico8/templates/racing.p8 +122 -0
- package/examples/pico8/templates/shmup.p8 +165 -0
- package/examples/pico8/templates/sports.p8 +140 -0
- package/package.json +7 -4
- package/src/cheats/gamegenie.js +101 -0
- package/src/cores/capabilities.js +28 -1
- package/src/cores/registry.js +5 -0
- package/src/host/LibretroHost.js +44 -1
- package/src/mcp/tools/cheats.js +4 -0
- package/src/mcp/tools/disasm.js +56 -2
- package/src/mcp/tools/project.js +27 -0
- package/src/toolchains/index.js +39 -1
- package/src/toolchains/pico8/pack.js +67 -0
|
@@ -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.
|
|
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.
|
|
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
|
}
|
package/src/cheats/gamegenie.js
CHANGED
|
@@ -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
|
package/src/cores/registry.js
CHANGED
|
@@ -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
|
package/src/host/LibretroHost.js
CHANGED
|
@@ -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
|
-
|
|
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). " +
|
package/src/mcp/tools/cheats.js
CHANGED
|
@@ -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
|