// Host-side test for examples/agi/agiPic.c. // // Loads a single PIC resource from a game directory, decodes it, and // writes two image files: a color PPM for the visual plane (palette // expanded to 8-bit RGB) and a grayscale PGM for the priority plane. // Also reports the byte count and pixel coverage so a regression in // the decoder shows up as a numeric diff. // // Usage: // testAgiPic [pic-id] // // The default pic-id is the first non-empty entry in PICDIR; KQ3 has // id 0 unused so the loader hops forward until it finds one. // // Exit codes: // 0 decoded; outputs written // 1 game directory missing, PIC slot empty, or decode failed // 2 invalid command line #include "agi.h" #include #include #include // 4-bit channel -> 8-bit channel scaling: x * 17 maps 0..15 to 0..255 // without bias (15 * 17 = 255 exactly). #define CHANNEL_SCALE 17 // ----- Prototypes ----- static int16_t firstNonEmptyPic(const AgiGameT *game); static bool writePgm(const char *path, const uint8_t *plane); static bool writePpm(const char *path, const uint8_t *plane); // ----- Helpers (alphabetical) ----- static int16_t firstNonEmptyPic(const AgiGameT *game) { uint16_t count; uint16_t i; count = game->resCount[AGI_RES_PIC]; for (i = 0u; i < count; i++) { if (game->resDir[AGI_RES_PIC][i].volume < AGI_MAX_VOLUMES) { return (int16_t)i; } } return -1; } static bool writePgm(const char *path, const uint8_t *plane) { FILE *fp; uint32_t i; uint8_t gray; fp = fopen(path, "wb"); if (fp == NULL) { return false; } fprintf(fp, "P5\n%d %d\n255\n", AGI_PIC_WIDTH, AGI_PIC_HEIGHT); for (i = 0u; i < (uint32_t)AGI_PIC_PIXELS; i++) { gray = (uint8_t)((plane[i] & 0x0Fu) * CHANNEL_SCALE); fputc(gray, fp); } fclose(fp); return true; } static bool writePpm(const char *path, const uint8_t *plane) { FILE *fp; uint32_t i; uint16_t rgb; uint8_t r; uint8_t g; uint8_t b; fp = fopen(path, "wb"); if (fp == NULL) { return false; } fprintf(fp, "P6\n%d %d\n255\n", AGI_PIC_WIDTH, AGI_PIC_HEIGHT); for (i = 0u; i < (uint32_t)AGI_PIC_PIXELS; i++) { rgb = kAgiPalette[plane[i] & 0x0Fu]; r = (uint8_t)(((rgb >> 8) & 0x0Fu) * CHANNEL_SCALE); g = (uint8_t)(((rgb >> 4) & 0x0Fu) * CHANNEL_SCALE); b = (uint8_t)((rgb & 0x0Fu) * CHANNEL_SCALE); fputc(r, fp); fputc(g, fp); fputc(b, fp); } fclose(fp); return true; } // ----- main ----- int main(int argc, char **argv) { AgiGameT game; AgiPicT pic; int16_t picId; uint8_t *bytes; uint16_t length; uint32_t nonBgVisual; uint32_t nonBgPriority; uint32_t i; char visPath[256]; char priPath[256]; if (argc < 2 || argc > 3) { fprintf(stderr, "usage: %s [pic-id]\n", argv[0]); return 2; } if (!agiResOpen(&game, argv[1])) { fprintf(stderr, "FAIL: agiResOpen rejected '%s'\n", argv[1]); return 1; } if (argc == 3) { picId = (int16_t)atoi(argv[2]); } else { picId = firstNonEmptyPic(&game); } if (picId < 0 || (uint16_t)picId >= game.resCount[AGI_RES_PIC]) { fprintf(stderr, "FAIL: no PIC at id %d\n", (int)picId); agiResClose(&game); return 1; } bytes = agiResLoad(&game, AGI_RES_PIC, (uint16_t)picId, &length); if (bytes == NULL) { fprintf(stderr, "FAIL: PIC %d failed to load\n", (int)picId); agiResClose(&game); return 1; } if (!agiPicAlloc(&pic)) { fprintf(stderr, "FAIL: agiPicAlloc out of memory\n"); free(bytes); agiResClose(&game); return 1; } agiPicClear(&pic); if (!agiPicDecode(&pic, bytes, length)) { fprintf(stderr, "FAIL: PIC %d decode rejected (malformed stream?)\n", (int)picId); agiPicFree(&pic); free(bytes); agiResClose(&game); return 1; } nonBgVisual = 0u; nonBgPriority = 0u; for (i = 0u; i < (uint32_t)AGI_PIC_PIXELS; i++) { if (pic.visual[i] != AGI_VISUAL_BG) { nonBgVisual++; } if (pic.priority[i] != AGI_PRIORITY_BG) { nonBgPriority++; } } snprintf(visPath, sizeof(visPath), "build/tests/pic%d_visual.ppm", (int)picId); snprintf(priPath, sizeof(priPath), "build/tests/pic%d_priority.pgm", (int)picId); if (!writePpm(visPath, pic.visual) || !writePgm(priPath, pic.priority)) { fprintf(stderr, "FAIL: could not write output images\n"); agiPicFree(&pic); free(bytes); agiResClose(&game); return 1; } printf("PIC %d: %u bytes, visual %u/%u px painted, priority %u/%u px painted\n", (int)picId, (unsigned)length, (unsigned)nonBgVisual, (unsigned)AGI_PIC_PIXELS, (unsigned)nonBgPriority, (unsigned)AGI_PIC_PIXELS); printf(" wrote %s\n", visPath); printf(" wrote %s\n", priPath); agiPicFree(&pic); free(bytes); agiResClose(&game); return 0; }