romdevtools 0.86.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/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 +2 -1
- package/src/cores/capabilities.js +28 -1
- package/src/cores/registry.js +5 -0
- package/src/host/LibretroHost.js +1 -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
package/AGENTS.md
CHANGED
|
@@ -4,7 +4,7 @@ This is romdev's GENERIC orientation — read it once. The platform-specific det
|
|
|
4
4
|
|
|
5
5
|
## What this server does
|
|
6
6
|
|
|
7
|
-
Drives the full homebrew ROM dev loop for 18 retro game platforms (NES, SNES, Game Boy, Game Boy Color, Game Boy Advance, Genesis, Sega Master System, Game Gear, Atari 2600/7800, Atari Lynx, Commodore 64, PC Engine / TurboGrafx-16, and MSX / MSX2, plus the open-hardware GameTank — and the 3D consoles Nintendo 64, Sony PlayStation, and Sega Dreamcast). Build → run → screenshot → inspect → patch → iterate. Also a strong reverse-engineering kit: disassemble existing ROMs into byte-exact rebuildable projects (`disasm({target:'project'})`/`disasm({target:'references'})` — the workhorse for any structural hack), find a value's address with the Cheat-Engine search loop (`memory({op:'search'})`/`memory({op:'searchNext'})`), find the EXACT instruction that wrote a RAM byte (`breakpoint({on:'write'})`, a core-level write watchpoint), confirm a patch is live in the running image (`memory({op:'readCart'})`), tell whether a "found table" is really ASCII (`memory({op:'classify'})`), trace where an on-screen graphic comes from (`watch({on:'copy'})` on the 15 classic platforms — writer PC per VRAM write; `watch({on:'dma'})` for Genesis DMA sources), drive menus by screen-change (`navigate`), and look up cheats (`cheats({op:'lookup'})`/`cheats({op:'search'})`: a free, crowd-sourced labeled RAM/code map for known ROMs), apply + create cheats, convert assets, study patterns from real games. **Doing a romhack? Start with `platform({op:'doc', platform:'romhacking', name:'playbook'})`** — the decision tree that wires all of the above together. Bundled WASM toolchains and emulator cores — no system dependencies, no installs.
|
|
7
|
+
Drives the full homebrew ROM dev loop for 18 retro game platforms (NES, SNES, Game Boy, Game Boy Color, Game Boy Advance, Genesis, Sega Master System, Game Gear, Atari 2600/7800, Atari Lynx, Commodore 64, PC Engine / TurboGrafx-16, and MSX / MSX2, plus the open-hardware GameTank — and the 3D consoles Nintendo 64, Sony PlayStation, and Sega Dreamcast) — plus the **PICO-8** fantasy console (via FAKE-08: a Lua VM, so its own tier — `build` PACKAGES a `.p8` from Lua, `disasm({target:'source'})` reads the cart's Lua instead of machine code, `memory({region:'system_ram'})` reads its 64 KB; cpuState/decompile/tile-inspectors are N/A). Build → run → screenshot → inspect → patch → iterate. Also a strong reverse-engineering kit: disassemble existing ROMs into byte-exact rebuildable projects (`disasm({target:'project'})`/`disasm({target:'references'})` — the workhorse for any structural hack), find a value's address with the Cheat-Engine search loop (`memory({op:'search'})`/`memory({op:'searchNext'})`), find the EXACT instruction that wrote a RAM byte (`breakpoint({on:'write'})`, a core-level write watchpoint), confirm a patch is live in the running image (`memory({op:'readCart'})`), tell whether a "found table" is really ASCII (`memory({op:'classify'})`), trace where an on-screen graphic comes from (`watch({on:'copy'})` on the 15 classic platforms — writer PC per VRAM write; `watch({on:'dma'})` for Genesis DMA sources), drive menus by screen-change (`navigate`), and look up cheats (`cheats({op:'lookup'})`/`cheats({op:'search'})`: a free, crowd-sourced labeled RAM/code map for known ROMs), apply + create cheats, convert assets, study patterns from real games. **Doing a romhack? Start with `platform({op:'doc', platform:'romhacking', name:'playbook'})`** — the decision tree that wires all of the above together. Bundled WASM toolchains and emulator cores — no system dependencies, no installs.
|
|
8
8
|
|
|
9
9
|
You drive the work. The human is a director — they may want a game, a ROM disassembly, a tool-assisted reverse-engineering session, or anything else this server can do.
|
|
10
10
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# romdev
|
|
2
2
|
|
|
3
|
-
The entry point for **romdev** — vibe-code real retro games. Build, run, inspect, and reverse-engineer actual homebrew ROMs (NES, SNES, Game Boy, Genesis, Atari, C64, GBA, PC Engine, MSX, GameTank — and the 3D consoles N64, PlayStation, and Dreamcast) with one command — drive it yourself or let a coding assistant do it.
|
|
3
|
+
The entry point for **romdev** — vibe-code real retro games. Build, run, inspect, and reverse-engineer actual homebrew ROMs (NES, SNES, Game Boy, Genesis, Atari, C64, GBA, PC Engine, MSX, GameTank — and the 3D consoles N64, PlayStation, and Dreamcast), plus the **PICO-8** fantasy console (via FAKE-08), with one command — drive it yourself or let a coding assistant do it.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npx romdevtools
|
|
@@ -13,6 +13,8 @@ npx romdevtools
|
|
|
13
13
|
- **Inspect + romhack** — read CPU/video/save RAM, watch memory, write-breakpoints, the Cheat-Engine value-search loop, a bundled cheat database, mapper-aware disassembly, and a byte-exact rebuildable-project disassembler.
|
|
14
14
|
- **Reverse-engineering analysis engine (all 18 platforms — incl. the 3D consoles' MIPS R3000/R4300 + SH-4)** — control-flow graphs, deep cross-references, auto-detected functions (ranked real-code-first), a one-shot structural map, and a Ghidra **decompiler** (C-like pseudocode, with hardware registers named and 6502 SLEIGH clutter folded to readable C): `disasm({target:'cfg'|'xrefs'|'functions'|'decompile'})` and `symbols({op:'analyze'})`. And the piece no static tool has: **live computed-jumptable recovery** — `breakpoint({on:'jumptable'})` runs the emulator to resolve the `JMP (table,X)` / RTS-trick dispatchers (state machines, script/battle VMs) that static analysis collapses to "could not recover." Understand *how* a routine works before you touch it — no $3,000 IDA license, no install.
|
|
15
15
|
- **Convert assets** — PNG → platform tiles/tilemaps, quantize-to-palette, audio importers (BRR for SNES, XGM2 PCM for Genesis).
|
|
16
|
+
- **Native game runtimes (beyond emulation)** — the same run/see/drive loop also hosts two *native* game formats: **wasmcart** (`.wasc` — WASM games from any language; 2026) and **jsgame** (`.jsgame` — JavaScript canvas/WebGL games; 2024). `loadMedia({platform:'wasmcart'|'jsgame'})` → `frame({op:'step'|'screenshot'})` → `input`, over the same tools — plus V8/WASM introspection (the cart's live WASM heap + exports; the JS game's globals) an emulator can't offer, and `pack({target:'wasc'|'jsgame'})` to zip a source dir into the distributable archive (the "build" step; romdev doesn't compile the WASM — bring your own). A capability descriptor marks the emulator-only tools (memory regions / cpuState / disasm) *not-applicable* for these kinds. Depends on the [`wasmcart`](https://www.npmjs.com/package/wasmcart) + [`rungame`](https://www.npmjs.com/package/rungame) packages (jsgame runs in a `vm` realm — the server self-re-execs with `--experimental-vm-modules`).
|
|
17
|
+
- **PICO-8 (fantasy console)** — the [FAKE-08](https://github.com/jtothebell/fake-08) player (MIT, no BIOS) runs PICO-8 `.p8` (Lua source) and `.p8.png` (cart-in-a-label-PNG) carts at 128×128 with sound. `loadMedia({platform:'pico8'})` → `frame`/`input` work like any core; `build({platform:'pico8', source: lua})` PACKAGES a runnable `.p8` from Lua (+ optional gfx/sfx/map sections) — it's a cart assembler, not a CPU compiler (the Lua IS the code). PICO-8 is a Lua VM, so instead of machine-code disasm/decompile you read the cart's Lua directly with `disasm({target:'source'})`; `memory({region:'system_ram'})` exposes the full 64KB PICO-8 address space (sprite sheet, map, sfx, general RAM, screen buffer). Its capability descriptor is a `fantasy` tier — cpuState/decompile/tile-inspectors report *not-applicable* (there's no CPU or tile hardware to inspect). Ships the [`romdev-core-fake08`](https://www.npmjs.com/package/romdev-core-fake08) core package.
|
|
16
18
|
|
|
17
19
|
Point any coding agent at it three ways:
|
|
18
20
|
|
|
@@ -36,13 +38,14 @@ This package contains all the JavaScript — the tool surface, the WASM emulator
|
|
|
36
38
|
|
|
37
39
|
`romdevtools` depends on the binary/data packages it needs (exact-pinned), so a single install gets a matched, tested set:
|
|
38
40
|
|
|
39
|
-
- 2D cores: `romdev-core-{fceumm,gambatte,gpgx,vice,handy,prosystem,geargrafx,bluemsx,gametank}`
|
|
41
|
+
- 2D cores: `romdev-core-{fceumm,gambatte,gpgx,vice,handy,prosystem,geargrafx,bluemsx,gametank,fake08}`
|
|
40
42
|
- 3D / GPU cores (rendered through `native-gles`): `romdev-core-{parallel-n64,beetle-psx-hw,flycast}`
|
|
41
43
|
- Platforms (core + dedicated toolchain bundled): `romdev-platform-{snes,gba,atari2600}`
|
|
42
44
|
- Toolchains: `romdev-toolchain-{cc65,sdcc,m68k-gcc,vasm,rgbds,mips-gcc,sh-gcc}` (mips-gcc = N64/PS1 C; sh-gcc = Dreamcast C)
|
|
43
45
|
- GPU deps (OPTIONAL — only the 3D cores need them; the 2D cores never touch GL): `native-gles` + `webgl-node`. A headless user without a GPU stack can still run all the 2D platforms.
|
|
44
46
|
- Analysis: `romdev-analysis` (Rizin → WASM: control-flow graphs, cross-references, function detection) and `romdev-analysis-decompiler` (Ghidra's C++ decompiler → WASM + SLEIGH processor specs for all 18 CPUs, incl. MIPS R3000/R4300 + SH-4). Power `disasm({target:'cfg'|'xrefs'|'functions'|'decompile'})` and `symbols({op:'analyze'})`. Lazy-loaded on first use.
|
|
45
47
|
- Data: `romdev_game_codes` — the bundled game-code / cheat database (a free labeled RAM/code map for thousands of known ROMs), split out so it can grow independently. Lazy-loaded one platform at a time.
|
|
48
|
+
- Native runtimes: [`wasmcart`](https://www.npmjs.com/package/wasmcart) (runs `.wasc` WASM game carts) and [`rungame`](https://www.npmjs.com/package/rungame) (runs `.jsgame` JavaScript games headlessly). Also `romdev-audio-resampler` (WASM+SIMD S16-stereo resampler, used by the live-window audio sink).
|
|
46
49
|
|
|
47
50
|
`@kmamal/sdl` is used only by `playtest()` / `romdevtools-cli play` (the live window). It ships its native binary via its own install script, which npm skips when romdev is a transitive dep (e.g. under `npx`) — so romdev's `postinstall` fetches it, and `playtest()` also self-heals at runtime if the binary is still missing (downloading the prebuilt before the first window open). Either way, if the binary can't be fetched (offline/locked-down network), the headless server is unaffected — only the live window degrades, and the error tells you the one command to fix it.
|
|
48
51
|
|
package/examples/README.md
CHANGED
|
@@ -27,6 +27,7 @@ Each example fits the convention:
|
|
|
27
27
|
| pce | `pce/<template>/main.c` | cc65 (HuC6280) | HuCard homebrew, no BIOS. Ships a direct-register VDC/PSG helper lib (`pce.h` + `pce.lib`) — cc65 has no PCE sprite/sound library. Examples: `sprite_move`, `catch_game`, `music_sfx`, plus the 5 genre games (shmup/platformer/puzzle/sports/racing). **`#include <stdint.h>`** for int8/16/32_t — `pce.h` only typedefs u8/u16. The genre games fill the BAT (32×32 virtual screen); the platformer smooth-scrolls via the VDC BXR register. |
|
|
28
28
|
| msx | `msx/<template>/main.c` | sdcc (z80) | Boots cartridge homebrew on the open C-BIOS (no proprietary ROM). Ships an AY-3-8910 + TMS9918/V9938 VDP helper lib (`msx_hw.h` + `msx_vdp.c`). Examples: `sprite_move`, `catch_game`, `music_sfx`, plus the 5 genre games. The bundled `msx_crt0.s` (applied by the dir-build recipe automatically) emits the `"AB"` cartridge header at $4000 + INIT pointer — **C-BIOS shows its logo for ~2-3 s, then CALLs INIT**, so run ≥240 frames before screenshotting. The platformer column-streams the SCREEN 2 name table for a tile-by-tile scroll. |
|
|
29
29
|
| gametank | `gametank/templates/*.c` | cc65 (W65C02S) | Build with `linkerConfig:"sdk"` + the bundled SDK runtime (`src/platforms/gametank/lib/gt/*`). FIVE complete genre games, fully reworked and play-tested on real hardware (RetroDeck): `shmup`, `platformer`, `puzzle` (CHROMA WELL — a Columns-style 3-jewel matcher), `sports` (2-player paddle), `racing`. The GameTank is a **framebuffer + blitter** machine — NO tilemap/nametable: every frame is redrawn from scratch via the SDK **draw QUEUE** (`gt_draw.h`), the only correct way to drive the blitter (synchronous blits corrupt the FB + tank the framerate). Shared helper headers the games fork: `gt_draw.h` (queue wrappers + `rnd8`), `gt_palette.h` (VERIFIED color indices), `gt_sprites.h` (GRAM sprite load/blit), `gt_hud.h` (box-font digits + 3×5 A–Z text), `gt_sound.h` (one-shot ACP SFX). **HARD-WON GOTCHAS (all documented in `gt_draw.h`): (1) a box of width OR height 128 is silently DROPPED — clamp to 127; (2) box top-edge scanlines flicker between the double-buffer pages — finish a frame that draws near the top with `queue_clear_border(topColor)`; (3) use `rnd8()`, NOT the SDK `rnd()`, which corrupts state on this single-bank build.** Per-frame GRAM **sprite** blits are the expensive part — too many overrun vblank and the queue drops draws (flicker), so keep sprites few (e.g. the platformer blits only the hero; coins are rects). |
|
|
30
|
+
| pico8 | `pico8/templates/*.p8` | Lua (FAKE-08) | FIVE complete genre games, each a full `.p8` cart (no compiler/crt0 — the Lua IS the code): `shmup` (STAR SWEEPER), `platformer` (HOP QUEST), `puzzle` (COLOR DROP), `sports` (RALLY VOLLEY, 1P-vs-CPU or 2P), `racing` (LANE RUNNER). Each carries hand-authored `__gfx__` pixel-art sprites (drawn with `spr()`), looping `__music__` + `__sfx__` banks, title/play/game-over states, and the genre's core mechanics. **`build({platform:'pico8', source: <the .p8 text>})` just PACKAGES the cart** (a `.p8` = header + `__lua__` + optional `__gfx__`/`__gff__`/`__map__`/`__sfx__`/`__music__` sections). PICO-8 is a Lua VM (128×128, 16-color, 6 buttons) — read/edit the Lua directly, no disassembly needed. |
|
|
30
31
|
|
|
31
32
|
## Guides
|
|
32
33
|
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
pico-8 cartridge // http://www.pico-8.com
|
|
2
|
+
version 18
|
|
3
|
+
__lua__
|
|
4
|
+
-- hop quest — a platformer scaffold
|
|
5
|
+
-- genre example for romdev/pico8. real hero sprite w/ walk + jump frames
|
|
6
|
+
-- (see __gfx__), looping music, sfx, gravity + solid-box collision,
|
|
7
|
+
-- animated spinning coins, a goal flag, parallax hills. fork it.
|
|
8
|
+
|
|
9
|
+
-- sprites: 1 idle,2 walk,3 jump · 5..8 coin spin frames · 10 flag
|
|
10
|
+
|
|
11
|
+
function _init()
|
|
12
|
+
best=99
|
|
13
|
+
state="title"
|
|
14
|
+
boot=0
|
|
15
|
+
build_level()
|
|
16
|
+
reset_player()
|
|
17
|
+
music(0)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
function build_level()
|
|
21
|
+
solids={
|
|
22
|
+
{0,120,128,8},{24,100,24,6},{60,84,30,6},
|
|
23
|
+
{98,64,24,6},{6,58,18,6},{40,42,28,6},
|
|
24
|
+
}
|
|
25
|
+
coins={}
|
|
26
|
+
for c in all({{34,90},{72,74},{108,54},{14,48},{50,32}}) do
|
|
27
|
+
add(coins,{x=c[1],y=c[2],f=rnd(4)})
|
|
28
|
+
end
|
|
29
|
+
goal={x=52,y=24}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
function reset_player()
|
|
33
|
+
p={x=6,y=104,dx=0,dy=0,grounded=false,face=1,anim=0}
|
|
34
|
+
collected=0 total=#coins t=0
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
function solid_at(x,y)
|
|
38
|
+
for s in all(solids) do
|
|
39
|
+
if x>=s[1] and x<s[1]+s[3] and y>=s[2] and y<s[2]+s[4] then return true end
|
|
40
|
+
end
|
|
41
|
+
return false
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
-- the top Y of a platform the feet CROSS this step: feet were at/above the top (oldy)
|
|
45
|
+
-- and reach at/below it (newy). returns the highest such top, or nil. this catch-the-
|
|
46
|
+
-- crossing test is what stops fast falls from tunneling straight through a platform.
|
|
47
|
+
function ground_top(x,oldy,newy)
|
|
48
|
+
local best=nil
|
|
49
|
+
for s in all(solids) do
|
|
50
|
+
local top=s[2]
|
|
51
|
+
if x>=s[1] and x<s[1]+s[3] and oldy<=top+1 and newy>=top then
|
|
52
|
+
if best==nil or top<best then best=top end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
return best
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
function _update()
|
|
59
|
+
t+=1
|
|
60
|
+
for c in all(coins) do c.f+=.2 end
|
|
61
|
+
if state=="title" then
|
|
62
|
+
boot+=1
|
|
63
|
+
if boot>10 and (btnp(4) or btnp(5)) then state="play" build_level() reset_player() music(-1) end
|
|
64
|
+
return
|
|
65
|
+
end
|
|
66
|
+
if state=="win" then
|
|
67
|
+
if btnp(4) or btnp(5) then state="title" end
|
|
68
|
+
return
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
local ax=0
|
|
72
|
+
if btn(0) then ax=-1 p.face=-1 end
|
|
73
|
+
if btn(1) then ax=1 p.face=1 end
|
|
74
|
+
p.dx=ax*1.6
|
|
75
|
+
if ax~=0 then p.anim+=.25 else p.anim=0 end
|
|
76
|
+
|
|
77
|
+
if (btn(4) or btn(2)) and p.grounded then p.dy=-4.2 p.grounded=false sfx(0) end
|
|
78
|
+
p.dy=min(p.dy+.3,4)
|
|
79
|
+
|
|
80
|
+
-- horizontal move (block at body height)
|
|
81
|
+
local nx=p.x+p.dx
|
|
82
|
+
if not solid_at(nx+(p.dx>0 and 3 or -3),p.y-3) then p.x=nx end
|
|
83
|
+
|
|
84
|
+
-- vertical move. p.y = the FEET. land exactly on a platform's top edge.
|
|
85
|
+
local ny=p.y+p.dy
|
|
86
|
+
if p.dy>=0 then
|
|
87
|
+
-- falling: if the feet CROSS a platform top this step, snap onto it (no tunneling).
|
|
88
|
+
local gt=ground_top(p.x,p.y,ny)
|
|
89
|
+
if gt~=nil then
|
|
90
|
+
p.y=gt p.dy=0 p.grounded=true
|
|
91
|
+
else
|
|
92
|
+
p.y=ny p.grounded=false
|
|
93
|
+
end
|
|
94
|
+
else
|
|
95
|
+
-- rising: bonk head on a ceiling
|
|
96
|
+
if solid_at(p.x,ny-7) then p.dy=0 else p.y=ny end
|
|
97
|
+
p.grounded=false
|
|
98
|
+
end
|
|
99
|
+
p.x=mid(4,p.x,124)
|
|
100
|
+
if p.y>140 then reset_player() sfx(3) end
|
|
101
|
+
|
|
102
|
+
for c in all(coins) do
|
|
103
|
+
if abs(c.x-p.x)<6 and abs(c.y-p.y)<7 then del(coins,c) collected+=1 sfx(1) end
|
|
104
|
+
end
|
|
105
|
+
if abs(goal.x-p.x)<7 and abs(goal.y-p.y)<9 then
|
|
106
|
+
best=min(best,t\30) state="win" sfx(2) music(-1)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
function _draw()
|
|
111
|
+
cls(12)
|
|
112
|
+
-- parallax hills
|
|
113
|
+
for i=0,4 do circfill(i*34-8,126,22,3) end
|
|
114
|
+
for i=0,3 do circfill(i*44+20,128,26,11) end
|
|
115
|
+
|
|
116
|
+
if state=="title" then
|
|
117
|
+
print("hop quest",44,44,7)
|
|
118
|
+
spr(1,60,60)
|
|
119
|
+
print("z/up jump",40,80,6)
|
|
120
|
+
print("z/x start",40,92,7)
|
|
121
|
+
return
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
for s in all(solids) do
|
|
125
|
+
rectfill(s[1],s[2],s[1]+s[3]-1,s[2]+s[4]-1,4)
|
|
126
|
+
rectfill(s[1],s[2],s[1]+s[3]-1,s[2],9) -- grass top
|
|
127
|
+
end
|
|
128
|
+
for c in all(coins) do spr(5+flr(c.f)%4,c.x-4,c.y-4) end
|
|
129
|
+
-- flag
|
|
130
|
+
rectfill(goal.x,goal.y,goal.x,goal.y+12,6)
|
|
131
|
+
spr(10,goal.x,goal.y-1)
|
|
132
|
+
-- hero
|
|
133
|
+
local s=p.grounded and (p.dx~=0 and (5+flr(p.anim)%2==5 and 2 or (flr(p.anim)%2==0 and 1 or 2)) or 1) or 3
|
|
134
|
+
spr(p.dx~=0 and (flr(p.anim)%2==0 and 1 or 2) or (p.grounded and 1 or 3),p.x-4,p.y-7,1,1,p.face<0)
|
|
135
|
+
|
|
136
|
+
rectfill(0,0,127,7,0)
|
|
137
|
+
print("coins "..collected.."/"..total,2,1,10)
|
|
138
|
+
if best<99 then print("best "..best.."s",84,1,6) end
|
|
139
|
+
if state=="win" then
|
|
140
|
+
rectfill(28,50,100,80,0) rect(28,50,100,80,11)
|
|
141
|
+
print("you win!",44,55,11)
|
|
142
|
+
print("time "..(t\30).."s",44,64,7)
|
|
143
|
+
print("z/x title",40,72,6)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
__gfx__
|
|
148
|
+
0000000000fff00000fff00080fff0080000000000aaaa00000aa0000000a000000aa00000066000066000000000000000000000000000000000000000000000
|
|
149
|
+
0000000000fff00000fff00008fff880000000000a9779a000a99a00000a9a0000a99a0000066660066600000000000000000000000000000000000000000000
|
|
150
|
+
0000000000f4f00000f4f00000f4f00000000000a977779a0a9779a0000a9a000a9779a000060000066660000000000000000000000000000000000000000000
|
|
151
|
+
0000000008888000088880000088880000000000a977779a0a9779a0000a9a000a9779a000060000066600000000000000000000000000000000000000000000
|
|
152
|
+
0000000008888000088880000088880000000000a999999a0a9999a0000a9a000a9999a000600000066000000000000000000000000000000000000000000000
|
|
153
|
+
0000000008880000088800000008800000000000a999999a0a9999a0000a9a000a9999a000060000060000000000000000000000000000000000000000000000
|
|
154
|
+
00000000008008000088000000800800000000000a9999a000a99a00000a9a0000a99a0000060000060000000000000000000000000000000000000000000000
|
|
155
|
+
0000000000b00b00000bb00000b00b000000000000aaaa00000aa0000000a000000aa00000060000060000000000000000000000000000000000000000000000
|
|
156
|
+
__sfx__
|
|
157
|
+
000300001f04025040290400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
158
|
+
000200002465527655296550000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
159
|
+
011000002465521655216551d6551a655000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
160
|
+
010600000865505655036550000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
161
|
+
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
162
|
+
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
163
|
+
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
164
|
+
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
165
|
+
001400001c4501c4501d4501f4501f4501d4501c4501a45018450184501a4501c4501c4501a4501a4501a4501c4501c4501d4501f4501f4501d4501c4501a45018450184501a4501c4501a450184501845018450
|
|
166
|
+
001400000c2300c2300c2301323013230132300c230132300c2300c230132300c2300c2301323013230132300c2300c2300c2301323013230132300c230132300c2300c230132300c230132300c2300c2300c230
|
|
167
|
+
__music__
|
|
168
|
+
03 08094040
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
pico-8 cartridge // http://www.pico-8.com
|
|
2
|
+
version 18
|
|
3
|
+
__lua__
|
|
4
|
+
-- color drop — a falling-column match puzzle (Columns-style) scaffold
|
|
5
|
+
-- genre example for romdev/pico8. THE GAME: a vertical column of 3 jewels
|
|
6
|
+
-- falls into an 8-wide well. move left/right, soft-drop (down), hard-drop
|
|
7
|
+
-- (x), and CYCLE the 3 colors (o). match 3+ of one color horizontally,
|
|
8
|
+
-- vertically, or DIAGONALLY to clear; gravity pulls survivors down, which
|
|
9
|
+
-- can CASCADE into more clears. fork it and reshape one thing at a time.
|
|
10
|
+
|
|
11
|
+
cols=8 rows=13 cell=9 ox=26 oy=3
|
|
12
|
+
ncol=5 -- jewel colors 1..5
|
|
13
|
+
-- well fits the 128px screen: oy + rows*cell = 3 + 117 = 120 (bottom row fully visible).
|
|
14
|
+
|
|
15
|
+
function _init()
|
|
16
|
+
hi=0
|
|
17
|
+
state="title"
|
|
18
|
+
boot=0
|
|
19
|
+
reset_game()
|
|
20
|
+
|
|
21
|
+
music(0)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
function reset_game()
|
|
25
|
+
grid={}
|
|
26
|
+
for y=1,rows do grid[y]={} for x=1,cols do grid[y][x]=0 end end
|
|
27
|
+
score=0 chain=0 level=1 cleared=0
|
|
28
|
+
fall=0 t=0
|
|
29
|
+
flashing=nil -- {cells=..,ttl=..} during a clear
|
|
30
|
+
nextp=randcol3()
|
|
31
|
+
spawn_piece()
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
function randcol3()
|
|
35
|
+
return {flr(rnd(ncol))+1,flr(rnd(ncol))+1,flr(rnd(ncol))+1}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
-- the falling column: 3 colors top->bottom at (px, py=top cell)
|
|
39
|
+
function spawn_piece()
|
|
40
|
+
pcol=nextp
|
|
41
|
+
nextp=randcol3()
|
|
42
|
+
px=flr(cols/2) py=1
|
|
43
|
+
-- game over if the top cells are blocked
|
|
44
|
+
if grid[1][px]~=0 or grid[2][px]~=0 or grid[3][px]~=0 then
|
|
45
|
+
hi=max(hi,score) state="over"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
function can_be(x,y)
|
|
50
|
+
-- can the 3-tall column sit with its TOP cell at (x,y)?
|
|
51
|
+
for i=0,2 do
|
|
52
|
+
local cy=y+i
|
|
53
|
+
if cy>rows then return false end
|
|
54
|
+
if cy>=1 and grid[cy][x]~=0 then return false end
|
|
55
|
+
end
|
|
56
|
+
return true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
function _update()
|
|
60
|
+
t+=1
|
|
61
|
+
|
|
62
|
+
-- resolve an in-progress clear (flash then remove + cascade)
|
|
63
|
+
if flashing then
|
|
64
|
+
flashing.ttl-=1
|
|
65
|
+
if flashing.ttl<=0 then
|
|
66
|
+
for c in all(flashing.cells) do grid[c.y][c.x]=0 end
|
|
67
|
+
collapse()
|
|
68
|
+
local m=find_matches()
|
|
69
|
+
if #m>0 then chain+=1 start_clear(m)
|
|
70
|
+
else chain=0 flashing=nil spawn_piece() end
|
|
71
|
+
end
|
|
72
|
+
return
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
if state=="title" then
|
|
76
|
+
boot+=1
|
|
77
|
+
if boot>10 and (btnp(4) or btnp(5)) then state="play" reset_game() music(-1) end
|
|
78
|
+
return
|
|
79
|
+
end
|
|
80
|
+
if state=="over" then
|
|
81
|
+
if btnp(4) or btnp(5) then state="title" end
|
|
82
|
+
return
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
-- move — snappy custom repeat (move on press, then again every 4 frames while held)
|
|
86
|
+
-- so a hold slides smoothly instead of pico-8's slow 15-frame btnp repeat.
|
|
87
|
+
hold_l = btn(0) and (hold_l or 0)+1 or 0
|
|
88
|
+
hold_r = btn(1) and (hold_r or 0)+1 or 0
|
|
89
|
+
if (hold_l==1 or (hold_l>4 and hold_l%3==0)) and px>1 and can_be(px-1,py) then px-=1 sfx(2) end
|
|
90
|
+
if (hold_r==1 or (hold_r>4 and hold_r%3==0)) and px<cols and can_be(px+1,py) then px+=1 sfx(2) end
|
|
91
|
+
-- cycle colors (o = btn4): rotate the 3-jewel column
|
|
92
|
+
if btnp(4) then
|
|
93
|
+
pcol={pcol[3],pcol[1],pcol[2]} sfx(3)
|
|
94
|
+
end
|
|
95
|
+
-- hard drop (x = btn5)
|
|
96
|
+
if btnp(5) then
|
|
97
|
+
while can_be(px,py+1) do py+=1 end
|
|
98
|
+
lock_piece() return
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
-- gravity (soft-drop on down)
|
|
102
|
+
local speed=btn(3) and 2 or max(6,26-level*2)
|
|
103
|
+
fall+=1
|
|
104
|
+
if fall>=speed then
|
|
105
|
+
fall=0
|
|
106
|
+
if can_be(px,py+1) then py+=1
|
|
107
|
+
else lock_piece() end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
function lock_piece()
|
|
112
|
+
for i=0,2 do
|
|
113
|
+
local cy=py+i
|
|
114
|
+
if cy>=1 and cy<=rows then grid[cy][px]=pcol[i+1] end
|
|
115
|
+
end
|
|
116
|
+
sfx(0)
|
|
117
|
+
local m=find_matches()
|
|
118
|
+
if #m>0 then chain=1 start_clear(m)
|
|
119
|
+
else spawn_piece() end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
function start_clear(cells)
|
|
123
|
+
flashing={cells=cells,ttl=12}
|
|
124
|
+
score+=#cells*10*chain
|
|
125
|
+
cleared+=#cells
|
|
126
|
+
level=1+flr(cleared/20)
|
|
127
|
+
sfx(1)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
-- scan H, V, and both diagonals for runs of 3+ same color.
|
|
131
|
+
-- returns a list of {x,y} cells to clear (deduped).
|
|
132
|
+
local dirs={{1,0},{0,1},{1,1},{1,-1}}
|
|
133
|
+
function find_matches()
|
|
134
|
+
local hit={} -- key "x,y" -> {x,y}
|
|
135
|
+
for y=1,rows do for x=1,cols do
|
|
136
|
+
local c=grid[y][x]
|
|
137
|
+
if c~=0 then
|
|
138
|
+
for d in all(dirs) do
|
|
139
|
+
-- only start a run at its beginning (no same-color cell behind us)
|
|
140
|
+
local bx,by=x-d[1],y-d[2]
|
|
141
|
+
local prev = (bx>=1 and bx<=cols and by>=1 and by<=rows) and grid[by][bx] or -1
|
|
142
|
+
if prev~=c then
|
|
143
|
+
local run={}
|
|
144
|
+
local cx,cy=x,y
|
|
145
|
+
while cx>=1 and cx<=cols and cy>=1 and cy<=rows and grid[cy][cx]==c do
|
|
146
|
+
add(run,{x=cx,y=cy}) cx+=d[1] cy+=d[2]
|
|
147
|
+
end
|
|
148
|
+
if #run>=3 then for r in all(run) do hit[r.x..","..r.y]={x=r.x,y=r.y} end end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end end
|
|
153
|
+
local out={} for _,v in pairs(hit) do add(out,v) end
|
|
154
|
+
return out
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
-- gravity: drop every column's survivors to the bottom
|
|
158
|
+
function collapse()
|
|
159
|
+
for x=1,cols do
|
|
160
|
+
local w=rows
|
|
161
|
+
for y=rows,1,-1 do
|
|
162
|
+
if grid[y][x]~=0 then
|
|
163
|
+
grid[w][x]=grid[y][x] if w~=y then grid[y][x]=0 end w-=1
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
-- ── draw ──
|
|
170
|
+
gemcol={8,9,10,11,12}
|
|
171
|
+
function drawgem(gx,gy,c)
|
|
172
|
+
local base=gemcol[c] or 8
|
|
173
|
+
rectfill(gx+1,gy,gx+6,gy+7,base)
|
|
174
|
+
rectfill(gx,gy+1,gx+7,gy+6,base)
|
|
175
|
+
rectfill(gx+2,gy+1,gx+3,gy+2,7) -- sparkle
|
|
176
|
+
line(gx+1,gy+6,gx+6,gy+6,1) -- shadow
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
function _draw()
|
|
180
|
+
cls(0)
|
|
181
|
+
rectfill(0,0,ox-2,127,1)
|
|
182
|
+
rectfill(ox+cols*cell+1,0,127,127,1)
|
|
183
|
+
|
|
184
|
+
if state=="title" then
|
|
185
|
+
print("color drop",42,40,12)
|
|
186
|
+
-- show a sample 3-jewel column
|
|
187
|
+
for i=1,3 do drawgem(60,50+i*9,i) end
|
|
188
|
+
print("arrows move",34,86,7)
|
|
189
|
+
print("o cycle x drop",26,96,6)
|
|
190
|
+
print("z/x start",40,108,7)
|
|
191
|
+
return
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
rect(ox-1,oy-1,ox+cols*cell,oy+rows*cell,5)
|
|
195
|
+
-- placed gems
|
|
196
|
+
for y=1,rows do for x=1,cols do
|
|
197
|
+
if grid[y][x]~=0 then drawgem(ox+(x-1)*cell,oy+(y-1)*cell,grid[y][x]) end
|
|
198
|
+
end end
|
|
199
|
+
-- flashing clears
|
|
200
|
+
if flashing then
|
|
201
|
+
for c in all(flashing.cells) do
|
|
202
|
+
if t%4<2 then rectfill(ox+(c.x-1)*cell,oy+(c.y-1)*cell,ox+(c.x-1)*cell+8,oy+(c.y-1)*cell+8,7) end
|
|
203
|
+
end
|
|
204
|
+
elseif state=="play" then
|
|
205
|
+
-- the falling 3-jewel column
|
|
206
|
+
for i=0,2 do
|
|
207
|
+
local cy=py+i
|
|
208
|
+
if cy>=1 then drawgem(ox+(px-1)*cell,oy+(cy-1)*cell,pcol[i+1]) end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
-- panel
|
|
213
|
+
print("score",102,8,7) print(score,102,16,10)
|
|
214
|
+
print("next",102,36,7)
|
|
215
|
+
for i=1,3 do drawgem(106,44+i*9,nextp[i]) end
|
|
216
|
+
print("lv "..level,102,76,7)
|
|
217
|
+
if chain>1 then print("x"..chain,102,88,8) end
|
|
218
|
+
|
|
219
|
+
if state=="over" then
|
|
220
|
+
rectfill(18,52,110,76,0) rect(18,52,110,76,8)
|
|
221
|
+
print("game over",44,56,8)
|
|
222
|
+
print("hi "..hi,52,66,10)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
__gfx__
|
|
227
|
+
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
228
|
+
__sfx__
|
|
229
|
+
010800001c0551c055000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
230
|
+
011000002465527655296552d65500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
231
|
+
010400001865518655000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
232
|
+
010c00002465521655000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
233
|
+
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
234
|
+
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
235
|
+
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
236
|
+
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
237
|
+
0014000028450274502845027450284502345026450244502145000000184501c4502145023450000001c450204502345024450000001c4502845027450284502745028450234502645024450214500000000000
|
|
238
|
+
001400001523000000000000000015230000000000000000102300000000000000000c23000000000000000015230000000000000000152300000000000000001023000000000000000015230000000000000000
|
|
239
|
+
__music__
|
|
240
|
+
03 08094040
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
pico-8 cartridge // http://www.pico-8.com
|
|
2
|
+
version 18
|
|
3
|
+
__lua__
|
|
4
|
+
-- lane runner — a top-down endless racing scaffold
|
|
5
|
+
-- genre example for romdev/pico8. car sprites (see __gfx__), looping
|
|
6
|
+
-- music, engine/crash sfx, 3-lane dodging, scrolling road + dashes,
|
|
7
|
+
-- speed ramp, distance score + best. fork it.
|
|
8
|
+
|
|
9
|
+
function _init()
|
|
10
|
+
best=0
|
|
11
|
+
state="title"
|
|
12
|
+
boot=0
|
|
13
|
+
reset_run()
|
|
14
|
+
music(0)
|
|
15
|
+
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
function reset_run()
|
|
19
|
+
car={x=64,lane=1}
|
|
20
|
+
lanes={40,64,88}
|
|
21
|
+
rivals={}
|
|
22
|
+
dist=0 speed=2 spawn=0 road=0 t=0 shake=0
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
function _update()
|
|
26
|
+
t+=1
|
|
27
|
+
road=(road+speed)%16
|
|
28
|
+
if shake>0 then shake-=1 end
|
|
29
|
+
if state=="title" then
|
|
30
|
+
boot+=1
|
|
31
|
+
if boot>10 and (btnp(4) or btnp(5)) then state="play" reset_run() music(-1) end
|
|
32
|
+
return
|
|
33
|
+
end
|
|
34
|
+
if state=="over" then
|
|
35
|
+
if btnp(4) or btnp(5) then state="title" end
|
|
36
|
+
return
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
if btnp(0) and car.lane>0 then car.lane-=1 sfx(0) end
|
|
40
|
+
if btnp(1) and car.lane<2 then car.lane+=1 sfx(0) end
|
|
41
|
+
car.x=car.x+(lanes[car.lane+1]-car.x)*.4
|
|
42
|
+
|
|
43
|
+
speed=min(2+dist/700,5.5)
|
|
44
|
+
dist+=speed
|
|
45
|
+
|
|
46
|
+
spawn-=1
|
|
47
|
+
if spawn<=0 then
|
|
48
|
+
add(rivals,{lane=flr(rnd(3)),y=-14,c=flr(rnd(3))})
|
|
49
|
+
spawn=max(16,44-flr(dist/110))
|
|
50
|
+
end
|
|
51
|
+
for r in all(rivals) do
|
|
52
|
+
r.y+=speed
|
|
53
|
+
if r.y>140 then del(rivals,r) end
|
|
54
|
+
if r.lane==car.lane and abs(r.y-104)<13 then
|
|
55
|
+
sfx(2) shake=12 best=max(best,flr(dist/10)) state="over"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
function _draw()
|
|
61
|
+
local sx=shake>0 and (rnd(4)-2) or 0
|
|
62
|
+
cls(3)
|
|
63
|
+
camera(sx,0)
|
|
64
|
+
-- grass texture
|
|
65
|
+
for i=0,8 do for j=0,8 do if (i+j)%2==0 then pset(i*16+2,j*16+8,11) end end end
|
|
66
|
+
-- road
|
|
67
|
+
rectfill(28,0,100,128,5)
|
|
68
|
+
rectfill(28,0,31,128,10) rectfill(97,0,100,128,10)
|
|
69
|
+
for y=-16,128,16 do
|
|
70
|
+
rectfill(52,y+road,54,y+road+9,7)
|
|
71
|
+
rectfill(74,y+road,76,y+road+9,7)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
if state=="title" then
|
|
75
|
+
camera()
|
|
76
|
+
rectfill(12,42,116,94,0)
|
|
77
|
+
print("lane runner",40,48,10)
|
|
78
|
+
spr(1,60,64)
|
|
79
|
+
print("left/right swerve",22,80,6)
|
|
80
|
+
print("z/x start",40,88,7)
|
|
81
|
+
if best>0 then print("best "..best,50,100,7) end
|
|
82
|
+
return
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
for r in all(rivals) do spr(2+r.c,lanes[r.lane+1]-4,r.y-6) end
|
|
86
|
+
spr(1,car.x-4,104-6)
|
|
87
|
+
|
|
88
|
+
camera()
|
|
89
|
+
rectfill(0,0,127,8,0)
|
|
90
|
+
print("dist "..flr(dist/10),2,1,7)
|
|
91
|
+
print("spd "..flr(speed*10),88,1,10)
|
|
92
|
+
|
|
93
|
+
if state=="over" then
|
|
94
|
+
rectfill(22,52,106,80,0) rect(22,52,106,80,8)
|
|
95
|
+
print("crashed!",46,56,8)
|
|
96
|
+
print("dist "..flr(dist/10),44,66,7)
|
|
97
|
+
print("z/x title",40,74,6)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
__gfx__
|
|
102
|
+
000000000008800000099000000aa000000ee0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
103
|
+
0000000000888800009999000aaaaa000eeeee000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
104
|
+
0000000008777780097777900a7777a00e7777e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
105
|
+
0000000008888880099999900aaaaaa00eeeeee00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
106
|
+
0000000008888880099999900aaaaaa00eeeeee00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
107
|
+
0000000008777780097777900a7777a00e7777e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
108
|
+
000000000088880000999900000aa000000ee0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
109
|
+
000000000080080000900900000aa000000ee0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
110
|
+
__sfx__
|
|
111
|
+
010800001505013050000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
112
|
+
010400001f05522055000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
113
|
+
010600000c05008050050550000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
114
|
+
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
115
|
+
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
116
|
+
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
117
|
+
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
118
|
+
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
119
|
+
0012000017350183501a3501c3501a35018350173501a350133501f3501c350133501f3501c350133501f35017350183501a3501c3501a35018350173501a3501c3501d3501f3501d3501c3501a3501835017350
|
|
120
|
+
001200000b230000000b230000000b230000000b230000000c230000000c230000000c230000000c230000000b230000000b230000000b230000000b230000001023000000102300000010230000000b23000000
|
|
121
|
+
__music__
|
|
122
|
+
03 08094040
|
|
@@ -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",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"romdev-audio-resampler": "0.1.0",
|
|
56
56
|
"romdev-core-beetle-psx-hw": "0.2.0",
|
|
57
57
|
"romdev-core-bluemsx": "0.7.0",
|
|
58
|
+
"romdev-core-fake08": "0.1.0",
|
|
58
59
|
"romdev-core-fceumm": "0.11.0",
|
|
59
60
|
"romdev-core-flycast": "0.2.0",
|
|
60
61
|
"romdev-core-gambatte": "0.10.0",
|
|
@@ -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,6 +212,7 @@ 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";
|
|
217
218
|
import { decodeCode as decodeCheatCode } from "../cheats/gamegenie.js";
|
package/src/mcp/tools/disasm.js
CHANGED
|
@@ -1190,6 +1190,59 @@ function renderBuildMd({ platform, romPath, regions, blobs, build, verifiable, n
|
|
|
1190
1190
|
* The default endian follows the CPU (Genesis/m68k → BE; everything else → LE),
|
|
1191
1191
|
* overridable via `endian`.
|
|
1192
1192
|
*/
|
|
1193
|
+
/**
|
|
1194
|
+
* target='source' — return a cart's high-level SOURCE instead of a disassembly, for
|
|
1195
|
+
* platforms where the "cart" IS source (not machine code). Today: PICO-8 (.p8 = Lua +
|
|
1196
|
+
* data sections; .p8.png = the same cart embedded in a label PNG). This is the honest
|
|
1197
|
+
* "understand this cart" path for a Lua VM — there's no machine code to disassemble, so
|
|
1198
|
+
* we hand back the actual Lua the game runs, plus a map of the other sections.
|
|
1199
|
+
*/
|
|
1200
|
+
async function readCartSourceCore(args) {
|
|
1201
|
+
const romPath = requireRomPath(args);
|
|
1202
|
+
const fs = await import("node:fs/promises");
|
|
1203
|
+
const lower = romPath.toLowerCase();
|
|
1204
|
+
const isPng = lower.endsWith(".p8.png");
|
|
1205
|
+
const isP8 = lower.endsWith(".p8");
|
|
1206
|
+
if (!isP8 && !isPng && args.platform !== "pico8") {
|
|
1207
|
+
throw new Error(
|
|
1208
|
+
`disasm({target:'source'}) is for source-form carts (PICO-8 .p8/.p8.png). '${romPath}' isn't one. ` +
|
|
1209
|
+
`For machine-code ROMs use target:'rom'/'project'/'decompile'.`,
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
if (isPng) {
|
|
1213
|
+
// A .p8.png embeds the cart in the PNG's low bits — decoding needs the FAKE-08 core.
|
|
1214
|
+
throw new Error(
|
|
1215
|
+
"disasm({target:'source'}): .p8.png carts store the source steganographically in the PNG — " +
|
|
1216
|
+
"load it (loadMedia({platform:'pico8'})) to run it; export the .p8 text form to read the Lua. " +
|
|
1217
|
+
"Pass a plain-text .p8 here to read its source directly.",
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
const text = await fs.readFile(romPath, "utf8");
|
|
1221
|
+
// Split on __section__ markers, preserving order.
|
|
1222
|
+
const parts = text.split(/(^__[a-z]+__$)/m);
|
|
1223
|
+
const sections = {};
|
|
1224
|
+
let current = "(header)";
|
|
1225
|
+
let buf = [];
|
|
1226
|
+
const flush = () => { if (buf.length) sections[current] = buf.join("\n").replace(/^\n+|\n+$/g, ""); buf = []; };
|
|
1227
|
+
for (const chunk of parts) {
|
|
1228
|
+
const m = chunk.match(/^__([a-z]+)__$/);
|
|
1229
|
+
if (m) { flush(); current = m[1]; } else { buf.push(chunk); }
|
|
1230
|
+
}
|
|
1231
|
+
flush();
|
|
1232
|
+
return {
|
|
1233
|
+
platform: "pico8",
|
|
1234
|
+
kind: "source",
|
|
1235
|
+
format: ".p8",
|
|
1236
|
+
note: "PICO-8 carts are SOURCE, not machine code — this is the Lua the game runs (+ its data sections). There is no disassembly to do.",
|
|
1237
|
+
lua: sections.lua ?? "",
|
|
1238
|
+
sections: Object.keys(sections),
|
|
1239
|
+
// Include non-lua data sections so callers can see the full cart shape (gfx/map/sfx/music/gff/label).
|
|
1240
|
+
dataSections: Object.fromEntries(
|
|
1241
|
+
Object.entries(sections).filter(([k]) => k !== "lua" && k !== "(header)"),
|
|
1242
|
+
),
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1193
1246
|
async function pointerTableCore(args) {
|
|
1194
1247
|
const { platform, loBase, hiBase, count, convention = "direct", reverseHandler, bank } = args;
|
|
1195
1248
|
if (loBase == null) throw new Error("disasm({target:'pointerTable'}): loBase (the table / low-byte base) is required.");
|
|
@@ -1485,11 +1538,11 @@ export function registerDisasmTools(server, z) {
|
|
|
1485
1538
|
"LLM folds it. `address` for all four comes from target:'functions' (a CPU/virtual address; the file-offset " +
|
|
1486
1539
|
"mapping is handled for you).",
|
|
1487
1540
|
{
|
|
1488
|
-
target: z.enum(["bytes", "rom", "project", "references", "cfg", "xrefs", "functions", "decompile", "resolveJumptable", "pointerTable", "recompile"]).describe("bytes = raw chunk; rom = mapper-aware ROM; project = full rebuildable disasm; references = flat da65 operand-refs to an address; functions/cfg/xrefs = Rizin RE engine (function list / control-flow graph / deep graph xrefs); decompile = Ghidra C pseudocode; resolveJumptable = recover a computed-jump dispatcher's targets (LIVE — redirects to breakpoint({on:'jumptable'}), which runs the emulator and records the real switch arms a static decompiler can't follow); recompile = EMIT backend — statically recompile a NES ROM's reset routine to SNES 65816 asar source that builds + boots (phase 1: NROM, 6502→65816 emulation mode, PPU/APU seam STUBBED). See the tool description for the RE loop + the decompile altitude rule + per-CPU quality (all 14 platforms)."),
|
|
1541
|
+
target: z.enum(["bytes", "rom", "project", "references", "cfg", "xrefs", "functions", "decompile", "source", "resolveJumptable", "pointerTable", "recompile"]).describe("bytes = raw chunk; rom = mapper-aware ROM; project = full rebuildable disasm; references = flat da65 operand-refs to an address; functions/cfg/xrefs = Rizin RE engine (function list / control-flow graph / deep graph xrefs); decompile = Ghidra C pseudocode; resolveJumptable = recover a computed-jump dispatcher's targets (LIVE — redirects to breakpoint({on:'jumptable'}), which runs the emulator and records the real switch arms a static decompiler can't follow); recompile = EMIT backend — statically recompile a NES ROM's reset routine to SNES 65816 asar source that builds + boots (phase 1: NROM, 6502→65816 emulation mode, PPU/APU seam STUBBED). See the tool description for the RE loop + the decompile altitude rule + per-CPU quality (all 14 platforms)."),
|
|
1489
1542
|
// shared
|
|
1490
1543
|
path: z.string().optional().describe("target=bytes: raw binary path. target=rom/project/references: ROM file path."),
|
|
1491
1544
|
base64: z.string().optional().describe("target=bytes: base64 of the bytes (OR `path`)."),
|
|
1492
|
-
platform: z.enum(["nes", "snes", "sms", "gg", "gb", "gbc", "atari2600", "atari7800", "c64", "genesis", "gba", "pce", "msx", "lynx"]).optional().describe("target=rom/project/references: override platform (else sniffed from extension)."),
|
|
1545
|
+
platform: z.enum(["nes", "snes", "sms", "gg", "gb", "gbc", "atari2600", "atari7800", "c64", "genesis", "gba", "pce", "msx", "lynx", "pico8"]).optional().describe("target=rom/project/references: override platform (else sniffed from extension). target=source: pico8 (.p8 carts are Lua source)."),
|
|
1493
1546
|
startAddress: z.number().int().min(0).max(0xffffffff).default(0x8000).describe("target=bytes/rom: address of the first byte (GBA auto-bumped to 0x08000000)."),
|
|
1494
1547
|
length: z.number().int().min(1).max(65536).optional().describe("target=rom: bytes to disassemble (default 256; mutually exclusive with endAddress)."),
|
|
1495
1548
|
addOrigin: z.boolean().default(true).describe("target=bytes/rom: prepend `.org` so the asm re-assembles through ca65."),
|
|
@@ -1540,6 +1593,7 @@ export function registerDisasmTools(server, z) {
|
|
|
1540
1593
|
case "xrefs": return jsonContent(await analyzeXrefs(requireRomPath(args), args.address, args.platform));
|
|
1541
1594
|
case "functions": return jsonContent(await analyzeFunctions(requireRomPath(args), args.platform));
|
|
1542
1595
|
case "decompile": return jsonContent(await analyzeDecompile(requireRomPath(args), args.address, args.platform));
|
|
1596
|
+
case "source": return jsonContent(await readCartSourceCore(args));
|
|
1543
1597
|
case "recompile": return await recompileCore(args);
|
|
1544
1598
|
case "pointerTable": return jsonContent(await pointerTableCore(args));
|
|
1545
1599
|
case "resolveJumptable":
|
package/src/mcp/tools/project.js
CHANGED
|
@@ -662,6 +662,33 @@ const TEMPLATES = {
|
|
|
662
662
|
mechanics: ["direct framebuffer paint (clear + solid rects)", "recognizable test pattern"],
|
|
663
663
|
techniques: ["PowerVR2 framebuffer bring-up (FB_R_CTRL/FB_R_SIZE/FB_R_SOF1 + SPG)", "640x480 RGB565 at VRAM 0xA5000000", "SH-4 bare crt0 (stack + .bss + main)", "boots on Flycast reios HLE — no firmware"] },
|
|
664
664
|
},
|
|
665
|
+
// PICO-8 (FAKE-08): the "source" IS a complete .p8 cart (Lua + __gfx__/__sfx__/__music__
|
|
666
|
+
// sections), so there's no crt0/runtime/linker — fork copies the .p8 and
|
|
667
|
+
// build({platform:'pico8', source:<the .p8 text>}) packages it. Each is a real playable
|
|
668
|
+
// game: hand-authored pixel-art sprites, looping music + SFX, title/play/over states,
|
|
669
|
+
// full genre mechanics. Fork one and reshape a single thing at a time.
|
|
670
|
+
pico8: {
|
|
671
|
+
shmup: { main: "templates/shmup.p8", runtime: [], lang: "Lua (FAKE-08)", ext: ".p8",
|
|
672
|
+
describe: "STAR SWEEPER — a vertical shooter. Ship sprite w/ thruster animation + i-frame blink, two enemy sprite types, projectile pool, ramping wave spawner, AABB collision, explosion sprites, score + persistent hi-score, looping title/play music that switches on start. The full shmup contract in ~130 lines of Lua.",
|
|
673
|
+
players: "1", mechanics: ["projectile pool", "wave spawner", "AABB collision", "i-frames", "title/play/over state machine"],
|
|
674
|
+
techniques: ["hand-authored __gfx__ sprites drawn with spr()", "__music__ + __sfx__ banks (music switches per state)", "sprite animation via frame flip", "scrolling starfield"] },
|
|
675
|
+
platformer: { main: "templates/platformer.p8", runtime: [], lang: "Lua (FAKE-08)", ext: ".p8",
|
|
676
|
+
describe: "HOP QUEST — a platformer. Hero sprite with idle/walk/jump frames (h-flipped by facing), gravity + solid-box collision, variable-height jump, animated spinning-coin sprites to collect, a goal flag, parallax hills, best-time tracking. Fork the movement/level and go.",
|
|
677
|
+
players: "1", mechanics: ["gravity + solid-box collision", "variable jump", "coin pickups", "goal + win state", "respawn on fall"],
|
|
678
|
+
techniques: ["hand-authored hero + coin sprites", "spr() h-flip by facing", "walk-cycle animation", "__music__ loop + jump/coin SFX", "parallax backdrop"] },
|
|
679
|
+
puzzle: { main: "templates/puzzle.p8", runtime: [], lang: "Lua (FAKE-08)", ext: ".p8",
|
|
680
|
+
describe: "COLOR DROP — a falling-gem match puzzle. Five faceted gem sprites, an 8×12 well, slide + soft/hard drop, horizontal match-3 line clears with a flash then gravity-settle + combo scoring, next-gem preview, game-over on stack-out. Looping music + drop/slide/match SFX.",
|
|
681
|
+
players: "1", mechanics: ["grid + falling piece", "match-3 clears", "gravity settle", "combo scoring", "next preview", "stack-out game over"],
|
|
682
|
+
techniques: ["hand-authored gem sprites", "flash-then-clear timing (state machine on a flash queue)", "column gravity compaction", "__music__ loop + SFX"] },
|
|
683
|
+
sports: { main: "templates/sports.p8", runtime: [], lang: "Lua (FAKE-08)", ext: ".p8",
|
|
684
|
+
describe: "RALLY VOLLEY — a 2-player paddle sports game. Paddle + ball sprites, angle-off-paddle physics, ball speed-up per rally, 1P-vs-CPU (tracking AI) or 2P couch mode toggled on the title, first-to-5 scoring. Looping music + wall/paddle/point SFX. The classic 2P scaffold.",
|
|
685
|
+
players: "1-2 (vs CPU or couch 2P)", mechanics: ["paddle + ball physics", "angle off paddle", "rally speed-up", "AI opponent", "first-to-5 match"],
|
|
686
|
+
techniques: ["hand-authored paddle/ball sprites", "mode toggle on title screen", "simple tracking AI", "__music__ loop + bounce/point SFX"] },
|
|
687
|
+
racing: { main: "templates/racing.p8", runtime: [], lang: "Lua (FAKE-08)", ext: ".p8",
|
|
688
|
+
describe: "LANE RUNNER — a top-down endless racer. Player + rival car sprites, 3-lane snap-steer dodging, scrolling road with animated lane dashes + shoulders, speed that ramps with distance, screen-shake on crash, distance score + best. Looping engine music + swerve/crash SFX.",
|
|
689
|
+
players: "1", mechanics: ["lane-snap steering", "rival spawner", "speed ramp", "crash + game over", "distance score"],
|
|
690
|
+
techniques: ["hand-authored car sprites", "scrolling-road illusion (moving dashes)", "camera() screen-shake", "__music__ loop + SFX"] },
|
|
691
|
+
},
|
|
665
692
|
};
|
|
666
693
|
// R37: GBC has its own scaffold tree at examples/gbc/templates/ +
|
|
667
694
|
// src/platforms/gbc/lib/c/. Same runtime files as GB (the APU + Z80 +
|
package/src/toolchains/index.js
CHANGED
|
@@ -1042,8 +1042,46 @@ export async function buildForPlatform(args) {
|
|
|
1042
1042
|
};
|
|
1043
1043
|
}
|
|
1044
1044
|
|
|
1045
|
+
if (args.platform === "pico8") {
|
|
1046
|
+
// PICO-8 "build" = PACKAGE a .p8 cart (plain-text sections), NOT compile to machine
|
|
1047
|
+
// code — the Lua is source. Accept bare Lua (`source`/`lua`) wrapped into a valid .p8,
|
|
1048
|
+
// or a complete .p8 passed through. Optional data sections (gfx/map/sfx/music) via
|
|
1049
|
+
// `sections`. The "binary" IS the .p8 text bytes, which FAKE-08 loads + runs directly.
|
|
1050
|
+
const { packP8 } = await import("./pico8/pack.js");
|
|
1051
|
+
try {
|
|
1052
|
+
const lua = args.lua ?? args.source ?? args.code;
|
|
1053
|
+
const { text, bytes, warnings } = packP8({
|
|
1054
|
+
lua,
|
|
1055
|
+
p8: args.p8,
|
|
1056
|
+
version: args.p8Version,
|
|
1057
|
+
sections: args.sections ?? {},
|
|
1058
|
+
});
|
|
1059
|
+
return {
|
|
1060
|
+
ok: true,
|
|
1061
|
+
binary: bytes,
|
|
1062
|
+
listing: text, // the readable .p8 IS the listing
|
|
1063
|
+
symbols: "",
|
|
1064
|
+
log: warnings.length ? warnings.map((w) => `[pico8] ${w}`).join("\n") : "[pico8] packaged .p8 cart",
|
|
1065
|
+
issues: [],
|
|
1066
|
+
exitCode: 0,
|
|
1067
|
+
toolchain: "pico8-pack",
|
|
1068
|
+
};
|
|
1069
|
+
} catch (e) {
|
|
1070
|
+
return {
|
|
1071
|
+
ok: false,
|
|
1072
|
+
binary: null,
|
|
1073
|
+
listing: "",
|
|
1074
|
+
symbols: "",
|
|
1075
|
+
log: String(e.message ?? e),
|
|
1076
|
+
issues: [],
|
|
1077
|
+
exitCode: 1,
|
|
1078
|
+
toolchain: "pico8-pack",
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1045
1083
|
throw new Error(
|
|
1046
|
-
`no bundled toolchain for platform '${args.platform}'. Supported: atari2600 (dasm), nes/c64/atari7800/lynx (cc65), snes (C via tcc-65816+wla+PVSnesLib, or asm via asar), genesis (C via m68k-gcc+SGDK, or asm via vasm68k), gba (C via arm-gcc+libtonc/libgba), gb/gbc (sdcc sm83 / rgbds), sms/gg (sdcc). Call listPlatforms for the live matrix.`,
|
|
1084
|
+
`no bundled toolchain for platform '${args.platform}'. Supported: atari2600 (dasm), nes/c64/atari7800/lynx (cc65), snes (C via tcc-65816+wla+PVSnesLib, or asm via asar), genesis (C via m68k-gcc+SGDK, or asm via vasm68k), gba (C via arm-gcc+libtonc/libgba), gb/gbc (sdcc sm83 / rgbds), sms/gg (sdcc), pico8 (.p8 cart packager). Call listPlatforms for the live matrix.`,
|
|
1047
1085
|
);
|
|
1048
1086
|
}
|
|
1049
1087
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// PICO-8 .p8 cart "builder" — a PACKAGER, not a compiler. PICO-8 carts are plain-text
|
|
2
|
+
// files with labeled sections (__lua__, __gfx__, __gff__, __label__, __map__, __sfx__,
|
|
3
|
+
// __music__); the Lua is source, not machine code. So "build" here = assemble a valid
|
|
4
|
+
// .p8 from the given Lua (+ optional data sections) that FAKE-08 can load and run.
|
|
5
|
+
//
|
|
6
|
+
// Accepts either a full .p8 already (passed through / validated) or bare Lua source
|
|
7
|
+
// (wrapped into a minimal but valid .p8). Data sections can be supplied verbatim.
|
|
8
|
+
|
|
9
|
+
const P8_HEADER = "pico-8 cartridge // http://www.pico-8.com";
|
|
10
|
+
const KNOWN_SECTIONS = ["lua", "gfx", "gff", "label", "map", "sfx", "music"];
|
|
11
|
+
|
|
12
|
+
/** Is this text already a full .p8 cart (has the header + a __lua__ section)? */
|
|
13
|
+
export function isP8Cart(text) {
|
|
14
|
+
return /^pico-8 cartridge/i.test(text.trimStart()) && /(^|\n)__lua__/.test(text);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Assemble a .p8 cart.
|
|
19
|
+
* @param {object} opts
|
|
20
|
+
* @param {string} opts.lua Lua source for the __lua__ section (required unless `p8` given).
|
|
21
|
+
* @param {string} [opts.p8] A complete .p8 already — validated + passed through.
|
|
22
|
+
* @param {number} [opts.version] PICO-8 format version line (default 18).
|
|
23
|
+
* @param {Object<string,string>} [opts.sections] Extra sections by name → body text
|
|
24
|
+
* (e.g. { gfx: "...", map: "...", sfx: "...", music: "..." }). Bodies are the raw
|
|
25
|
+
* hex/number rows PICO-8 uses; passed through verbatim.
|
|
26
|
+
* @returns {{ text: string, bytes: Uint8Array, warnings: string[] }}
|
|
27
|
+
*/
|
|
28
|
+
export function packP8({ lua, p8, version = 18, sections = {} } = {}) {
|
|
29
|
+
const warnings = [];
|
|
30
|
+
|
|
31
|
+
if (p8 != null) {
|
|
32
|
+
if (!isP8Cart(p8)) {
|
|
33
|
+
throw new Error("pico8 build: `p8` was given but doesn't look like a .p8 cart (missing 'pico-8 cartridge' header or __lua__ section).");
|
|
34
|
+
}
|
|
35
|
+
const text = p8.endsWith("\n") ? p8 : p8 + "\n";
|
|
36
|
+
return { text, bytes: new TextEncoder().encode(text), warnings };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (typeof lua !== "string" || lua.trim() === "") {
|
|
40
|
+
throw new Error("pico8 build: provide `lua` (the cart's Lua source) or a full `p8`.");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Validate any extra section names.
|
|
44
|
+
for (const name of Object.keys(sections)) {
|
|
45
|
+
if (!KNOWN_SECTIONS.includes(name)) {
|
|
46
|
+
warnings.push(`unknown .p8 section '__${name}__' — passing through, but PICO-8/FAKE-08 may ignore it.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const parts = [P8_HEADER, `version ${version}`, "__lua__", lua.replace(/\n$/, "")];
|
|
51
|
+
// Emit data sections in canonical order (skip __lua__, handled above).
|
|
52
|
+
for (const name of KNOWN_SECTIONS) {
|
|
53
|
+
if (name === "lua") continue;
|
|
54
|
+
if (sections[name] != null) {
|
|
55
|
+
parts.push(`__${name}__`, String(sections[name]).replace(/\n$/, ""));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Pass through any unknown sections last (so nothing the caller sent is dropped).
|
|
59
|
+
for (const [name, body] of Object.entries(sections)) {
|
|
60
|
+
if (!KNOWN_SECTIONS.includes(name)) {
|
|
61
|
+
parts.push(`__${name}__`, String(body).replace(/\n$/, ""));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const text = parts.join("\n") + "\n";
|
|
66
|
+
return { text, bytes: new TextEncoder().encode(text), warnings };
|
|
67
|
+
}
|