499 lines
16 KiB
C
499 lines
16 KiB
C
// 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 <up-to-23-char level name>
|
|
// @tilebank <0..255>
|
|
// @music <0..255>
|
|
// @bgColor <palette slot 0..15>
|
|
// @borderColor <palette slot 0..15>
|
|
// @taxiSpawn <tileX> <tileY>
|
|
// @accel <xAccel> <yAccel> [optional, default 14 14]
|
|
// @gravity <xGrav> <yGrav> [optional, default 0 1; yGrav signed]
|
|
// @vicColor <bg1> <bg2> <bg3> <mc0> <mc1> <sc0> <sc1> [optional, default all 0]
|
|
// @pad <letter> <tileX> <tileY> <tileW> [up to 10 of these]
|
|
// @fare <spawnLetter> <destLetter> [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 <ctype.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#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;
|
|
}
|