fs2port/port/tools/extractFS2Sections.c
2026-05-13 21:32:05 -05:00

207 lines
8.7 KiB
C

// Walk the FS2.1 ProDOS scenery file and emit each section as a
// separate clean bytecode blob. No 6502 emulation, no 64K dump
// scaffolding. Just file -> per-section files.
//
// A "section" in chunk5 terms is a sequence of scenery opcodes loaded
// by a $0D HEADER. We don't have an explicit section table, so we
// scan for plausible section starts ($13 cull or $21 cull at sector
// boundaries) and walk forward until we hit a stream terminator
// ($79+, $80+, or an invalid opcode). For each one we emit:
//
// out_section_<offset>.bin -- raw bytes from start to terminator
//
// This is a best-effort static dump. The output isn't guaranteed to
// be a complete chunk5 section (some have $0B back-jumps that
// reference yet other regions of the file), but it gives us clean
// chunks to inspect, diff, and load into the port without the boot
// machinery overhead.
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int isTerminator(uint8_t op) {
// chunk5 dispatcher: opcodes with bit 7 set or >= $46 fall off
// the SceneryOpcodeTable and reset the stream.
if (op & 0x80) return 1;
if (op >= 0x46) return 1;
// Specific $08/$0C/$0F/$10/$15-17/$1F/$26-27/$2A/$2C-2E/$30/
// $34/$36-3F/$43-45 are SceneryOpInvalid and also terminate.
switch (op) {
case 0x08: case 0x0C: case 0x0F: case 0x10:
case 0x15: case 0x16: case 0x17:
case 0x1F:
case 0x26: case 0x27: case 0x2A:
case 0x2C: case 0x2D: case 0x2E:
case 0x30:
case 0x34:
case 0x36: case 0x37: case 0x38: case 0x39:
case 0x3B: case 0x3C: case 0x3D: case 0x3E: case 0x3F:
case 0x43: case 0x44: case 0x45:
return 1;
}
return 0;
}
// Static-walk advance for each opcode. Returns 0 if we should stop
// (terminator or unknown that needs more state). Best-effort -- no
// full state machine.
static int advanceLength(uint8_t op, const uint8_t *p, size_t avail) {
switch (op) {
case 0x00: case 0x01: case 0x02:
case 0x40: case 0x41:
return 5; // emit-vertex 5-byte
case 0x42:
return 6; // refresh-cache: op + idx + 4 vertex bytes
case 0x03:
return 6; // SceneryOpCall64K_2
case 0x04:
// CullByOutcodeList: walks vertex idx bytes
// until terminator with bit 7 set.
{
size_t off = 3;
while (off < avail) {
if (p[off++] & 0x80) break;
}
return (int)off;
}
case 0x05: // ADF station
return 9;
case 0x06: // DrawLine
return 5;
case 0x07: // EnterLocalFrame
return 14;
case 0x09: case 0x0A: // SKIP3
return 3;
case 0x0B: // REL_JUMP -- can't follow statically; stop
return 0;
case 0x0D: // HEADER
return 6;
case 0x0E:
return 1;
case 0x11: // SKIP_1
return 1;
case 0x12: // SetColor
return 2;
case 0x13: case 0x14: // JumpIfBeyondXY/XYZ -- consume but don't follow
return 9;
case 0x18: // SUB_INVOKE -- can't follow statically; stop
return 0;
case 0x19: // RETURN
return 0;
case 0x1A: // WriteWord
return 5;
case 0x1B: case 0x1C: // ModeWhite / DayOnly
return 1;
case 0x1D: // NAV station
return 11;
case 0x1E: // COM record (variable length, byte 1 = total)
if (avail < 2) return 0;
return p[1];
case 0x20: // CullN_1
return 9;
case 0x21: // CullN_2
return 15;
case 0x22: // CullN_3
return 21;
case 0x23: // JumpIfBitsClear
return 7;
case 0x24: // PushOriginWithStash
return 8;
case 0x25: // StoreImmWord
return 5;
case 0x28: // JumpIfWordCompare
return 8;
case 0x29: // CopyToD2
return 1;
case 0x2B: // EmitCurve (best-effort)
return 12;
case 0x2F: // ResetState
return 1;
case 0x31: case 0x32: case 0x33:
case 0x35:
return 2;
}
return 0; // unknown / can't advance
}
int main(int argc, char **argv) {
if (argc != 3) {
fprintf(stderr, "usage: %s <fs2.1 file> <out dir>\n", argv[0]);
return 1;
}
FILE *f = fopen(argv[1], "rb");
if (f == NULL) { perror(argv[1]); return 1; }
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
uint8_t *buf = malloc(size);
fread(buf, 1, size, f);
fclose(f);
printf("FS2.1: %ld bytes\n", size);
int sectionsFound = 0;
int reachedSection = 0;
long off = 0;
while (off < size) {
// Plausible section start: $13 ($14) cull or $21
// ($20/$22) cull at a 256-byte boundary.
uint8_t op = buf[off];
int sectorBoundary = (off & 0xFF) == 0;
int looksLikeSection = sectorBoundary && (
op == 0x13 || op == 0x14 ||
op == 0x20 || op == 0x21 || op == 0x22);
if (!looksLikeSection) {
off++;
continue;
}
// Walk forward, counting bytes until terminator.
long start = off;
long polygonOps = 0;
long opCount = 0;
while (off < size) {
uint8_t cur = buf[off];
if (isTerminator(cur)) break;
int len = advanceLength(cur, &buf[off], (size_t)(size - off));
if (len <= 0) {
// $0B / $18 / $19 -- can't follow.
break;
}
opCount++;
if (cur == 0x00 || cur == 0x40 || cur == 0x41) {
polygonOps++;
}
if (off + len > size) break;
off += len;
}
long sectionLen = off - start;
if (sectionLen >= 16 && polygonOps > 0) {
char name[256];
snprintf(name, sizeof(name),
"%s/section_%05lX_polyops%ld_oplen%ld.bin",
argv[2], start, polygonOps, opCount);
FILE *out = fopen(name, "wb");
if (out != NULL) {
fwrite(&buf[start], 1, sectionLen, out);
fclose(out);
printf(" $%05lX: %ld bytes, %ld ops, %ld polygon emits -> %s\n",
start, sectionLen, opCount, polygonOps, name);
if (polygonOps >= 5) reachedSection++;
}
sectionsFound++;
}
if (off == start) off++; // avoid infinite loop on stuck cursor
}
printf("\n%d candidate sections found, %d with >=5 polygon emits\n",
sectionsFound, reachedSection);
free(buf);
return 0;
}