joeylib2/examples/spacetaxi/mkstlevel/mkstlevel.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;
}