// Offline scenery station dumper. // // Loads an A2.SD* scenery file and runs the port's actual scenery // VM (port/sceneryVm.c) over it -- same dispatcher, same advance // counts, same SceneryOpInvalid termination -- to collect every ADF // ($05), NAV ($1D), and COM ($1E) station record reachable from // every entry point in the FS2 content region. // // Differs from extractstations.c (the original hand-rolled walker) // in two ways: // // 1. Uses the same code path the running game would, so any future // changes to the dispatcher are picked up here automatically. // 2. Records are dispatched through a callback instead of a global // array, so the tool can dedupe / filter inline. // // Stubs out renderer/palette since we don't draw anything on a station // scan -- those calls only fire on $06 (DrawLine) and $12 (SetColor) // records and have no bearing on station extraction. #include #include #include #include #include "sceneryVm.h" #include "palette.h" typedef struct CollectedT { SceneryStationT station; char nameBuf[16]; } CollectedT; #define MAX_COLLECTED 8192 static CollectedT collected[MAX_COLLECTED]; static int collectedCount; // BCD validation -- same rules as extractstations.c, lifted from // chunk5 DecodeBCDFreqString and chunk3 LookupADFStation. static int adfFreqValid(uint16_t freq); static int bcdNibblesValid(uint8_t b); static int comFreqValid(uint16_t freq); static int navFreqValid(uint16_t freq); static void onStation(SceneryStateT *state, const SceneryStationT *s); static int processFile(const char *path); static int bcdNibblesValid(uint8_t b) { return ((b & 0x0F) <= 0x09) && ((b >> 4) <= 0x09); } static int adfFreqValid(uint16_t freq) { uint8_t lo = (uint8_t)(freq & 0xFF); uint8_t hi = (uint8_t)(freq >> 8); if (!bcdNibblesValid(lo)) { return 0; } if ((hi & 0xF0) != 0x00 || (hi & 0x0F) > 0x09) { return 0; } int khz = (hi & 0x0F) * 100 + ((lo >> 4) * 10) + (lo & 0x0F); return (khz >= 200 && khz <= 999); } static int navFreqValid(uint16_t freq) { uint8_t lo = (uint8_t)(freq & 0xFF); uint8_t hi = (uint8_t)(freq >> 8); if (!bcdNibblesValid(lo) || !bcdNibblesValid(hi)) { return 0; } if (hi == 0x10) { return lo >= 0x80; } if (hi == 0x11) { return lo <= 0x79; } return 0; } static int comFreqValid(uint16_t freq) { uint8_t lo = (uint8_t)(freq & 0xFF); uint8_t hi = (uint8_t)(freq >> 8); if (!bcdNibblesValid(lo) || !bcdNibblesValid(hi)) { return 0; } if (hi == 0x11) { return lo >= 0x80; } if (hi == 0x12) { return 1; } if (hi == 0x13) { return lo <= 0x69; } return 0; } static void onStation(SceneryStateT *state, const SceneryStationT *s) { (void)state; if (collectedCount >= MAX_COLLECTED) { return; } switch (s->type) { case SCENERY_STATION_ADF: if (!adfFreqValid(s->freq)) return; break; case SCENERY_STATION_NAV: if (!navFreqValid(s->freq)) return; break; case SCENERY_STATION_COM: if (!comFreqValid(s->freq)) return; break; } CollectedT *c = &collected[collectedCount++]; c->station = *s; c->nameBuf[0] = '\0'; if (s->name != NULL) { size_t n = strlen(s->name); if (n >= sizeof(c->nameBuf)) { n = sizeof(c->nameBuf) - 1; } memcpy(c->nameBuf, s->name, n); c->nameBuf[n] = '\0'; c->station.name = c->nameBuf; } } static int processFile(const char *path) { FILE *f = fopen(path, "rb"); if (f == NULL) { fprintf(stderr, "cannot open %s\n", path); return 0; } fseek(f, 0, SEEK_END); long sz = ftell(f); fseek(f, 0, SEEK_SET); uint8_t *buf = malloc((size_t)sz); if (buf == NULL || fread(buf, 1, (size_t)sz, f) != (size_t)sz) { fclose(f); free(buf); return 0; } fclose(f); // Use a heap-allocated state to keep the 200KB visited array // off the stack. SceneryStateT *state = malloc(sizeof(SceneryStateT)); if (state == NULL) { free(buf); return 0; } sceneryInit(state, buf, (uint32_t)sz, NULL); sceneryAttachStationCb(state, onStation, NULL); int before = collectedCount; // Walk every byte in the FS2 content region. The cycle guard // inside sceneryRun makes this O(file size). for (uint32_t entry = 0x2000; entry < (uint32_t)sz; entry++) { sceneryWalkFrom(state, entry); } int adf = 0; int nav = 0; int com = 0; for (int i = before; i < collectedCount; i++) { switch (collected[i].station.type) { case SCENERY_STATION_ADF: adf++; break; case SCENERY_STATION_NAV: nav++; break; case SCENERY_STATION_COM: com++; break; } } printf("%-40s ADF=%d NAV=%d COM=%d\n", path, adf, nav, com); free(state); free(buf); return 1; } int main(int argc, char **argv) { if (argc < 2) { fprintf(stderr, "usage: %s file.SD [file.SD ...]\n", argv[0]); return 1; } int adf = 0; int nav = 0; int com = 0; for (int i = 1; i < argc; i++) { if (!processFile(argv[i])) { continue; } } for (int i = 0; i < collectedCount; i++) { switch (collected[i].station.type) { case SCENERY_STATION_ADF: adf++; break; case SCENERY_STATION_NAV: nav++; break; case SCENERY_STATION_COM: com++; break; } } printf("\ntotal: %d ADF, %d NAV, %d COM\n", adf, nav, com); for (int i = 0; i < collectedCount; i++) { const SceneryStationT *s = &collected[i].station; printf(" %c freq=$%04X x=%d y=%d z=%d %s\n", (char)s->type, s->freq, s->x, s->y, s->z, s->name != NULL ? s->name : ""); } return 0; } // --- Stubs so we don't have to link the SDL-driven renderer --- void rendererSetDrawColor(struct RenderStateT *state, ColorE color) { (void)state; (void)color; } void rendererSetHiresColor(struct RenderStateT *state, uint8_t hiresCode) { (void)state; (void)hiresCode; } void rendererDrawLine(struct RenderStateT *state, int16_t x1, int16_t y1, int16_t x2, int16_t y2) { (void)state; (void)x1; (void)y1; (void)x2; (void)y2; } void rendererDrawColorSpan(struct RenderStateT *state, int16_t xRight, int16_t length, int16_t y) { (void)state; (void)xRight; (void)length; (void)y; } void rendererFillPolygon(struct RenderStateT *state, const int16_t *xs, const int16_t *ys, int count) { (void)state; (void)xs; (void)ys; (void)count; } ColorE paletteFromSceneryCode(uint8_t code) { (void)code; return COLOR_BLACK; } // sceneryAttachCamera (in sceneryVm.c) calls into camera.c -- stub // since station scans don't need a real camera matrix. void cameraGet2x3Matrix(const struct CameraT *cam, int8_t outRowX[3], int8_t outRowZ[3]) { (void)cam; outRowX[0] = 0; outRowX[1] = 0; outRowX[2] = 0; outRowZ[0] = 0; outRowZ[1] = 0; outRowZ[2] = 0; }