Skip to content

Instantly share code, notes, and snippets.

@DanielGibson
Last active December 17, 2023 06:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DanielGibson/3b030d69baddd6c40529bd80f4c0ff8a to your computer and use it in GitHub Desktop.
Save DanielGibson/3b030d69baddd6c40529bd80f4c0ff8a to your computer and use it in GitHub Desktop.
Tool that converts Quake2 .wal to .png (needs stb_image_write.h), most probably won't work on Windows without some changes
// the Quake2 standard colormap/palette
static unsigned char colormap[256][3] = {
{0, 0, 0}, {15, 15, 15}, {31, 31, 31}, {47, 47, 47}, {63, 63, 63}, {75, 75, 75},
{91, 91, 91}, {107, 107, 107}, {123, 123, 123}, {139, 139, 139}, {155, 155, 155}, {171, 171, 171},
{187, 187, 187}, {203, 203, 203}, {219, 219, 219}, {235, 235, 235}, {99, 75, 35}, {91, 67, 31},
{83, 63, 31}, {79, 59, 27}, {71, 55, 27}, {63, 47, 23}, {59, 43, 23}, {51, 39, 19},
{47, 35, 19}, {43, 31, 19}, {39, 27, 15}, {35, 23, 15}, {27, 19, 11}, {23, 15, 11},
{19, 15, 7}, {15, 11, 7}, {95, 95, 111}, {91, 91, 103}, {91, 83, 95}, {87, 79, 91},
{83, 75, 83}, {79, 71, 75}, {71, 63, 67}, {63, 59, 59}, {59, 55, 55}, {51, 47, 47},
{47, 43, 43}, {39, 39, 39}, {35, 35, 35}, {27, 27, 27}, {23, 23, 23}, {19, 19, 19},
{143, 119, 83}, {123, 99, 67}, {115, 91, 59}, {103, 79, 47}, {207, 151, 75}, {167, 123, 59},
{139, 103, 47}, {111, 83, 39}, {235, 159, 39}, {203, 139, 35}, {175, 119, 31}, {147, 99, 27},
{119, 79, 23}, {91, 59, 15}, {63, 39, 11}, {35, 23, 7}, {167, 59, 43}, {159, 47, 35},
{151, 43, 27}, {139, 39, 19}, {127, 31, 15}, {115, 23, 11}, {103, 23, 7}, {87, 19, 0},
{75, 15, 0}, {67, 15, 0}, {59, 15, 0}, {51, 11, 0}, {43, 11, 0}, {35, 11, 0},
{27, 7, 0}, {19, 7, 0}, {123, 95, 75}, {115, 87, 67}, {107, 83, 63}, {103, 79, 59},
{95, 71, 55}, {87, 67, 51}, {83, 63, 47}, {75, 55, 43}, {67, 51, 39}, {63, 47, 35},
{55, 39, 27}, {47, 35, 23}, {39, 27, 19}, {31, 23, 15}, {23, 15, 11}, {15, 11, 7},
{111, 59, 23}, {95, 55, 23}, {83, 47, 23}, {67, 43, 23}, {55, 35, 19}, {39, 27, 15},
{27, 19, 11}, {15, 11, 7}, {179, 91, 79}, {191, 123, 111}, {203, 155, 147}, {215, 187, 183},
{203, 215, 223}, {179, 199, 211}, {159, 183, 195}, {135, 167, 183}, {115, 151, 167}, {91, 135, 155},
{71, 119, 139}, {47, 103, 127}, {23, 83, 111}, {19, 75, 103}, {15, 67, 91}, {11, 63, 83},
{7, 55, 75}, {7, 47, 63}, {7, 39, 51}, {0, 31, 43}, {0, 23, 31}, {0, 15, 19},
{0, 7, 11}, {0, 0, 0}, {139, 87, 87}, {131, 79, 79}, {123, 71, 71}, {115, 67, 67},
{107, 59, 59}, {99, 51, 51}, {91, 47, 47}, {87, 43, 43}, {75, 35, 35}, {63, 31, 31},
{51, 27, 27}, {43, 19, 19}, {31, 15, 15}, {19, 11, 11}, {11, 7, 7}, {0, 0, 0},
{151, 159, 123}, {143, 151, 115}, {135, 139, 107}, {127, 131, 99}, {119, 123, 95}, {115, 115, 87},
{107, 107, 79}, {99, 99, 71}, {91, 91, 67}, {79, 79, 59}, {67, 67, 51}, {55, 55, 43},
{47, 47, 35}, {35, 35, 27}, {23, 23, 19}, {15, 15, 11}, {159, 75, 63}, {147, 67, 55},
{139, 59, 47}, {127, 55, 39}, {119, 47, 35}, {107, 43, 27}, {99, 35, 23}, {87, 31, 19},
{79, 27, 15}, {67, 23, 11}, {55, 19, 11}, {43, 15, 7}, {31, 11, 7}, {23, 7, 0},
{11, 0, 0}, {0, 0, 0}, {119, 123, 207}, {111, 115, 195}, {103, 107, 183}, {99, 99, 167},
{91, 91, 155}, {83, 87, 143}, {75, 79, 127}, {71, 71, 115}, {63, 63, 103}, {55, 55, 87},
{47, 47, 75}, {39, 39, 63}, {35, 31, 47}, {27, 23, 35}, {19, 15, 23}, {11, 7, 7},
{155, 171, 123}, {143, 159, 111}, {135, 151, 99}, {123, 139, 87}, {115, 131, 75}, {103, 119, 67},
{95, 111, 59}, {87, 103, 51}, {75, 91, 39}, {63, 79, 27}, {55, 67, 19}, {47, 59, 11},
{35, 47, 7}, {27, 35, 0}, {19, 23, 0}, {11, 15, 0}, {0, 255, 0}, {35, 231, 15},
{63, 211, 27}, {83, 187, 39}, {95, 167, 47}, {95, 143, 51}, {95, 123, 51}, {255, 255, 255},
{255, 255, 211}, {255, 255, 167}, {255, 255, 127}, {255, 255, 83}, {255, 255, 39}, {255, 235, 31},
{255, 215, 23}, {255, 191, 15}, {255, 171, 7}, {255, 147, 0}, {239, 127, 0}, {227, 107, 0},
{211, 87, 0}, {199, 71, 0}, {183, 59, 0}, {171, 43, 0}, {155, 31, 0}, {143, 23, 0},
{127, 15, 0}, {115, 7, 0}, {95, 0, 0}, {71, 0, 0}, {47, 0, 0}, {27, 0, 0},
{239, 0, 0}, {55, 55, 255}, {255, 0, 0}, {0, 0, 255}, {43, 43, 35}, {27, 27, 23},
{19, 19, 15}, {235, 151, 127}, {195, 115, 83}, {159, 87, 51}, {123, 63, 27}, {235, 211, 199},
{199, 171, 155}, {167, 139, 119}, {135, 107, 87}, {159, 91, 83}
};
// (C) 2023 Daniel Gibson
#include <errno.h>
#include <limits.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include "colormap.h"
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
#define eprintf(...) fprintf(stderr, __VA_ARGS__)
typedef struct Image {
int width, height;
int stride, numColorComponents;
unsigned char* data;
} Image;
static int getInt16(const unsigned char* data)
{
return data[0] + ((int)data[1] << 8);
}
static int32_t getInt32(const unsigned char* data)
{
uint32_t x = getInt16(data);
uint32_t y = getInt16(data+2);
return x | (y << 16);
}
/* Daikatana .wal header:
Offset: field
0: char version; // NEW: should be 3
1: char padding[3]; // NEW this is just garbage, skip it
4: char name[32];
36: unsigned width, height; // size of mipmap level 0, found at offset offsets[0]
44: unsigned offsets[9]; // NEW 9 instead of 4 mip maps stored
80: char animname[32];
112: int flags;
116: int contents;
120: unsigned char palette[256*3]; // NEW: 256 RGB color values
888: int value;
=> size 892
Quake2 .wal header:
0: char name[32];
32: unsigned width, height;
40: unsigned offsets[MIPLEVELS = 4];
56: char animname[32]; // next frame in animation chain
88: int flags;
92: int contents;
96: int value;
=> size 100
*/
static Image parseWal(const unsigned char* data, const size_t len)
{
Image ret = {0};
ret.width = ret.height = ret.stride = -1;
if(len < 100) // that's the size of the Q2 WAL header
{
eprintf("Too small for a valid Q2 .wal!\n");
return ret;
}
const unsigned char* d = data;
size_t w = getInt32(d+32);
size_t h = getInt32(d+36);
uint32_t offset = getInt32(d+40); // only use miplevel 0
size_t dataSize = w*h;
if(len < offset+dataSize)
{
eprintf(".wal malformed, not long enough to actually contain image data!\n");
return ret;
}
unsigned char* buf = malloc(dataSize*3);
if(buf == NULL)
{
eprintf("Couldn't allocate %zd bytes of data - OOM?!\n", dataSize);
return ret;
}
const unsigned char* pix = data+offset;
for(size_t i=0; i<dataSize; ++i)
{
unsigned char* color = colormap[pix[i]];
unsigned char* outpix = buf + i*3;
outpix[0] = color[0];
outpix[1] = color[1];
outpix[2] = color[2];
}
ret.width = w;
ret.height = h;
ret.stride = w*3;
ret.numColorComponents = 3;
ret.data = buf;
return ret;
}
static unsigned char* loadFile(const char* filename, int* len)
{
*len = -1;
FILE* f = fopen(filename, "rb");
if(f == NULL)
{
eprintf("Failed to open '%s' !\n", filename);
return NULL;
}
struct stat statbuf = {0};
if(fstat(fileno(f), &statbuf) != 0)
{
eprintf("Can't get filesize of '%s'\n", filename);
fclose(f);
return NULL;
}
if(statbuf.st_size > INT_MAX)
{
eprintf("'%s' is too big!\n", filename);
fclose(f);
return NULL;
}
unsigned char* ret = malloc(statbuf.st_size);
if(ret == NULL)
{
eprintf("Couldn't allocate %lld bytes, OOM?!\n", (long long)statbuf.st_size);
fclose(f);
return NULL;
}
if(fread(ret, statbuf.st_size, 1, f) != 1)
{
eprintf("Couldn't read %s: %d (%s)\n", filename, errno, strerror(errno));
free(ret);
fclose(f);
return NULL;
}
*len = statbuf.st_size;
fclose(f);
return ret;
}
static bool checkAndCreateDir(const char* outdir)
{
struct stat statbuf = {0};
int staterr = 0;
if(stat(outdir, &statbuf) != 0)
{
staterr = errno;
if(staterr != ENOENT)
{
eprintf("Error while stat-ing output directory '%s': %d (%s)\n", outdir, errno, strerror(errno));
return false;
}
}
if(staterr)
{
// directory doesn't exist yet, create it, recursively
char* path = strdup(outdir);
char* dirnamestart = path;
if(dirnamestart[0] == '/')
++dirnamestart;
else if(dirnamestart[0] == '.' && dirnamestart[1] == '/')
dirnamestart += 2;
// if path is /path/to/foo/bar, start with /path, then /path/to, then /path/to/foo
// and eventually, after the loop, /path/to/foo/bar
// in each iteration make sure that the current path part exists before checking/creating the next part
for( char* next = strchr(dirnamestart, '/'); next != NULL; next = strchr(next+1, '/') )
{
// cut off after current directory name
*next = '\0';
staterr = 0;
if(stat(path, &statbuf) != 0)
{
staterr = errno;
if(staterr != ENOENT)
{
eprintf("Error while stat-ing part of output directory '%s': %d (%s)\n", path, errno, strerror(errno));
return false;
}
}
if(staterr == ENOENT && mkdir(path, 0755) != 0)
{
eprintf("Couldn't create (part of) outputdirectory '%s': %d (%s)\n", path, errno, strerror(errno));
free(path);
return false;
} // else do nothing, this directory already exists
*next = '/'; // restore /
next = strchr(next+1, '/');
}
// create the last path component
if(mkdir(outdir, 0755) != 0)
{
eprintf("Couldn't create (part of) outputdirectory '%s': %d (%s)\n", path, errno, strerror(errno));
free(path);
return false;
}
free(path);
}
else if((statbuf.st_mode & S_IFMT) != S_IFDIR)
{
eprintf("Given output directory '%s' exists, but is no directory!\n", outdir);
return false;
}
return true;
}
int main(int argc, char** argv)
{
if(argc < 3)
{
eprintf("Missing arguments!\n");
eprintf("Usage: %s <output_dir> <input_wal>\n", argv[0]);
eprintf(" /path/to/foo.wal will be converted to <output_dir>/foo.png\n");
eprintf(" <output_dir> will be created recursively, if it doesn't exist\n");
return 1;
}
const char* outdir = argv[1];
const char* walpath = argv[2];
if(!checkAndCreateDir(outdir))
return 1; // we already printed an error
int wallen;
unsigned char* walbuf = loadFile(walpath, &wallen);
if(walbuf == NULL)
return 1;
Image img = parseWal(walbuf, wallen);
free(walbuf);
walbuf = NULL;
if(img.data == NULL)
return 1;
// get only the filename from the path to the .wal
const char* walname = strrchr(walpath, '/');
if(walname == NULL)
walname = walpath;
else
++walname; // skip '/'
char pngpath[4096];
snprintf(pngpath, sizeof(pngpath), "%s/%s", outdir, walname);
char* fileending = strrchr(pngpath, '.');
if(fileending == NULL)
{
eprintf("Why did the input .wal file path not contain a '.' ?!\n");
free(img.data);
return 1;
}
memcpy(fileending+1, "png", 4); // replace "wal" (or "WAL" or whatever) with "png", incl. terminating \0
if(!stbi_write_png(pngpath, img.width, img.height, img.numColorComponents, img.data, img.stride))
{
eprintf("Writing PNG failed!\n");
free(img.data);
return 1;
}
free(img.data);
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment