// mkstlevel: convert a human-readable text level definition into the // STL2 binary format read by examples/spacetaxi/stLevel.c. // // Usage: // mkstlevel input.txt output.dat // // Input format (.txt, line-oriented, comments start with '#'): // // @name // @tilebank <0..255> // @music <0..255> // @bgColor // @borderColor // @taxiSpawn // @accel [optional, default 14 14] // @gravity [optional, default 0 1; yGrav signed] // @vicColor [optional, default all 0] // @pad [up to 10 of these] // @fare [up to 16 of these] // @tilemap -- followed by exactly 25 lines of 40 chars each // @colormap -- followed by exactly 25 lines of 40 chars each // // Tilemap / colormap character encoding (chars are tile/palette indices): // '.' or ' ' -> 0 (background) // '0'..'9' -> 0..9 // 'A'..'Z' -> 10..35 // 'a'..'z' -> 36..61 // // Per the spacetaxi engine convention: // tile index 0 = empty (non-solid) // tile index 1..63 = solid (walls) // tile index 64..127 = landing-pad surface // tile index 128..255 = decorative non-solid // // Color cells use the same character->index decoding; cell values are // palette slots 0..15. Use '0'..'9','a'..'f' for the standard 16 // surface palettes. #include #include #include #include #include #define ST_TILEMAP_W 40 #define ST_PLAYFIELD_ROWS 25 #define ST_MAX_PADS 10 #define ST_MAX_FARES 16 #define ST_NAME_MAX 23 #define LINE_MAX 256 typedef struct { uint8_t letter; uint8_t tileX; uint8_t tileY; uint8_t tileW; } PadT; typedef struct { uint8_t spawnPad; uint8_t destPad; } FareT; typedef struct { char name[ST_NAME_MAX + 1]; uint8_t tileBank; uint8_t music; uint8_t bgColor; uint8_t borderColor; uint8_t taxiSpawnX; uint8_t taxiSpawnY; uint8_t xAccel; uint8_t yAccel; int8_t xGrav; int8_t yGrav; uint8_t bgColor1; uint8_t bgColor2; uint8_t bgColor3; uint8_t spriteMc0; uint8_t spriteMc1; uint8_t sprite0Color; uint8_t sprite1Color; uint8_t padCount; PadT pads[ST_MAX_PADS]; uint8_t fareCount; FareT fares[ST_MAX_FARES]; uint8_t tilemap[ST_TILEMAP_W * ST_PLAYFIELD_ROWS]; uint8_t colormap[ST_TILEMAP_W * ST_PLAYFIELD_ROWS]; } LevelT; static int charToIndex(int c, int *outIdx); static int findPadByLetter(const LevelT *L, uint8_t letter); static int parseGrid(FILE *fp, const char *gridName, uint8_t *outCells, int *outLine); static int parseLevel(FILE *fp, LevelT *out); static int readLine(FILE *fp, char *buf, int n); static int stripLine(char *s); static int writeOut(const LevelT *L, const char *path); static int charToIndex(int c, int *outIdx) { if (c == '.' || c == ' ') { *outIdx = 0; return 1; } if (c >= '0' && c <= '9') { *outIdx = c - '0'; return 1; } if (c >= 'A' && c <= 'Z') { *outIdx = 10 + (c - 'A'); return 1; } if (c >= 'a' && c <= 'z') { *outIdx = 36 + (c - 'a'); return 1; } return 0; } static int findPadByLetter(const LevelT *L, uint8_t letter) { uint8_t i; for (i = 0; i < L->padCount; i++) { if (L->pads[i].letter == letter) { return (int)i; } } return -1; } // Reads one line; returns >0 on success, 0 on EOF, -1 on error. // Strips trailing newline. Lines starting with '#' are returned with // the leading '#' so the caller can decide whether to skip them. static int readLine(FILE *fp, char *buf, int n) { int c; int k = 0; while (k < n - 1) { c = fgetc(fp); if (c == EOF) { buf[k] = '\0'; return (k == 0) ? 0 : 1; } if (c == '\n') { break; } if (c == '\r') { continue; // tolerate CRLF } buf[k++] = (char)c; } buf[k] = '\0'; return 1; } // Strip leading whitespace; return 1 if blank-or-comment, 0 if data. static int stripLine(char *s) { int i = 0; int j = 0; while (s[i] == ' ' || s[i] == '\t') { i++; } if (s[i] == '\0' || s[i] == '#') { return 1; } while (s[i] != '\0') { s[j++] = s[i++]; } s[j] = '\0'; return 0; } static int parseGrid(FILE *fp, const char *gridName, uint8_t *outCells, int *outLine) { char line[LINE_MAX]; int row; int col; int idx; for (row = 0; row < ST_PLAYFIELD_ROWS; row++) { if (readLine(fp, line, sizeof(line)) <= 0) { fprintf(stderr, "mkstlevel: %s: unexpected EOF on row %d\n", gridName, row); return 0; } (*outLine)++; // Strip CRLF/trailing space but DON'T treat indent as significant; // do NOT strip leading whitespace within the grid (chars are literal). { int len = (int)strlen(line); while (len > 0 && (line[len-1] == ' ' || line[len-1] == '\t')) { line[--len] = '\0'; } // Allow a leading '#' comment line inside the grid only if it // appears in the first column; otherwise treat as data error. if (line[0] == '#') { row--; // re-run this slot continue; } if (len == 0) { row--; // skip blank lines silently continue; } if (len != ST_TILEMAP_W) { fprintf(stderr, "mkstlevel: %s line %d: expected %d chars, got %d (\"%s\")\n", gridName, *outLine, ST_TILEMAP_W, len, line); return 0; } } for (col = 0; col < ST_TILEMAP_W; col++) { if (!charToIndex((unsigned char)line[col], &idx)) { fprintf(stderr, "mkstlevel: %s line %d col %d: bad char '%c' (0x%02X)\n", gridName, *outLine, col + 1, line[col], (unsigned)(unsigned char)line[col]); return 0; } outCells[row * ST_TILEMAP_W + col] = (uint8_t)idx; } } return 1; } static int parseLevel(FILE *fp, LevelT *out) { char line[LINE_MAX]; int lineNo = 0; int gotTilemap = 0; int gotColormap = 0; memset(out, 0, sizeof(*out)); out->bgColor = 0; out->borderColor = 0; // Physics defaults match level 1's templates so hand-authored // levels without an @accel/@gravity directive still play sanely. out->xAccel = 14; out->yAccel = 14; out->xGrav = 0; out->yGrav = 1; while (readLine(fp, line, sizeof(line)) > 0) { lineNo++; if (stripLine(line)) { continue; } if (line[0] != '@') { fprintf(stderr, "mkstlevel: line %d: expected @directive or comment, got \"%s\"\n", lineNo, line); return 0; } if (strncmp(line, "@name ", 6) == 0) { int len = (int)strlen(line + 6); if (len > ST_NAME_MAX) { len = ST_NAME_MAX; } memcpy(out->name, line + 6, (size_t)len); out->name[len] = '\0'; } else if (strncmp(line, "@tilebank ", 10) == 0) { out->tileBank = (uint8_t)atoi(line + 10); } else if (strncmp(line, "@music ", 7) == 0) { out->music = (uint8_t)atoi(line + 7); } else if (strncmp(line, "@bgColor ", 9) == 0) { out->bgColor = (uint8_t)atoi(line + 9); } else if (strncmp(line, "@borderColor ", 13) == 0) { out->borderColor = (uint8_t)atoi(line + 13); } else if (strncmp(line, "@taxiSpawn ", 11) == 0) { int x = 0; int y = 0; if (sscanf(line + 11, "%d %d", &x, &y) != 2) { fprintf(stderr, "mkstlevel: line %d: bad taxiSpawn\n", lineNo); return 0; } out->taxiSpawnX = (uint8_t)x; out->taxiSpawnY = (uint8_t)y; } else if (strncmp(line, "@accel ", 7) == 0) { int ax = 0; int ay = 0; if (sscanf(line + 7, "%d %d", &ax, &ay) != 2) { fprintf(stderr, "mkstlevel: line %d: bad accel\n", lineNo); return 0; } out->xAccel = (uint8_t)ax; out->yAccel = (uint8_t)ay; } else if (strncmp(line, "@gravity ", 9) == 0) { int gx = 0; int gy = 0; if (sscanf(line + 9, "%d %d", &gx, &gy) != 2) { fprintf(stderr, "mkstlevel: line %d: bad gravity\n", lineNo); return 0; } out->xGrav = (int8_t)gx; out->yGrav = (int8_t)gy; } else if (strncmp(line, "@vicColor ", 10) == 0) { int b1 = 0; int b2 = 0; int b3 = 0; int mc0 = 0; int mc1 = 0; int sc0 = 0; int sc1 = 0; if (sscanf(line + 10, "%d %d %d %d %d %d %d", &b1, &b2, &b3, &mc0, &mc1, &sc0, &sc1) != 7) { fprintf(stderr, "mkstlevel: line %d: bad vicColor (need 7 values)\n", lineNo); return 0; } out->bgColor1 = (uint8_t)b1; out->bgColor2 = (uint8_t)b2; out->bgColor3 = (uint8_t)b3; out->spriteMc0 = (uint8_t)mc0; out->spriteMc1 = (uint8_t)mc1; out->sprite0Color = (uint8_t)sc0; out->sprite1Color = (uint8_t)sc1; } else if (strncmp(line, "@pad ", 5) == 0) { char letter = '\0'; int px = 0; int py = 0; int pw = 0; if (sscanf(line + 5, " %c %d %d %d", &letter, &px, &py, &pw) != 4) { fprintf(stderr, "mkstlevel: line %d: bad pad\n", lineNo); return 0; } if (out->padCount >= ST_MAX_PADS) { fprintf(stderr, "mkstlevel: line %d: too many pads (max %d)\n", lineNo, ST_MAX_PADS); return 0; } out->pads[out->padCount].letter = (uint8_t)letter; out->pads[out->padCount].tileX = (uint8_t)px; out->pads[out->padCount].tileY = (uint8_t)py; out->pads[out->padCount].tileW = (uint8_t)pw; out->padCount++; } else if (strncmp(line, "@fare ", 6) == 0) { char src = '\0'; char dst = '\0'; int srcIdx; int dstIdx; if (sscanf(line + 6, " %c %c", &src, &dst) != 2) { fprintf(stderr, "mkstlevel: line %d: bad fare\n", lineNo); return 0; } srcIdx = findPadByLetter(out, (uint8_t)src); dstIdx = findPadByLetter(out, (uint8_t)dst); if (srcIdx < 0 || dstIdx < 0) { fprintf(stderr, "mkstlevel: line %d: fare references unknown pad (src '%c' dst '%c')\n", lineNo, src, dst); return 0; } if (out->fareCount >= ST_MAX_FARES) { fprintf(stderr, "mkstlevel: line %d: too many fares (max %d)\n", lineNo, ST_MAX_FARES); return 0; } out->fares[out->fareCount].spawnPad = (uint8_t)srcIdx; out->fares[out->fareCount].destPad = (uint8_t)dstIdx; out->fareCount++; } else if (strcmp(line, "@tilemap") == 0) { if (!parseGrid(fp, "tilemap", out->tilemap, &lineNo)) { return 0; } gotTilemap = 1; } else if (strcmp(line, "@colormap") == 0) { if (!parseGrid(fp, "colormap", out->colormap, &lineNo)) { return 0; } gotColormap = 1; } else { fprintf(stderr, "mkstlevel: line %d: unknown directive \"%s\"\n", lineNo, line); return 0; } } if (out->name[0] == '\0') { fprintf(stderr, "mkstlevel: missing @name\n"); return 0; } if (!gotTilemap) { fprintf(stderr, "mkstlevel: missing @tilemap\n"); return 0; } if (!gotColormap) { fprintf(stderr, "mkstlevel: missing @colormap\n"); return 0; } if (out->padCount == 0) { fprintf(stderr, "mkstlevel: at least one @pad required\n"); return 0; } return 1; } static int writeOut(const LevelT *L, const char *path) { FILE *fp; uint8_t nameLen; uint8_t i; size_t cells; fp = fopen(path, "wb"); if (fp == NULL) { fprintf(stderr, "mkstlevel: cannot open %s for write\n", path); return 0; } if (fputc('S', fp) == EOF || fputc('T', fp) == EOF || fputc('L', fp) == EOF || fputc('2', fp) == EOF) { goto fail; } nameLen = (uint8_t)strlen(L->name); if (fputc(nameLen, fp) == EOF) { goto fail; } if (fwrite(L->name, 1, nameLen, fp) != nameLen) { goto fail; } if (fputc(L->tileBank, fp) == EOF || fputc(L->music, fp) == EOF || fputc(L->bgColor, fp) == EOF || fputc(L->borderColor, fp) == EOF || fputc(L->taxiSpawnX, fp) == EOF || fputc(L->taxiSpawnY, fp) == EOF) { goto fail; } if (fputc(L->xAccel, fp) == EOF || fputc(L->yAccel, fp) == EOF || fputc((uint8_t)L->xGrav, fp) == EOF || fputc((uint8_t)L->yGrav, fp) == EOF) { goto fail; } if (fputc(L->bgColor1, fp) == EOF || fputc(L->bgColor2, fp) == EOF || fputc(L->bgColor3, fp) == EOF || fputc(L->spriteMc0, fp) == EOF || fputc(L->spriteMc1, fp) == EOF || fputc(L->sprite0Color, fp) == EOF || fputc(L->sprite1Color, fp) == EOF) { goto fail; } if (fputc(L->padCount, fp) == EOF) { goto fail; } for (i = 0; i < L->padCount; i++) { if (fputc(L->pads[i].letter, fp) == EOF || fputc(L->pads[i].tileX, fp) == EOF || fputc(L->pads[i].tileY, fp) == EOF || fputc(L->pads[i].tileW, fp) == EOF) { goto fail; } } if (fputc(L->fareCount, fp) == EOF) { goto fail; } for (i = 0; i < L->fareCount; i++) { if (fputc(L->fares[i].spawnPad, fp) == EOF || fputc(L->fares[i].destPad, fp) == EOF) { goto fail; } } cells = (size_t)ST_TILEMAP_W * ST_PLAYFIELD_ROWS; if (fwrite(L->tilemap, 1, cells, fp) != cells) { goto fail; } if (fwrite(L->colormap, 1, cells, fp) != cells) { goto fail; } fclose(fp); return 1; fail: fclose(fp); fprintf(stderr, "mkstlevel: write error\n"); return 0; } int main(int argc, char **argv) { FILE *fp; LevelT L; if (argc != 3) { fprintf(stderr, "usage: %s input.txt output.dat\n", argv[0]); return 2; } fp = fopen(argv[1], "r"); if (fp == NULL) { fprintf(stderr, "mkstlevel: cannot open %s\n", argv[1]); return 1; } if (!parseLevel(fp, &L)) { fclose(fp); return 1; } fclose(fp); if (!writeOut(&L, argv[2])) { return 1; } printf("mkstlevel: wrote %s (name=\"%s\", pads=%u, fares=%u)\n", argv[2], L.name, (unsigned)L.padCount, (unsigned)L.fareCount); return 0; }