DirkSimple/dirksimple.c
2023-06-30 18:47:35 -04:00

1878 lines
63 KiB
C

/**
* DirkSimple; a dirt-simple player for FMV games.
*
* Please see the file LICENSE.txt in the source's root directory.
*
* This file written by Ryan C. Gordon.
*/
#include "dirksimple_platform.h"
#include "theoraplay.h"
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"
#define DIRKSIMPLE_LUA_NAMESPACE "DirkSimple"
#define DIRKSIMPLE_FAKE_DISC_LAG_TICKS 800
// "included in all copies or substantial portions of the Software"
static const char *GLuaLicense =
"Lua:\n"
"\n"
"Copyright (C) 1994-2008 Lua.org, PUC-Rio.\n"
"\n"
"Permission is hereby granted, free of charge, to any person obtaining a copy\n"
"of this software and associated documentation files (the \"Software\"), to deal\n"
"in the Software without restriction, including without limitation the rights\n"
"to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n"
"copies of the Software, and to permit persons to whom the Software is\n"
"furnished to do so, subject to the following conditions:\n"
"\n"
"The above copyright notice and this permission notice shall be included in\n"
"all copies or substantial portions of the Software.\n"
"\n"
"THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n"
"IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n"
"FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n"
"AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n"
"LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n"
"OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n"
"THE SOFTWARE.\n"
"\n";
typedef enum RenderPrimitive
{
RENDPRIM_CLEAR,
RENDPRIM_SPRITE,
RENDPRIM_SOUND,
} RenderPrimitive;
typedef struct RenderCommand
{
RenderPrimitive prim;
union
{
struct
{
uint8_t r, g, b;
} clear;
struct
{
char name[32];
int32_t sx, sy, sw, sh, dx, dy, dw, dh;
uint8_t r, g, b;
} sprite;
struct
{
char name[32];
} sound;
} data;
} RenderCommand;
static char *GGameName = NULL;
static char *GGamePath = NULL;
static char *GDataDir = NULL;
static char *GGameDir = NULL;
static THEORAPLAY_Decoder *GDecoder = NULL;
static THEORAPLAY_Io GTheoraplayIo;
static lua_State *GLua = NULL;
static uint64_t GPreviousInputBits = 0;
static int GDiscoveredVideoFormat = 0;
static int GDiscoveredAudioFormat = 0;
static int GAudioChannels = 0;
static int GAudioFreq = 0;
static uint64_t GTicks = 0; // Current ticks into the game, increases each iteration.
static uint64_t GTicksOffset = 0; // offset from monotonic clock where we started.
static uint32_t GFrameMS = 0; // milliseconds each video frame takes.
static int64_t GSeekToTicksOffset = 0;
static uint64_t GClipStartMs = 0; // Milliseconds into video stream that starts this clip.
static uint64_t GClipStartTicks = 0; // GTicks when clip started playing
static int GHalted = 0;
static int GShowingSingleFrame = 0;
static int GFakeDiscLag = 0;
static uint64_t GDiscLagUntilTicks = 0;
static unsigned int GSeekGeneration = 0;
static int GNeedInitialLuaTick = 1;
static const THEORAPLAY_VideoFrame *GPendingVideoFrame = NULL;
static DirkSimple_Sprite *GSprites = NULL;
static DirkSimple_Wave *GWaves = NULL;
static RenderCommand *GRenderCommands = NULL;
static int GNumRenderCommands = 0;
static int GNumAllocatedRenderCommands = 0;
static uint8_t *GBlankVideoFrame = NULL;
static void out_of_memory(void)
{
DirkSimple_panic("Out of memory!");
}
void *DirkSimple_xmalloc(size_t len)
{
void *retval = DirkSimple_malloc(len);
if (!retval) {
out_of_memory();
}
return retval;
}
void *DirkSimple_xcalloc(size_t nmemb, size_t len)
{
void *retval = DirkSimple_calloc(nmemb, len);
if (!retval) {
out_of_memory();
}
return retval;
}
void *DirkSimple_xrealloc(void *ptr, size_t len)
{
void *retval = DirkSimple_realloc(ptr, len);
if (!retval && (len > 0)) {
out_of_memory();
}
return retval;
}
char *DirkSimple_xstrdup(const char *str)
{
char *retval = DirkSimple_strdup(str);
if (!retval) {
out_of_memory();
}
return retval;
}
const char *DirkSimple_gamename(void) { return GGameName; }
const char *DirkSimple_datadir(void) { return GDataDir; }
const char *DirkSimple_gamedir(void) { return GGameDir; }
void DirkSimple_log(const char *fmt, ...)
{
char *str = NULL;
va_list ap;
size_t len;
va_start(ap, fmt);
len = vsnprintf(NULL, 0, fmt, ap);
va_end(ap);
str = DirkSimple_xmalloc(len + 1);
va_start(ap, fmt);
vsnprintf(str, len + 1, fmt, ap);
va_end(ap);
DirkSimple_writelog(str);
DirkSimple_free(str);
}
static long theoraplayiobridge_read(THEORAPLAY_Io *io, void *buf, long buflen)
{
DirkSimple_Io *dio = (DirkSimple_Io *) io->userdata;
return dio->read(dio, buf, buflen);
}
static long theoraplayiobridge_streamlen(THEORAPLAY_Io *io)
{
DirkSimple_Io *dio = (DirkSimple_Io *) io->userdata;
return dio->streamlen(dio);
}
static int theoraplayiobridge_seek(THEORAPLAY_Io *io, long absolute_offset)
{
DirkSimple_Io *dio = (DirkSimple_Io *) io->userdata;
return dio->seek(dio, absolute_offset);
}
static void theoraplayiobridge_close(THEORAPLAY_Io *io)
{
DirkSimple_Io *dio = (DirkSimple_Io *) io->userdata;
dio->close(dio);
}
static uint8_t *invalid_media(const char *fname, const char *err)
{
DirkSimple_log("Failed to load '%s': %s", fname, err);
return NULL;
}
uint8_t *DirkSimple_loadmedia(const char *fname, long *flen)
{
DirkSimple_Io *io = DirkSimple_openfile_read(fname);
if (!io) {
return invalid_media(fname, "Couldn't open file for reading");
}
*flen = io->streamlen(io);
uint8_t *fbuf = (uint8_t *) DirkSimple_malloc(*flen);
if (!fbuf) {
return invalid_media(fname, "Out of memory");
}
const long br = io->read(io, fbuf, *flen);
io->close(io);
if (br != *flen) {
DirkSimple_free(fbuf);
return invalid_media(fname, "couldn't read whole file into memory");
}
return fbuf;
}
static uint32_t readui32le(const uint8_t **pptr)
{
const uint8_t *ptr = *pptr;
uint32_t retval = (uint32_t) ptr[0];
retval |= ((uint32_t) ptr[1]) << 8;
retval |= ((uint32_t) ptr[2]) << 16;
retval |= ((uint32_t) ptr[3]) << 24;
*pptr += sizeof (retval);
return retval;
}
static uint16_t readui16le(const uint8_t **pptr)
{
const uint8_t *ptr = *pptr;
uint16_t retval = (uint16_t) ptr[0];
retval |= ((uint16_t) ptr[1]) << 8;
*pptr += sizeof (retval);
return retval;
}
static uint8_t *loadbmp_from_memory(const char *fname, const uint8_t *buf, int buflen, int *_w, int *_h)
{
const uint8_t *ptr = buf;
*_w = *_h = 0;
if (buflen < 50) {
return invalid_media(fname, "Not a .BMP file");
}
if ((ptr[0] != 'B') || (ptr[1] != 'M')) {
return invalid_media(fname, "Not a .BMP file");
}
ptr += 10;
const uint32_t offset_bits = readui32le(&ptr);
const uint32_t infolen = readui32le(&ptr);
if (offset_bits > buflen) {
return invalid_media(fname, "Incomplete or corrupt .BMP file");
} else if ((infolen + 10) > buflen) {
return invalid_media(fname, "Incomplete or corrupt .BMP file");
} else if (infolen < 40) { // not a standard BITMAPINFOHEADER.
return invalid_media(fname, "Unsupported .BMP format");
} else if (infolen == 64) { // this is some ancient, incompatible OS/2 thing.
return invalid_media(fname, "Unsupported .BMP format");
}
const uint32_t bmpwidth = readui32le(&ptr);
const uint32_t bmpheight = readui32le(&ptr);
if ((bmpwidth > 1024) || (bmpheight > 1024)) {
return invalid_media(fname, "Image is too big"); // this is just an arbitrary limit.
} else if ((bmpwidth == 0) || (bmpheight == 0)) {
return invalid_media(fname, "Image is zero pixels in size");
} else if ((offset_bits + (bmpwidth * bmpheight * 4)) > buflen) {
return invalid_media(fname, "Incomplete or corrupt .BMP file");
}
ptr += 2; // skip planes
const uint16_t bitcount = readui16le(&ptr);
if (bitcount != 32) {
return invalid_media(fname, "Only 32bpp .BMP files supported");
}
const uint32_t compression = readui32le(&ptr);
if ((compression != 0) && (compression != 3)) {
return invalid_media(fname, "Only uncompressed RGB .BMP files supported");
}
// we don't check the color masks; we assume they match what we want,
// and checking them just to verify is a lot of compatibility tapdancing.
uint8_t *pixels = (uint8_t *) DirkSimple_xmalloc(bmpwidth * bmpheight * 4);
// of course the stupid pixels are upside down in a bmp file.
const size_t rowlen = bmpwidth * 4;
const uint8_t *src = (buf + offset_bits) + (rowlen * (bmpheight - 1));
uint8_t *dst = pixels;
for (int y = 0; y < bmpheight; y++) {
memcpy(dst, src, rowlen);
dst += rowlen;
src -= rowlen;
}
*_w = (int) bmpwidth;
*_h = (int) bmpheight;
return pixels;
}
uint8_t *DirkSimple_loadbmp(const char *fname, int *_w, int *_h)
{
long flen = 0;
uint8_t *fbuf = DirkSimple_loadmedia(fname, &flen);
uint8_t *pixels = loadbmp_from_memory(fname, fbuf, flen, _w, _h);
DirkSimple_free(fbuf);
return (uint8_t *) pixels;
}
// !!! FIXME: we should probably cache all sprites and waves we see in the
// !!! FIXME: game's dir during DirkSimple_startup. There are only a handful
// !!! FIXME: of them, we might as well just load them all upfront instead of
// !!! FIXME: risking a stall on the first use.
static DirkSimple_Sprite *get_cached_sprite(const char *name)
{
DirkSimple_Sprite *sprite = NULL;
// lowercase the name, just in case.
char *loweredname = DirkSimple_xstrdup(name);
int i;
for (i = 0; name[i]; i++) {
char ch = name[i];
if ((ch >= 'A') && (ch <= 'Z')) {
loweredname[i] = ch - ('A' - 'a');
}
}
name = loweredname;
for (sprite = GSprites; sprite != NULL; sprite = sprite->next) {
if (strcmp(sprite->name, loweredname) == 0) {
DirkSimple_free(loweredname);
return sprite; // already cached.
}
}
// not cached yet, load it from disk.
const size_t slen = strlen(GGameDir) + strlen(name) + 8;
char *fname = (char *) DirkSimple_xmalloc(slen);
snprintf(fname, slen, "%s%s.bmp", GGameDir, name);
int w, h;
uint8_t *rgba = DirkSimple_loadbmp(fname, &w, &h);
if (rgba) {
DirkSimple_log("Loaded sprite '%s': %dx%d, RGBA", fname, w, h);
}
DirkSimple_free(fname);
if (!rgba) {
char errmsg[128];
snprintf(errmsg, sizeof (errmsg), "Failed to load needed sprite '%s'. Check your installation?", name);
DirkSimple_panic(errmsg);
}
sprite = (DirkSimple_Sprite *) DirkSimple_xmalloc(sizeof (DirkSimple_Sprite));
sprite->name = loweredname;
sprite->width = w;
sprite->height = h;
sprite->rgba = rgba;
sprite->platform_handle = NULL;
sprite->next = GSprites;
GSprites = sprite;
return sprite;
}
/* this is an extremely simple, low-quality converter, because presumably we're dealing with simple, low-quality audio. */
static float *convertwav(const char *fname, float *pcm, int *_frames, int havechannels, int havefreq, int wantchannels, int wantfreq)
{
const int frames = *_frames;
float *dst;
float *src;
float *retval = NULL;
float *rechanneled = NULL;
DirkSimple_log("Convert audio from '%s': %d frames, %d channels, %dHz -> %d channels, %dHz", fname, frames, havechannels, havefreq, wantchannels, wantfreq);
if (havechannels == wantchannels) {
rechanneled = pcm;
} else {
dst = rechanneled = (float *) DirkSimple_xmalloc(sizeof (float) * frames * wantchannels);
src = pcm;
if ((havechannels == 1) && (wantchannels == 2)) {
for (int i = 0; i < frames; i++, dst += 2) {
dst[0] = dst[1] = src[i];
}
} else if ((havechannels == 2) && (wantchannels == 1)) {
for (int i = 0; i < frames; i++, src += 2) {
*(dst++) = (src[0] + src[1]) * 0.5f;
}
} else {
DirkSimple_free(rechanneled);
return (float *) invalid_media(fname, "Unsupported audio channel count");
}
}
if (havefreq == wantfreq) {
retval = rechanneled;
} else {
const float scale = ((float) havefreq) / ((float) wantfreq);
const int newframes = (int) (((float) frames) * (1.0f / scale));
dst = retval = (float *) DirkSimple_xmalloc(sizeof (float) * newframes * wantchannels);
src = rechanneled;
if (wantchannels == 1) {
float prev = src[0];
for (int i = 0; i < newframes; i++) {
const int newframe = (int) (((float) i) * scale);
const float newval = src[newframe];
*(dst++) = (prev + newval) * 0.5f;
prev = newval;
}
} else if (wantchannels == 2) {
float prevl = src[0];
float prevr = src[1];
for (int i = 0; i < newframes; i++) {
const int newframe = ((int) (((float) i) * scale)) * 2;
const float newl = src[newframe];
const float newr = src[newframe+1];
*(dst++) = (prevl + newl) * 0.5f;
*(dst++) = (prevr + newr) * 0.5f;
prevl = newl;
prevr = newr;
}
} else {
if (rechanneled != pcm) {
DirkSimple_free(rechanneled);
}
DirkSimple_free(retval);
return (float *) invalid_media(fname, "Unsupported audio channel count");
}
if (rechanneled != pcm) {
DirkSimple_free(rechanneled);
}
*_frames = newframes;
}
return retval;
}
static float *loadwav_from_memory(const char *fname, const uint8_t *buf, int buflen, int *_frames, int *_channels, int *_freq)
{
const uint8_t *ptr = buf;
if (buflen < 50) {
return (float *) invalid_media(fname, "Not a .WAV file");
}
if ((ptr[0] != 'R') || (ptr[1] != 'I') || (ptr[2] != 'F') || (ptr[3] != 'F')) {
return (float *) invalid_media(fname, "Not a .WAV file");
}
ptr += 8; // skip RIFF and size of file in bytes
if ((ptr[0] != 'W') || (ptr[1] != 'A') || (ptr[2] != 'V') || (ptr[3] != 'E')) {
return (float *) invalid_media(fname, "Not a .WAV file");
}
ptr += 4;
if ((ptr[0] != 'f') || (ptr[1] != 'm') || (ptr[2] != 't') || (ptr[3] != ' ')) {
return (float *) invalid_media(fname, "Didn't get expected 'fmt ' tag");
}
ptr += 4;
const uint32_t fmtlen = readui32le(&ptr);
const uint16_t fmttype = readui16le(&ptr);
const uint16_t chans = readui16le(&ptr);
const uint32_t samplerate = readui32le(&ptr);
if (fmtlen != 16) { /* Just handling an exact format for simplicity. */
return (float *) invalid_media(fname, "Unexpected .WAV fmt chunk length");
} else if (fmttype != 3) { /* must be float32 encoded for simplicity. */
return (float *) invalid_media(fname, "Only float32 PCM .WAV files supported");
} else if ((chans != 1) && (chans != 2)) {
return (float *) invalid_media(fname, "Only stereo and mono .WAV files supported");
}
ptr += 8; // don't care about the next two fields.
// skip until we find the data chunk and assume everything else is unnecessary. :O
uint32_t datalen;
while ((ptr[0] != 'd') || (ptr[1] != 'a') || (ptr[2] != 't') || (ptr[3] != 'a')) {
//DirkSimple_log("CHUNK %c%c%c%c", (char) ptr[0], (char) ptr[1], (char) ptr[2], (char) ptr[3]);
ptr += 4;
datalen = readui32le(&ptr);
//DirkSimple_log("CHUNKLEN %d", (int) datalen);
if (datalen > (buflen - ((ptr - buf)))) {
return (float *) invalid_media(fname, "Unexpected .WAV data chunk length");
}
ptr += datalen;
}
ptr += 4;
datalen = readui32le(&ptr);
if (datalen > (buflen - ((ptr - buf)))) {
return (float *) invalid_media(fname, "Unexpected .WAV data chunk length");
}
const int frames = (int) ((datalen / sizeof (float)) / chans);
datalen = frames * sizeof (float) * chans; // so this chops off half-frames.
*_frames = frames;
*_channels = (int) chans;
*_freq = (int) samplerate;
float *pcm = (float *) DirkSimple_xmalloc(datalen);
memcpy(pcm, ptr, datalen);
return pcm;
}
float *DirkSimple_loadwav(const char *fname, int *_numframes, const int wantchannels, int wantfreq)
{
long flen = 0;
int havechannels = 0;
int havefreq = 0;
int frames = 0;
uint8_t *fbuf = DirkSimple_loadmedia(fname, &flen);
float *pcm = loadwav_from_memory(fname, fbuf, flen, &frames, &havechannels, &havefreq);
DirkSimple_free(fbuf);
if (pcm) {
float *cvtpcm = convertwav(fname, pcm, &frames, havechannels, havefreq, wantchannels, wantfreq);
if (cvtpcm != pcm) {
DirkSimple_free(pcm);
pcm = cvtpcm;
}
}
*_numframes = frames;
return pcm;
}
static DirkSimple_Wave *get_cached_wave(const char *name)
{
DirkSimple_Wave *wave = NULL;
int frames = 0;
if (!GAudioChannels || !GAudioFreq) {
DirkSimple_log("Attempting to use wave '%s' before laserdisc is ready!", name);
return NULL;
}
// lowercase the name, just in case.
char *loweredname = DirkSimple_xstrdup(name);
int i;
for (i = 0; name[i]; i++) {
char ch = name[i];
if ((ch >= 'A') && (ch <= 'Z')) {
loweredname[i] = ch - ('A' - 'a');
}
}
name = loweredname;
for (wave = GWaves; wave != NULL; wave = wave->next) {
if (strcmp(wave->name, loweredname) == 0) {
DirkSimple_free(loweredname);
return wave; // already cached.
}
}
// not cached yet, load it from disk.
const size_t slen = strlen(GGameDir) + strlen(name) + 8;
char *fname = (char *) DirkSimple_xmalloc(slen);
snprintf(fname, slen, "%s%s.wav", GGameDir, name);
float *pcm = DirkSimple_loadwav(fname, &frames, GAudioChannels, GAudioFreq);
if (pcm) {
DirkSimple_log("Loaded wave '%s': %d channels, %dHz", fname, GAudioChannels, GAudioFreq);
}
DirkSimple_free(fname);
if (!pcm) {
char errmsg[128];
snprintf(errmsg, sizeof (errmsg), "Failed to load needed wave '%s'. Check your installation?", name);
DirkSimple_panic(errmsg);
}
wave = (DirkSimple_Wave *) DirkSimple_xmalloc(sizeof (DirkSimple_Wave));
wave->name = loweredname;
wave->numframes = frames;
wave->pcm = pcm;
wave->duration_ticks = (uint64_t) ((((float) frames) / ((float) GAudioFreq)) * 1000.0f);
wave->ticks_when_available = 0;
wave->platform_handle = NULL;
wave->next = GWaves;
GWaves = wave;
return wave;
}
// Allocator interface for internal Lua use.
static void *DirkSimple_lua_allocator(void *ud, void *ptr, size_t osize, size_t nsize)
{
if (nsize == 0) {
DirkSimple_free(ptr);
return NULL;
}
return DirkSimple_xrealloc(ptr, nsize);
}
// Read data from a DirkSimple_Io when loading Lua code.
static const char *DirkSimple_lua_reader(lua_State *L, void *data, size_t *size)
{
static char buffer[1024];
DirkSimple_Io *in = (DirkSimple_Io *) data;
const long br = in->read(in, buffer, sizeof (buffer));
if (br <= 0) { // eof or error? (lua doesn't care which?!)
*size = 0;
return NULL;
}
*size = (size_t) br;
return buffer;
}
static void collect_lua_garbage(lua_State *L)
{
lua_gc(L, LUA_GCCOLLECT, 0);
}
static inline int snprintfcat(char **ptr, size_t *len, const char *fmt, ...)
{
int bw = 0;
va_list ap;
va_start(ap, fmt);
bw = vsnprintf(*ptr, *len, fmt, ap);
va_end(ap);
*ptr += bw;
*len -= bw;
return bw;
}
static int luahook_DirkSimple_stackwalk(lua_State *L)
{
const char *errstr = lua_tostring(L, 1);
lua_Debug ldbg;
int i;
if (errstr != NULL) {
DirkSimple_log(errstr);
}
DirkSimple_log("Lua stack backtrace:");
// start at 1 to skip this function.
for (i = 1; lua_getstack(L, i, &ldbg); i++) {
char scratchbuf[256];
char *ptr = scratchbuf;
size_t len = sizeof (scratchbuf);
int bw = snprintfcat(&ptr, &len, "#%d", i-1);
const int maxspacing = 4;
int spacing = maxspacing - bw;
while (spacing-- > 0) {
snprintfcat(&ptr, &len, " ");
}
if (!lua_getinfo(L, "nSl", &ldbg)) {
snprintfcat(&ptr, &len, "???");
DirkSimple_log(scratchbuf);
continue;
}
if (ldbg.namewhat[0]) {
snprintfcat(&ptr, &len, "%s ", ldbg.namewhat);
}
if ((ldbg.name) && (ldbg.name[0])) {
snprintfcat(&ptr, &len, "function %s ()", ldbg.name);
} else {
if (strcmp(ldbg.what, "main") == 0) {
snprintfcat(&ptr, &len, "mainline of chunk");
} else if (strcmp(ldbg.what, "tail") == 0) {
snprintfcat(&ptr, &len, "tail call");
} else {
snprintfcat(&ptr, &len, "unidentifiable function");
}
}
DirkSimple_log(scratchbuf);
ptr = scratchbuf;
len = sizeof (scratchbuf);
for (spacing = 0; spacing < maxspacing; spacing++) {
snprintfcat(&ptr, &len, " ");
}
if (strcmp(ldbg.what, "C") == 0) {
snprintfcat(&ptr, &len, "in native code");
} else if (strcmp(ldbg.what, "tail") == 0) {
snprintfcat(&ptr, &len, "in Lua code");
} else if ( (strcmp(ldbg.source, "=?") == 0) && (ldbg.currentline == 0) ) {
snprintfcat(&ptr, &len, "in Lua code (debug info stripped)");
} else {
snprintfcat(&ptr, &len, "in Lua code at %s", ldbg.short_src);
if (ldbg.currentline != -1)
snprintfcat(&ptr, &len, ":%d", ldbg.currentline);
}
DirkSimple_log(scratchbuf);
}
lua_pushstring(L, errstr ? errstr : "");
return 1;
}
// This just lets you punch in one-liners and Lua will run them as individual
// chunks, but you can completely access all Lua state, including calling C
// functions and altering tables. At this time, it's more of a "console"
// than a debugger. You can do "p DirkSimple_lua_debugger()" from gdb to launch this
// from a breakpoint in native code, or call DirkSimple.debugger() to launch
// it from Lua code (with stacktrace intact, too: type 'bt' to see it).
static int luahook_DirkSimple_debugger(lua_State *L)
{
int origtop;
lua_pushcfunction(L, luahook_DirkSimple_stackwalk);
origtop = lua_gettop(L);
printf("Quick and dirty Lua debugger. Type 'exit' to quit.\n");
while (1) {
char buf[256];
int len = 0;
printf("> ");
fflush(stdout);
if (fgets(buf, sizeof (buf), stdin) == NULL) {
printf("\n\n fgets() on stdin failed: ");
break;
}
len = (int) (strlen(buf) - 1);
while ( (len >= 0) && ((buf[len] == '\n') || (buf[len] == '\r')) ) {
buf[len--] = '\0';
}
if (strcmp(buf, "q") == 0) {
break;
} else if (strcmp(buf, "quit") == 0) {
break;
} else if (strcmp(buf, "exit") == 0) {
break;
} else if (strcmp(buf, "bt") == 0) {
strcpy(buf, "DirkSimple.stackwalk()");
}
if ( (luaL_loadstring(L, buf) != 0) ||
(lua_pcall(L, 0, LUA_MULTRET, -2) != 0) ) {
printf("%s\n", lua_tostring(L, -1));
lua_pop(L, 1);
} else {
printf("Returned %d values.\n", lua_gettop(L) - origtop);
while (lua_gettop(L) != origtop) {
// !!! FIXME: dump details of values to stdout here.
lua_pop(L, 1);
}
printf("\n");
}
}
lua_pop(L, 1);
printf("exiting debugger...\n");
return 0;
}
void DirkSimple_debugger(void)
{
luahook_DirkSimple_debugger(GLua);
}
static void DirkSimple_halt_video(void)
{
if (GDecoder) {
DirkSimple_log("HALT VIDEO: GTicks %u", (unsigned int) GTicks);
GClipStartMs = 0;
GClipStartTicks = GTicks;
GHalted = 1;
GShowingSingleFrame = 0;
GSeekGeneration--; // this just makes it throw away _anything_ until the next seek.
DirkSimple_cleardiscaudio();
DirkSimple_discvideo(GBlankVideoFrame);
}
}
static int luahook_DirkSimple_halt_video(lua_State *L)
{
DirkSimple_halt_video();
return 0;
}
static void DirkSimple_show_single_frame(uint32_t startms)
{
if (GDecoder) {
DirkSimple_log("START SINGLE FRAME: GTicks %u, startms %u", (unsigned int) GTicks, (unsigned int) startms);
GSeekGeneration = THEORAPLAY_seek(GDecoder, startms);
DirkSimple_cleardiscaudio();
GClipStartMs = startms;
GClipStartTicks = 0;
GHalted = 0;
GShowingSingleFrame = 1;
if (GFakeDiscLag) {
DirkSimple_discvideo(GBlankVideoFrame);
GDiscLagUntilTicks = GTicks + DIRKSIMPLE_FAKE_DISC_LAG_TICKS;
}
}
}
static int luahook_DirkSimple_show_single_frame(lua_State *L)
{
const uint32_t startms = (uint32_t) lua_tonumber(L, 1);
DirkSimple_show_single_frame(startms);
return 0;
}
static void DirkSimple_start_clip(uint32_t startms)
{
if (GDecoder) {
DirkSimple_log("START CLIP: GTicks %u, startms %u\n", (unsigned int) GTicks, (unsigned int) startms);
GSeekGeneration = THEORAPLAY_seek(GDecoder, startms);
DirkSimple_cleardiscaudio();
GClipStartMs = startms;
GClipStartTicks = 0;
GHalted = 0;
GShowingSingleFrame = 0;
if (GFakeDiscLag) {
DirkSimple_discvideo(GBlankVideoFrame);
GDiscLagUntilTicks = GTicks + DIRKSIMPLE_FAKE_DISC_LAG_TICKS;
}
}
}
static int luahook_DirkSimple_start_clip(lua_State *L)
{
const uint32_t startms = (uint32_t) lua_tonumber(L, 1);
DirkSimple_start_clip(startms);
return 0;
}
static int luahook_DirkSimple_truncate(lua_State *L)
{
lua_pushinteger(L, (lua_Integer) lua_tonumber(L, 1));
return 1;
}
static int luahook_DirkSimple_to_int(lua_State *L)
{
if (lua_isstring(L, 1)) {
lua_pushinteger(L, atoi(lua_tostring(L, 1)));
} else if (lua_isboolean(L, 1)) {
lua_pushinteger(L, lua_toboolean(L, 1) ? 1 : 0);
} else if (lua_isnumber(L, 1)) {
lua_pushinteger(L, (lua_Integer) lua_tonumber(L, 1));
} else {
lua_pushinteger(L, 0);
}
return 1;
}
static int luahook_DirkSimple_to_bool(lua_State *L)
{
if (lua_isstring(L, 1)) {
lua_pushboolean(L, (strcmp(lua_tostring(L, 1), "true") == 0));
} else if (lua_isboolean(L, 1)) {
lua_pushboolean(L, lua_toboolean(L, 1));
} else if (lua_isnumber(L, 1)) {
lua_pushboolean(L, (lua_tonumber(L, 1) != 0));
} else {
lua_pushboolean(L, 0);
}
return 1;
}
static void register_lua_libs(lua_State *L)
{
// We always need the string and base libraries (although base has a
// few we could trim). The rest you can compile in if you want/need them.
int i;
static const luaL_Reg lualibs[] = {
{"_G", luaopen_base},
{LUA_STRLIBNAME, luaopen_string},
{LUA_TABLIBNAME, luaopen_table},
//{LUA_LOADLIBNAME, luaopen_package},
//{LUA_IOLIBNAME, luaopen_io},
//{LUA_OSLIBNAME, luaopen_os},
//{LUA_MATHLIBNAME, luaopen_math},
//{LUA_DBLIBNAME, luaopen_debug},
//{LUA_BITLIBNAME, luaopen_bit32},
//{LUA_COLIBNAME, luaopen_coroutine},
};
for (i = 0; i < (sizeof (lualibs) / sizeof (lualibs[0])); i++) {
luaL_requiref(L, lualibs[i].name, lualibs[i].func, 1);
lua_pop(L, 1); // remove lib
}
}
static int luahook_panic(lua_State *L)
{
const char *errstr;
luahook_DirkSimple_stackwalk(L);
errstr = lua_tostring(L, -1);
if (errstr == NULL) {
errstr = "Something disastrous happened, aborting."; // doesn't actually return.
}
DirkSimple_panic(errstr);
return 1; // doesn't actually return, but stackwalk pushed a return value, so return 1 for hygiene purposes.
}
static int luahook_DirkSimple_log(lua_State *L)
{
const char *str = lua_tostring(L, 1);
DirkSimple_log("%s", str);
return 0;
}
static RenderCommand *new_render_command(const RenderPrimitive prim)
{
if (GNumRenderCommands >= GNumAllocatedRenderCommands) {
if (GNumAllocatedRenderCommands == 0) {
GNumAllocatedRenderCommands = 32;
} else {
GNumAllocatedRenderCommands *= 2;
}
GRenderCommands = DirkSimple_xrealloc(GRenderCommands, sizeof (RenderCommand) * GNumAllocatedRenderCommands);
}
RenderCommand *retval = &GRenderCommands[GNumRenderCommands++];
retval->prim = prim;
return retval;
}
static int luahook_DirkSimple_clear_screen(lua_State *L)
{
RenderCommand *cmd = new_render_command(RENDPRIM_CLEAR);
cmd->data.clear.r = (uint8_t) lua_tonumber(L, 1);
cmd->data.clear.g = (uint8_t) lua_tonumber(L, 2);
cmd->data.clear.b = (uint8_t) lua_tonumber(L, 3);
return 0;
}
static int luahook_DirkSimple_draw_sprite(lua_State *L)
{
RenderCommand *cmd = new_render_command(RENDPRIM_SPRITE);
snprintf(cmd->data.sprite.name, sizeof (cmd->data.sprite.name), "%s", lua_tostring(L, 1));
cmd->data.sprite.sx = (int32_t) lua_tonumber(L, 2);
cmd->data.sprite.sy = (int32_t) lua_tonumber(L, 3);
cmd->data.sprite.sw = (int32_t) lua_tonumber(L, 4);
cmd->data.sprite.sh = (int32_t) lua_tonumber(L, 5);
cmd->data.sprite.dx = (int32_t) lua_tonumber(L, 6);
cmd->data.sprite.dy = (int32_t) lua_tonumber(L, 7);
cmd->data.sprite.dw = (int32_t) lua_tonumber(L, 8);
cmd->data.sprite.dh = (int32_t) lua_tonumber(L, 9);
cmd->data.sprite.r = (uint8_t) lua_tonumber(L, 10);
cmd->data.sprite.g = (uint8_t) lua_tonumber(L, 11);
cmd->data.sprite.b = (uint8_t) lua_tonumber(L, 12);
return 0;
}
static int luahook_DirkSimple_play_sound(lua_State *L)
{
RenderCommand *cmd = new_render_command(RENDPRIM_SOUND);
snprintf(cmd->data.sound.name, sizeof (cmd->data.sound.name), "%s", lua_tostring(L, 1));
return 0;
}
static void load_lua_gamecode(lua_State *L, const char *gamedir)
{
int rc;
DirkSimple_Io *io;
const size_t slen = strlen(gamedir) + 32;
char *fname = (char *) DirkSimple_xmalloc(slen);
snprintf(fname, slen, "@%sgame.luac", gamedir);
io = DirkSimple_openfile_read(fname + 1);
if (!io) {
snprintf(fname, slen, "@%sgame.lua", gamedir);
io = DirkSimple_openfile_read(fname + 1);
if (!io) {
char *err = (char *) DirkSimple_xmalloc(slen * 2);
snprintf(err, slen * 2, "Failed to open Lua code at '%s'", fname + 1);
DirkSimple_panic(err);
}
}
lua_pushcfunction(L, luahook_DirkSimple_stackwalk);
rc = lua_load(L, DirkSimple_lua_reader, io, fname, NULL);
io->close(io);
DirkSimple_free(fname);
if (rc != 0) {
lua_error(L);
} else {
// Call new chunk on top of the stack (lua_pcall will pop it off).
if (lua_pcall(L, 0, 0, -2) != 0) { // retvals are dumped.
lua_error(L); // error on stack has debug info.
}
// if this didn't panic, we succeeded.
}
lua_pop(L, 1); // dump stackwalker.
}
static void do_cvar_registration(const char *name, const char *desc, const char *values)
{
if (name && desc && values) {
DirkSimple_log("Registering cvar '%s' (%s), valid values are '%s'", name, desc, values);
DirkSimple_registercvar(GGameName, name, desc, values);
}
}
static void register_cvars(lua_State *L)
{
if (L) {
lua_getglobal(L, DIRKSIMPLE_LUA_NAMESPACE);
if (lua_istable(L, -1)) { // namespace is sane?
lua_getfield(L, -1, "cvars");
if (lua_istable(L, -1)) { // if not a table, maybe unsupported by this game
lua_pushnil(L); // first key for iteration...
while (lua_next(L, -2)) { // replaces key, pushes value.
if (lua_istable(L, -1)) {
const char *cvar_name = NULL;
const char *cvar_desc = NULL;
const char *cvar_values = NULL;
lua_getfield(L, -1, "name");
cvar_name = lua_tostring(L, -1);
lua_getfield(L, -2, "desc");
cvar_desc = lua_tostring(L, -1);
lua_getfield(L, -3, "values");
cvar_values = lua_tostring(L, -1);
do_cvar_registration(cvar_name, cvar_desc, cvar_values);
lua_pop(L, 3); // dump name, desc, values
}
lua_pop(L, 1); // remove table, keep key for next iteration.
}
}
lua_pop(L, 1); // pop the cvars table
}
lua_pop(L, 1); // pop the namespace
}
// !!! FIXME: register engine-level cvars here.
}
// setter lua function is on top of the Lua stack. This function will pop it.
static void set_lua_cvar(lua_State *L, const char *name, const char *valid_values, const char *newvalue)
{
const size_t slenvalid = strlen(valid_values);
const size_t slennew = strlen(newvalue);
const char *ptr;
if (slenvalid < slennew) {
lua_pop(L, 1); // pop the setter
return; // newvalue can't be listed in valid_values, it's bigger than the valid list string.
}
ptr = strstr(valid_values, newvalue);
if (!ptr) {
lua_pop(L, 1); // pop the setter
return; // not listed in valid_values
}
if ( (slenvalid == slennew) || // matches entire string
((ptr == valid_values) && (valid_values[slennew] == '|')) || // matches start of string
((ptr > valid_values) && (ptr[-1] == '|') && ((ptr[slennew] == '|') || (ptr[slennew] == '\0'))) ) { // matches middle or end of string
// it's valid, pass it to Lua.
lua_pushstring(L, name);
lua_pushstring(L, newvalue);
lua_call(L, 2, 0);
}
}
void DirkSimple_setcvar(const char *name, const char *newvalue)
{
lua_State *L = GLua;
if (L) {
lua_getglobal(L, DIRKSIMPLE_LUA_NAMESPACE);
if (lua_istable(L, -1)) { // namespace is sane?
lua_getfield(L, -1, "cvars");
if (lua_istable(L, -1)) { // if not a table, maybe unsupported by this game
lua_pushnil(L); // first key for iteration...
while (lua_next(L, -2)) { // replaces key, pushes value.
int endloop = 0;
if (lua_istable(L, -1)) {
const char *cvarname;
lua_getfield(L, -1, "name");
cvarname = lua_tostring(L, -1);
if (cvarname && (strcmp(cvarname, name) == 0)) {
const char *values;
lua_getfield(L, -2, "values");
values = lua_tostring(L, -1);
if (values) {
lua_getfield(L, -3, "setter");
if (lua_isfunction(L, -1)) {
set_lua_cvar(L, name, values, newvalue);
} else {
lua_pop(L, 1); // pop the setter
}
endloop = 1; // we're done.
}
lua_pop(L, 1); // pop the values
}
lua_pop(L, 1); // pop the name
}
lua_pop(L, 1); // pop the table, keep key for next iteration.
if (endloop) {
lua_pop(L, 1); // dump key, too.
break;
}
}
}
lua_pop(L, 1); // pop the cvars table
}
lua_pop(L, 1); // pop the namespace
}
}
// Sets t[sym]=f, where t is on the top of the Lua stack.
static void set_cfunc(lua_State *L, lua_CFunction f, const char *sym)
{
lua_pushcfunction(L, f);
lua_setfield(L, -2, sym);
}
// Sets t[sym]=f, where t is on the top of the Lua stack.
static void set_integer(lua_State *L, const int x, const char *sym)
{
lua_pushinteger(L, x);
lua_setfield(L, -2, sym);
}
// Sets t[sym]=f, where t is on the top of the Lua stack.
static void set_string(lua_State *L, const char *str, const char *sym)
{
lua_pushstring(L, str);
lua_setfield(L, -2, sym);
}
static void setup_lua(void)
{
GLua = lua_newstate(DirkSimple_lua_allocator, NULL); // calls DirkSimple_panic() on failure.
lua_atpanic(GLua, luahook_panic);
register_lua_libs(GLua);
// Build DirkSimple namespace for Lua to access and fill in C bridges...
lua_newtable(GLua);
set_cfunc(GLua, luahook_DirkSimple_show_single_frame, "show_single_frame");
set_cfunc(GLua, luahook_DirkSimple_start_clip, "start_clip");
set_cfunc(GLua, luahook_DirkSimple_halt_video, "halt_video");
set_cfunc(GLua, luahook_DirkSimple_log, "log");
set_cfunc(GLua, luahook_panic, "panic");
set_cfunc(GLua, luahook_DirkSimple_stackwalk, "stackwalk");
set_cfunc(GLua, luahook_DirkSimple_debugger, "debugger");
set_cfunc(GLua, luahook_DirkSimple_truncate, "truncate");
set_cfunc(GLua, luahook_DirkSimple_to_int, "to_int");
set_cfunc(GLua, luahook_DirkSimple_to_bool, "to_bool");
set_cfunc(GLua, luahook_DirkSimple_clear_screen, "clear_screen");
set_cfunc(GLua, luahook_DirkSimple_draw_sprite, "draw_sprite");
set_cfunc(GLua, luahook_DirkSimple_play_sound, "play_sound");
set_string(GLua, "", "gametitle");
set_string(GLua, GLuaLicense, "lua_license"); // just so deadcode elimination can't remove this string from the binary.
lua_setglobal(GLua, DIRKSIMPLE_LUA_NAMESPACE);
load_lua_gamecode(GLua, GGameDir);
register_cvars(GLua);
collect_lua_garbage(GLua); // get rid of old init crap we don't need.
}
static void setup_game_strings(const char *basedir, const char *gamepath, const char *gamename)
{
char *ptr;
size_t slen;
GGamePath = DirkSimple_xstrdup(gamepath);
if (gamename) {
GGameName = DirkSimple_xstrdup(gamename);
} else {
// Find the filename, without parent directories, to figure out the game name.
for (ptr = GGamePath + strlen(GGamePath); ptr >= GGamePath; ptr--) {
#if defined(_WIN32) || defined(__OS2__)
if (*ptr == '\\') {
ptr++;
break;
}
#endif
if (*ptr == '/') {
ptr++;
break;
}
}
if (ptr < GGamePath) {
ptr = GGamePath;
}
GGameName = DirkSimple_xstrdup(ptr);
// dump the filename extension.
for (ptr = GGameName + strlen(GGameName); ptr >= GGameName; ptr--) {
if (*ptr == '.') {
*ptr = '\0';
break;
}
}
}
// lowercase what's left, just in case.
for (ptr = GGameName; *ptr; ptr++) {
const char ch = *ptr;
if ((ch >= 'A') && (ch <= 'Z')) {
*ptr = ch - ('A' - 'a');
}
}
slen = strlen(basedir) + 32;
GDataDir = (char *) DirkSimple_xmalloc(slen);
snprintf(GDataDir, slen, "%sdata%s", basedir, DIRSEP);
slen = strlen(GDataDir) + strlen(GGameName) + 32;
GGameDir = (char *) DirkSimple_xmalloc(slen);
snprintf(GGameDir, slen, "%sgames%s%s%s", GDataDir, DIRSEP, GGameName, DIRSEP);
}
static void *DirkSimple_theoraplay_allocate(const THEORAPLAY_Allocator *allocator, unsigned int len) { return DirkSimple_xmalloc((size_t) len); }
static void DirkSimple_theoraplay_deallocate(const THEORAPLAY_Allocator *allocator, void *ptr) { DirkSimple_free(ptr); }
static void setup_movie(const char *gamepath, DirkSimple_PixFmt pixfmt)
{
THEORAPLAY_Allocator allocator;
allocator.allocate = DirkSimple_theoraplay_allocate;
allocator.deallocate = DirkSimple_theoraplay_deallocate;
allocator.userdata = NULL;
DirkSimple_Io *io = DirkSimple_openfile_read(gamepath);
if (!io) {
const size_t slen = strlen(gamepath) + 5;
char *gamepath_ext = DirkSimple_xmalloc(slen);
if (gamepath_ext) {
snprintf(gamepath_ext, slen, "%s.ogv", gamepath);
io = DirkSimple_openfile_read(gamepath_ext);
DirkSimple_free(gamepath_ext);
}
if (!io) {
DirkSimple_panic("Couldn't open game path!");
}
}
GTheoraplayIo.read = theoraplayiobridge_read;
GTheoraplayIo.streamlen = theoraplayiobridge_streamlen;
GTheoraplayIo.seek = theoraplayiobridge_seek;
GTheoraplayIo.close = theoraplayiobridge_close;
GTheoraplayIo.userdata = io;
GDecoder = THEORAPLAY_startDecode(&GTheoraplayIo, 30, (THEORAPLAY_VideoFormat) pixfmt, &allocator, DIRKSIMPLE_MULTITHREADED);
if (!GDecoder) {
DirkSimple_panic("Failed to start movie decoding!");
}
}
void DirkSimple_startup(const char *basedir, const char *gamepath, const char *gamename, DirkSimple_PixFmt pixfmt)
{
DirkSimple_shutdown(); // safe to call even if not started up at the moment.
setup_game_strings(basedir, gamepath, gamename);
setup_lua();
setup_movie(GGamePath, pixfmt);
}
void DirkSimple_restart(void) // DO NOT CALL THIS FROM LUA CODE
{
if (GLua) {
lua_close(GLua);
GLua = NULL;
}
setup_lua();
}
static size_t serialize_lua_array(lua_State *L, void *_data, size_t len)
{
uint8_t *data = (uint8_t *) _data;
size_t bw = 0;
lua_Integer i, total;
lua_len(L, -1);
total = lua_tointeger(L, -1);
lua_pop(L, 1);
for (i = 1; i <= total; i++) {
const int luatype = lua_geti(L, -1, i);
if (bw < len) {
*(data++) = (uint8_t) luatype;
}
bw++;
switch (luatype) {
case LUA_TBOOLEAN:
if (bw < len) {
*(data++) = lua_toboolean(L, -1) ? 1 : 0;
}
bw++;
break;
case LUA_TNUMBER:
if ((bw+4) <= len) {
union { float f; uint32_t u32; uint8_t u8[4]; } cvt;
cvt.f = (float) lua_tonumber(L, -1);
cvt.u32 = (((uint32_t) cvt.u8[0]) << 24) | (((uint32_t) cvt.u8[1]) << 16) | (((uint32_t) cvt.u8[2]) << 8) | (((uint32_t) cvt.u8[3]) << 0);
*(data++) = cvt.u8[0];
*(data++) = cvt.u8[1];
*(data++) = cvt.u8[2];
*(data++) = cvt.u8[3];
}
bw += 4;
break;
case LUA_TSTRING:
if ((bw+32) <= len) {
size_t i;
size_t slen = 0;
const char *str = lua_tolstring (L, -1, &slen);
if (slen > 31) {
DirkSimple_panic("Tried to serialized too-long string. This is a DirkSimple bug!");
}
*(data++) = (uint8_t) slen;
for (i = 0; i < slen; i++) {
*(data++) = str[i];
}
while (i < 31) {
*(data++) = '\0';
i++;
}
}
bw += 32; // retroarch needs the size to be consistent, so we pack all strings to 32 bytes.
break;
default:
DirkSimple_panic("Tried to serialized unsupported data type. This is a DirkSimple bug!");
break;
}
lua_pop(L, 1); // drop the aray element.
}
return bw;
}
static int unserialize_lua_array(lua_State *L, const void *_data, size_t len)
{
const uint8_t *data = (const uint8_t *) _data;
lua_Integer idx = 0;
lua_newtable(L); // the Lua array we'll decode into.
while (len > 0) {
const int luatype = (int) *(data++);
len--;
switch (luatype) {
case LUA_TBOOLEAN:
if (len < 1) {
lua_pop(L, 1); // dump the table
return 0;
}
lua_pushboolean(L, *(data++) ? 1 : 0);
len--;
break;
case LUA_TNUMBER:
if (len < 4) {
lua_pop(L, 1); // dump the table
return 0;
} else {
union { float f; uint32_t u32; uint8_t u8[4]; } cvt;
cvt.u8[0] = *(data++);
cvt.u8[1] = *(data++);
cvt.u8[2] = *(data++);
cvt.u8[3] = *(data++);
cvt.u32 = (((uint32_t) cvt.u8[0]) << 24) | (((uint32_t) cvt.u8[1]) << 16) | (((uint32_t) cvt.u8[2]) << 8) | (((uint32_t) cvt.u8[3]) << 0);
lua_pushnumber(L, (lua_Number) cvt.f);
}
len -= 4;
break;
case LUA_TSTRING:
if (len < 32) {
lua_pop(L, 1); // dump the table
return 0;
} else {
const uint8_t chars = *(data++);
if (chars > 31) {
lua_pop(L, 1); // dump the table
return 0;
} else {
lua_pushlstring(L, (const char *) data, chars);
}
}
data += 31;
len -= 32;
break;
default: // unsupported data type or corrupt data, dump it.
lua_pop(L, 1); // dump the table
return 0;
}
lua_seti(L, -2, ++idx);
}
return 1;
}
size_t DirkSimple_serialize(void *data, size_t len)
{
lua_State *L = GLua;
size_t retval = 0;
if (L) {
lua_getglobal(L, DIRKSIMPLE_LUA_NAMESPACE);
if (lua_istable(L, -1)) { // namespace is sane?
lua_getfield(L, -1, "serialize");
if (lua_isfunction(L, -1)) { // if not function, maybe unsupported by this game
lua_call(L, 0, 1); // top of the stack is the return value.
if (lua_istable(L, -1)) {
retval = serialize_lua_array(L, data, len);
}
lua_pop(L, 1); // pop the return value.
}
}
lua_pop(L, 1); // pop the namespace
}
return retval;
}
int DirkSimple_unserialize(const void *data, size_t len)
{
lua_State *L = GLua;
int retval = 0;
if (L) {
lua_getglobal(L, DIRKSIMPLE_LUA_NAMESPACE);
if (lua_istable(L, -1)) { // namespace is sane?
lua_getfield(L, -1, "unserialize");
if (lua_isfunction(L, -1)) { // if not function, maybe unsupported by this game
if (unserialize_lua_array(L, data, len)) { // this will push the array on top of the function as an argument.
lua_call(L, 1, 1); // this will pop the array argument and function.
retval = lua_toboolean(L, -1) ? 1 : 0;
}
lua_pop(L, 1); // pop the return value (or the function we never called).
}
}
lua_pop(L, 1); // pop the namespace
}
return retval;
}
void DirkSimple_shutdown(void)
{
DirkSimple_Sprite *sprite;
DirkSimple_Sprite *spritenext;
for (sprite = GSprites; sprite != NULL; sprite = spritenext) {
spritenext = sprite->next;
DirkSimple_destroysprite(sprite);
DirkSimple_free(sprite->name);
DirkSimple_free(sprite->rgba);
DirkSimple_free(sprite);
}
GSprites = NULL;
DirkSimple_Wave *wave;
DirkSimple_Wave *wavenext;
for (wave = GWaves; wave != NULL; wave = wavenext) {
wavenext = wave->next;
DirkSimple_destroywave(wave);
DirkSimple_free(wave->name);
DirkSimple_free(wave->pcm);
DirkSimple_free(wave);
}
GWaves = NULL;
DirkSimple_free(GRenderCommands);
GRenderCommands = NULL;
GNumRenderCommands = 0;
GNumAllocatedRenderCommands = 0;
THEORAPLAY_stopDecode(GDecoder);
GDecoder = NULL;
GFrameMS = 0;
GTicks = 0;
GTicksOffset = 0;
GSeekToTicksOffset = 0;
GClipStartMs = 0xFFFFFFFF;
GHalted = 0;
GShowingSingleFrame = 0;
GClipStartTicks = 0;
GSeekGeneration = 0;
GNeedInitialLuaTick = 1;
GPreviousInputBits = 0;
GDiscoveredVideoFormat = 0;
GDiscoveredAudioFormat = 0;
GAudioChannels = 0;
GAudioFreq = 0;
GPendingVideoFrame = NULL;
GFakeDiscLag = 0;
GDiscLagUntilTicks = 0;
DirkSimple_free(GBlankVideoFrame);
GBlankVideoFrame = NULL;
DirkSimple_free(GGameName);
GGameName = NULL;
DirkSimple_free(GGamePath);
GGamePath = NULL;
DirkSimple_free(GGameDir);
GGameDir = NULL;
DirkSimple_free(GDataDir);
GDataDir = NULL;
if (GLua) {
lua_close(GLua);
GLua = NULL;
}
}
// Sets t[sym]=f, where t is on the top of the Lua stack.
static void set_boolean(lua_State *L, int x, const char *sym)
{
lua_pushboolean(L, x);
lua_setfield(L, -2, sym);
}
typedef int (*inputbit_testfn)(const uint64_t curbits, const uint64_t prevbits, const uint64_t flag);
static int inputbit_is_pressed(const uint64_t curbits, const uint64_t prevbits, const uint64_t flag)
{
return ((curbits & flag) != 0) && ((prevbits & flag) == 0);
}
static int inputbit_is_held(const uint64_t curbits, const uint64_t prevbits, const uint64_t flag)
{
return ((curbits & prevbits & flag) != 0);
}
static int inputbit_is_released(const uint64_t curbits, const uint64_t prevbits, const uint64_t flag)
{
return ((curbits & flag) == 0) && ((prevbits & flag) != 0);
}
static int inputbit_is_untouched(const uint64_t curbits, const uint64_t prevbits, const uint64_t flag)
{
return ((curbits & prevbits & flag) == 0);
}
static void set_input_subtable(lua_State *L, const char *tablename, inputbit_testfn testfn, uint64_t curbits, uint64_t prevbits)
{
lua_newtable(L); // the subtable
set_boolean(L, testfn(curbits, prevbits, DIRKSIMPLE_INPUT_UP), "up");
set_boolean(L, testfn(curbits, prevbits, DIRKSIMPLE_INPUT_DOWN), "down");
set_boolean(L, testfn(curbits, prevbits, DIRKSIMPLE_INPUT_LEFT), "left");
set_boolean(L, testfn(curbits, prevbits, DIRKSIMPLE_INPUT_RIGHT), "right");
set_boolean(L, testfn(curbits, prevbits, DIRKSIMPLE_INPUT_ACTION1), "action");
set_boolean(L, testfn(curbits, prevbits, DIRKSIMPLE_INPUT_ACTION2), "action2");
set_boolean(L, testfn(curbits, prevbits, DIRKSIMPLE_INPUT_COINSLOT), "coinslot");
set_boolean(L, testfn(curbits, prevbits, DIRKSIMPLE_INPUT_START), "start");
lua_setfield(L, -2, tablename);
}
static void push_inputs_table(lua_State *L, const uint64_t curbits)
{
const uint64_t prevbits = GPreviousInputBits;
lua_newtable(L); // the inputs table
set_input_subtable(L, "pressed", inputbit_is_pressed, curbits, prevbits);
set_input_subtable(L, "held", inputbit_is_held, curbits, prevbits);
set_input_subtable(L, "released", inputbit_is_released, curbits, prevbits);
set_input_subtable(L, "untouched", inputbit_is_untouched, curbits, prevbits);
// inputs table is ready, on top of Lua stack.
}
static void call_lua_tick(lua_State *L, uint64_t ticks, uint64_t clipstartticks, uint64_t inputbits)
{
lua_getglobal(L, DIRKSIMPLE_LUA_NAMESPACE);
if (!lua_istable(L, -1)) { // namespace is sane?
DirkSimple_panic("DirkSimple Lua namespace is not a table!");
}
lua_getfield(L, -1, "tick");
if (!lua_isfunction(L, -1)) {
DirkSimple_panic("DirkSimple.tick is not a function!");
}
lua_pushnumber(L, (lua_Number) ticks);
lua_pushnumber(L, (lua_Number) clipstartticks);
push_inputs_table(L, inputbits);
lua_call(L, 3, 0); // this will pop the function and args
lua_pop(L, 1); // pop the namespace
// Clean up any Lua tick waste.
collect_lua_garbage(L); // we can move this to start_clip if it turns out to be too heavy.
}
static const char *pixfmtstr(const THEORAPLAY_VideoFormat pixfmt)
{
switch (pixfmt) {
#define PIXFMTCASE(x) case THEORAPLAY_VIDFMT_##x: return #x
PIXFMTCASE(YV12);
PIXFMTCASE(IYUV);
PIXFMTCASE(RGB);
PIXFMTCASE(RGBA);
PIXFMTCASE(BGRA);
PIXFMTCASE(RGB565);
#undef PIXFMTCASE
default: break;
}
return "[unknown]";
}
static void send_rendering_primitives(void)
{
DirkSimple_beginframe();
for (int i = 0; i < GNumRenderCommands; i++) {
const RenderCommand *cmd = &GRenderCommands[i];
switch (cmd->prim) {
case RENDPRIM_CLEAR:
DirkSimple_clearscreen(cmd->data.clear.r, cmd->data.clear.g, cmd->data.clear.b);
break;
case RENDPRIM_SPRITE:
DirkSimple_drawsprite(get_cached_sprite(cmd->data.sprite.name),
cmd->data.sprite.sx, cmd->data.sprite.sy, cmd->data.sprite.sw, cmd->data.sprite.sh,
cmd->data.sprite.dx, cmd->data.sprite.dy, cmd->data.sprite.dw, cmd->data.sprite.dh,
cmd->data.sprite.r, cmd->data.sprite.g, cmd->data.sprite.b);
break;
case RENDPRIM_SOUND:
DirkSimple_playwave(get_cached_wave(cmd->data.sound.name));
break;
default:
DirkSimple_panic("Unexpected rendering primitive");
break;
}
}
DirkSimple_endframe();
GNumRenderCommands = 0;
}
static void DirkSimple_tick_impl(uint64_t monotonic_ms, uint64_t inputbits)
{
lua_State *L = GLua;
const THEORAPLAY_AudioPacket *audio = NULL;
if (!L) {
DirkSimple_panic("Lua VM is missing?!");
} else if (!GDecoder) {
DirkSimple_panic("Video decoder is missing?!");
}
THEORAPLAY_pumpDecode(GDecoder, 5);
if (!THEORAPLAY_isInitialized(GDecoder)) {
return; // still waiting for the decoder to spin up, don't do anything yet.
}
if (!GDiscoveredVideoFormat) {
const THEORAPLAY_VideoFrame *video;
const char *gametitle_lua = NULL;
char *gametitle = NULL;
if (!THEORAPLAY_hasVideoStream(GDecoder)) {
DirkSimple_panic("Movie file has no video stream!");
}
video = THEORAPLAY_getVideo(GDecoder);
if (!video) {
return; // still waiting on video to arrive so we can figure out format.
}
// see if the Lua code told us the proper title for this game.
if (L != NULL) {
lua_getglobal(L, DIRKSIMPLE_LUA_NAMESPACE);
if (lua_istable(L, -1)) { // namespace is sane?
lua_getfield(L, -1, "gametitle");
gametitle_lua = lua_tostring(L, -1);
if (gametitle_lua) {
gametitle = DirkSimple_xstrdup(gametitle_lua);
}
lua_pop(L, 1);
// This is the dimensions of the video, which might not actually match what the arcade used.
set_integer(L, (int) video->width, "video_width");
set_integer(L, (int) video->height, "video_height");
}
lua_pop(L, 1);
}
GFrameMS = (video->fps == 0.0) ? 0 : ((uint32_t) (1000.0 / video->fps));
GDiscoveredVideoFormat = 1;
DirkSimple_log("We are playing '%s'", gametitle);
DirkSimple_log("Virtual laserdisc video format: %s, %dx%d, %ffps", pixfmtstr(video->format), (int) video->width, video->height, video->fps);
DirkSimple_videoformat(gametitle, video->width, video->height, video->fps);
DirkSimple_free(gametitle);
switch (video->format) {
case THEORAPLAY_VIDFMT_YV12:
case THEORAPLAY_VIDFMT_IYUV:
GBlankVideoFrame = DirkSimple_xcalloc(1, (video->width * video->height) + ((video->width * video->height) / 2));
memset(GBlankVideoFrame + (video->width * video->height), 128, (video->width * video->height) / 2);
break;
case THEORAPLAY_VIDFMT_RGB:
GBlankVideoFrame = DirkSimple_xcalloc(video->width * video->height, 3);
break;
case THEORAPLAY_VIDFMT_RGBA:
case THEORAPLAY_VIDFMT_BGRA:
GBlankVideoFrame = DirkSimple_xcalloc(video->width * video->height, sizeof (uint32_t));
break;
case THEORAPLAY_VIDFMT_RGB565:
GBlankVideoFrame = DirkSimple_xcalloc(video->width * video->height, sizeof (uint16_t));
break;
}
THEORAPLAY_freeVideo(video); // dump this, the game is going to seek at startup anyhow.
DirkSimple_discvideo(GBlankVideoFrame);
}
if (!GDiscoveredAudioFormat) {
if (!THEORAPLAY_hasAudioStream(GDecoder)) {
DirkSimple_panic("Movie file has no audio stream!"); // maybe this shouldn't be fatal?
}
audio = THEORAPLAY_getAudio(GDecoder);
if (!audio) {
return; // still waiting on audio to arrive so we can figure out format.
}
GDiscoveredAudioFormat = 1;
GAudioChannels = audio->channels;
GAudioFreq = audio->freq;
DirkSimple_log("Virtual laserdisc audio format: float32, %d channels, %dHz", (int) audio->channels, (int) audio->freq);
DirkSimple_audioformat(audio->channels, audio->freq);
THEORAPLAY_freeAudio(audio); // dump this, the game is going to seek at startup anyhow.
}
if (GTicksOffset == 0) {
if (monotonic_ms < 2) {
return; // just let this tick up until subtracting 1 is still > 0.
}
GTicksOffset = monotonic_ms - 1;
} else if (GTicksOffset > monotonic_ms) {
DirkSimple_panic("Time ran backwards! Aborting!");
}
GTicks = monotonic_ms - GTicksOffset;
//DirkSimple_log("Tick %u\n", (unsigned int) GTicks);
// Call our Lua tick function if we aren't seeking...
const unsigned int expected_seek_generation = GSeekGeneration;
if (GNeedInitialLuaTick) {
GNeedInitialLuaTick = 0;
call_lua_tick(L, 0, 0, 0);
} else if (GClipStartTicks) {
call_lua_tick(L, GTicks, (GTicks - GClipStartTicks), inputbits);
}
if (GHalted) {
return; // video playback is halted? We've done the tick, just return now.
}
// the tick function demanded a seek, don't bother messing with the movie this frame.
// also doing this after the tick makes sure we don't render any frames past the end
// of the scene due to latency and process scheduling.
if (GSeekGeneration != expected_seek_generation) {
return;
}
if (!GPendingVideoFrame) {
GPendingVideoFrame = THEORAPLAY_getVideo(GDecoder);
}
// Clean out decoded frames from before the latest seek that had already made it into the queue.
// Also, seeking might drop us back before the start of the clip, because we have to find a key frame,
// so throw out any frames with timestamps before the current clip's start, too, even if they're in
// the current seek generation. This also helps it not show video at startup before the game logic has
// selected a clip to play.
while (GPendingVideoFrame && ((GPendingVideoFrame->seek_generation != GSeekGeneration) || (GPendingVideoFrame->playms < GClipStartMs))) {
//if (GPendingVideoFrame->seek_generation != GSeekGeneration) { DirkSimple_log("Dumping video frame from previous seek generation"); }
//else if (GPendingVideoFrame->playms < GClipStartMs) { DirkSimple_log("Dumping video frame from before clip start time (%u vs %u)", (unsigned int) GPendingVideoFrame->playms, (unsigned int) GClipStartMs); }
THEORAPLAY_freeVideo(GPendingVideoFrame);
GPendingVideoFrame = THEORAPLAY_getVideo(GDecoder);
}
if (GDiscLagUntilTicks) {
if (GDiscLagUntilTicks > GTicks) {
return; // we're faking the stall while the laserdisc player seeks to a new position. Done for now.
}
GDiscLagUntilTicks = 0;
}
// has seek completed? Sync us back up.
if (GPendingVideoFrame && (GClipStartTicks == 0)) {
GClipStartTicks = GTicks;
GSeekToTicksOffset = ((int64_t) GTicks) - ((int64_t) GClipStartMs);
if (GShowingSingleFrame) {
DirkSimple_discvideo(GPendingVideoFrame->pixels);
THEORAPLAY_freeVideo(GPendingVideoFrame);
GPendingVideoFrame = NULL;
}
// we didn't call the tick earlier in this function because we were still waiting; do it now that the seek is resolved.
call_lua_tick(L, GTicks, (GTicks - GClipStartTicks), inputbits);
}
if (!GShowingSingleFrame) {
while ((audio = THEORAPLAY_getAudio(GDecoder)) != NULL) {
if (audio->seek_generation == GSeekGeneration) { // frame from before our latest seek, dump it.
DirkSimple_discaudio(audio->samples, audio->frames);
}
THEORAPLAY_freeAudio(audio);
}
// Feed the next video frame when it's time.
if (GPendingVideoFrame) {
int64_t playms = ((int64_t) GPendingVideoFrame->playms) + GSeekToTicksOffset;
//DirkSimple_log("Consider frame->playms=%u GTicks=%u offset=%d cvt=%d\n", (unsigned int) GPendingVideoFrame->playms, (unsigned int) GTicks, (int) GSeekToTicksOffset, (int) playms);
if (playms <= (int64_t) GTicks) {
//DirkSimple_log("Play video frame (%u ms)!\n", (unsigned int) (video->playms - GSeekToTicksOffset));
if ( GFrameMS && ((GTicks - playms) >= GFrameMS) ) {
// Skip frames to catch up, but keep track of the last one
// in case we catch up to a series of dupe frames, which
// means we'd have to draw that final frame and then wait for
// more.
const THEORAPLAY_VideoFrame *last = GPendingVideoFrame;
while ((GPendingVideoFrame = THEORAPLAY_getVideo(GDecoder)) != NULL) {
THEORAPLAY_freeVideo(last);
last = GPendingVideoFrame;
playms = ((int64_t) GPendingVideoFrame->playms) + GSeekToTicksOffset;
//DirkSimple_log("Catchup frame %u\n", (unsigned int) playms);
if ((((int64_t) GTicks) - playms) < ((int64_t) GFrameMS)) {
break;
}
}
if (!GPendingVideoFrame) {
GPendingVideoFrame = last;
}
}
if (!GPendingVideoFrame) { // do nothing; we're far behind and out of options.
static uint64_t last_warned = 0;
if ((!last_warned) || ((GTicks - last_warned) > 10000)) {
last_warned = GTicks;
DirkSimple_log("WARNING: Video playback can't keep up!");
}
} else {
DirkSimple_discvideo(GPendingVideoFrame->pixels);
THEORAPLAY_freeVideo(GPendingVideoFrame);
GPendingVideoFrame = NULL;
}
}
}
}
}
void DirkSimple_tick(uint64_t monotonic_ms, uint64_t inputbits)
{
DirkSimple_tick_impl(monotonic_ms, inputbits);
send_rendering_primitives();
GPreviousInputBits = inputbits;
}
// end of dirksimple.c ...