/*
 * tngp/tngp.c
 *
 * Copyright (C) 2022 bzt
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * @brief Main function for the TirNanoG Player
 * https://tirnanog.codeberg.page
 *
 */

#define STB_IMAGE_IMPLEMENTATION
#define SSFN_IMPLEMENTATION
#define SSFN_CONSOLEBITMAP_TRUECOLOR
#include "tngp.h"
#include "theora.h"
#include "version.h"
#include "ssfn.h"
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
void mbedtls_platform_zeroize(void *s, size_t n) { memset(s, 0, n); }
#endif

/**
 * Translations
 */
char **lang = NULL;
char *dict[NUMLANGS][NUMTEXTS + 1] = {{
#include "lang/en.h"
},{
#include "lang/hu.h"
},{
#include "lang/de.h"
},{
#include "lang/fr.h"
},{
#include "lang/es.h"
},{
#include "lang/pt.h"
},{
#include "lang/ru.h"
},{
#include "lang/zh.h"
},{
#include "lang/ja.h"
},{
#include "lang/ar.h"
},{
#include "lang/bg.h"
},{
#include "lang/bn.h"
},{
#include "lang/ca.h"
},{
#include "lang/cz.h"
},{
#include "lang/da.h"
},{
#include "lang/el.h"
},{
#include "lang/et.h"
},{
#include "lang/fi.h"
},{
#include "lang/ga.h"
},{
#include "lang/he.h"
},{
#include "lang/hi.h"
},{
#include "lang/hr.h"
},{
#include "lang/it.h"
},{
#include "lang/ko.h"
},{
#include "lang/lt.h"
},{
#include "lang/lv.h"
},{
#include "lang/mk.h"
},{
#include "lang/mt.h"
},{
#include "lang/nl.h"
},{
#include "lang/no.h"
},{
#include "lang/pl.h"
},{
#include "lang/ro.h"
},{
#include "lang/sk.h"
},{
#include "lang/sl.h"
},{
#include "lang/sq.h"
},{
#include "lang/sr.h"
},{
#include "lang/sv.h"
},{
#include "lang/tr.h"
},{
#include "lang/uk.h"
},{
#include "lang/vi.h"
}};

/* provide workarounds for dynamically linked shared libraries */
#ifdef _DYNSDLFIX_
char *strtok_r(char *s1, const char *s2, char **ptr);
char *SDL_strtokr(char *s1, const char *s2, char **ptr) { return strtok_r(s1, s2, ptr); }
#endif

char *copyright[3] = {
    "You should have received a copy of the GNU General Public License",
    "along with this program; if not, write to the Free Software",
    "Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA."
};
SDL_Window *window;
SDL_Renderer *renderer;
SDL_Event event;
SDL_Texture *logo = NULL, *screen = NULL;
SDL_Rect lgopos = { 0, 67, 440, 66 }, iconpos = { 224, 160, 32, 32 }, scroll;
SDL_GameController *controller = NULL;
uint32_t *scr_data = NULL, main_timetotal, main_start;
uint16_t *main_scrshot = NULL;
volatile uint32_t main_tick = 0;
volatile int main_draw = 1;
int tab = -1, win_w, win_h, scr_w, scr_h, scr_p, verbose = 0, touch = 0, audio = 0, error_reason = 0, main_ret = 0;
int main_delay = 0, main_fade, main_fademax, main_cursorcnt = 0, main_cursor = 0, main_cursorlast, main_curx, main_cury;
int main_w, main_h, main_musvol = MIX_MAX_VOLUME, main_sfxvol = MIX_MAX_VOLUME, main_spcvol = MIX_MAX_VOLUME, main_gptres = 8000;
int main_takescrshot = 0;
char detlng[3] = { 0 }, forcelng = 0, *main_fn = NULL;
char ws[0x110000];
const char *main_me = "tngp", *main_url = "ht""tp""s:""//ti""rna""no""g.co""deb""er""g.p""ag""e";
world_sprite_t *main_cursors[5] = { 0 };
int main_init2();
#if DEBUG
extern int script_trace;
#endif

/* do NOT include SDL_mixer, it won't compile on mingw */
#define MIX_INIT_OGG 0x00000010
extern DECLSPEC int SDLCALL Mix_Init(int flags);
extern DECLSPEC void SDLCALL Mix_Quit(void);
extern DECLSPEC int SDLCALL Mix_OpenAudio(int frequency, Uint16 format, int channels, int chunksize);
extern DECLSPEC void SDLCALL Mix_CloseAudio(void);

/**
 * Windows workaround
 */
#ifdef __WIN32__
#define CLIFLAG "/"
/* only include these when necessary, some of their defines conflict with SDL_mixer's... */
#include <windows.h>
#include <winnls.h>
#include <fileapi.h>
#include <shellapi.h>

/* these two functions were borrowed from sdl_windows_main.c */
static void UnEscapeQuotes(char *arg)
{
    char *last = NULL, *c_curr, *c_last;

    while (*arg) {
        if (*arg == '"' && (last != NULL && *last == '\\')) {
            c_curr = arg;
            c_last = last;

            while (*c_curr) {
                *c_last = *c_curr;
                c_last = c_curr;
                c_curr++;
            }
            *c_last = '\0';
        }
        last = arg;
        arg++;
    }
}

/* Parse a command line buffer into arguments */
static int ParseCommandLine(char *cmdline, char **argv)
{
    char *bufp;
    char *lastp = NULL;
    int argc, last_argc;

    argc = last_argc = 0;
    for (bufp = cmdline; *bufp;) {
        /* Skip leading whitespace */
        while (SDL_isspace(*bufp)) {
            ++bufp;
        }
        /* Skip over argument */
        if (*bufp == '"') {
            ++bufp;
            if (*bufp) {
                if (argv) {
                    argv[argc] = bufp;
                }
                ++argc;
            }
            /* Skip over word */
            lastp = bufp;
            while (*bufp && (*bufp != '"' || *lastp == '\\')) {
                lastp = bufp;
                ++bufp;
            }
        } else {
            if (*bufp) {
                if (argv) {
                    argv[argc] = bufp;
                }
                ++argc;
            }
            /* Skip over word */
            while (*bufp && !SDL_isspace(*bufp)) {
                ++bufp;
            }
        }
        if (*bufp) {
            if (argv) {
                *bufp = '\0';
            }
            ++bufp;
        }

        /* Strip out \ from \" sequences */
        if (argv && last_argc != argc) {
            UnEscapeQuotes(argv[last_argc]);
        }
        last_argc = argc;
    }
    if (argv) {
        argv[argc] = NULL;
    }
    return (argc);
}

/* Windows entry point */
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    char *cmdline = GetCommandLineA();
    int ret, argc = ParseCommandLine(cmdline, NULL);
    char **argv = SDL_stack_alloc(char*, argc+2);
    (void)hInstance;
    (void)hPrevInstance;
    (void)lpCmdLine;
    (void)nCmdShow;
    ParseCommandLine(cmdline, argv);
    SDL_SetMainReady();
    ret = main(argc, argv);
    SDL_stack_free(argv);
    exit(ret);
    return ret;
}
#else
#define CLIFLAG "-"
#ifdef __MACOSX__
#define OPEN "open"
#else
#define OPEN "xdg-open"
#endif
#endif

void main_quit();

/**
 * Return text width in pixels
 */
int main_textwidth(char *str)
{
    int ret = 0, i = 0, u;

    if(!str || !*str) return 0;
    while(*str) {
        u = ssfn_utf8(&str);
        if(u == '\n') { if(ret < i) ret = i; i = 0; }
        else i += (int)ws[u];
    }
    if(ret < i) ret = i;
    return ret;
}

/**
 * Display text
 */
void main_text(SDL_Texture *dst, int x, int y, char *str)
{
    char *s = str;
    int p, u;

    ssfn_dst.w = scr_w;
    ssfn_dst.h = scr_h;
    if(y + 16 <= 0 || x > scr_w || y > scr_h) return;
    if(dst == screen) { ssfn_dst.ptr = (unsigned char *)scr_data; p = scr_p; }
    else SDL_LockTexture(dst, NULL, (void**)&ssfn_dst.ptr, &p);
    ssfn_dst.p = p;
    ssfn_dst.x = x;
    ssfn_dst.y = y;
    while(s && *s) {
        u = ssfn_utf8(&s);
        if(u == '\n') { ssfn_dst.x = x; ssfn_dst.y += 16; } else
        if(ssfn_dst.x >= 0 && ssfn_dst.x < scr_w)
            ssfn_putc(u);
        else
            ssfn_dst.x += ws[u];
    }
    if(dst != screen) SDL_UnlockTexture(dst);
}

/**
 * Common error handler
 */
void main_error(int msg)
{
    char *mess = lang ? lang[msg] : dict[0][msg];
#ifdef __EMSCRIPTEN__
    /* there's no stderr, only console.log */
    printf("tngp: %s\r\n", mess);
    EM_ASM_({alert('TirNanoG\\r\\n\\r\\n'+UTF8ToString($0));}, mess);
#else
#ifdef __WIN32__
    wchar_t buf[256];
    int wlen;

    wlen = MultiByteToWideChar(CP_UTF8, 0, mess, strlen(mess), NULL, 0);
    if(wlen > 255) wlen = 255;
    if(!wlen) {
        /* we don't have stderr, only stdout */
        fprintf(stdout, "tngp: %s\r\n", mess);
        MessageBoxA(NULL, mess, "TirNanoG", MB_ICONEXCLAMATION | MB_OK);
    } else {
        MultiByteToWideChar(CP_UTF8, 0, mess, strlen(mess), buf, wlen);
        buf[wlen] = 0;
        fwprintf(stdout, L"tngp: %s\r\n", buf);
        MessageBoxW(NULL, buf, L"TirNanoG", MB_ICONEXCLAMATION | MB_OK);
    }
#else
    SDL_DisplayMode dm;
    SDL_Window *window = NULL;
    SDL_Surface *surface = NULL;
    char *s = mess;
    int w;

    fprintf(stderr, "tngp: %s\r\n", mess);
    fflush(stdout); fflush(stderr);
    /* create a dummy MessageBox on Linux */
    if(msg != ERR_DISPLAY && ssfn_src && SDL_WasInit(SDL_INIT_VIDEO)) {
        SDL_ShowCursor(SDL_ENABLE);
        w = main_textwidth(mess) + 48;
        SDL_GetDesktopDisplayMode(0, &dm);
        window = SDL_CreateWindow("TirNanoG", (dm.w - w) / 2, (dm.h - 64) / 2, w, 64, SDL_WINDOW_ALWAYS_ON_TOP |
            SDL_WINDOW_SKIP_TASKBAR);
        if(window) {
            surface = SDL_GetWindowSurface(window);
            if(surface) {
                memset(surface->pixels, 0x88, 64 * surface->pitch);
                ssfn_dst.ptr = (uint8_t*)surface->pixels + 24 * surface->pitch + 96;
                ssfn_dst.p = surface->pitch;
                ssfn_dst.x = ssfn_dst.y = ssfn_dst.w = ssfn_dst.h = 0; ssfn_dst.bg = 0; ssfn_dst.fg = 0xff000000;
                while(s && *s) ssfn_putc(ssfn_utf8(&s));
                while(1) {
                    SDL_UpdateWindowSurface(window);
                    SDL_RaiseWindow(window);
                    SDL_WaitEvent(&event);
                    if(event.type == SDL_QUIT || event.type == SDL_MOUSEBUTTONUP || event.type == SDL_KEYUP ||
                        event.type == SDL_FINGERUP || event.type == SDL_JOYBUTTONUP || event.type == SDL_CONTROLLERBUTTONUP ||
                        (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_CLOSE)) break;
                }
            }
            SDL_DestroyWindow(window);
        }
    }
#endif
#endif
    main_quit();
    /* this will crash on android, but there's nothing we can do about it, we MUST quit */
    exit(1);
}

/**
 * Initialize error message tab
 */
void main_error_init()
{
    int i;

    if(screen) SDL_DestroyTexture(screen);
    scr_w = win_w; scr_h = 42;
    screen = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ABGR8888, SDL_TEXTUREACCESS_STREAMING, scr_w, scr_h);
    if(screen) {
        SDL_SetTextureBlendMode(screen, SDL_BLENDMODE_BLEND);
        SDL_LockTexture(screen, NULL, (void**)&scr_data, &scr_p);
        for(i = 0; i < scr_w * scr_h; i++) scr_data[i] = 0xff000040;
        ssfn_dst.fg = 0xffffffff;
        main_text(screen, (scr_w - main_textwidth(lang[error_reason])) / 2, 13, lang[error_reason]);
        SDL_UnlockTexture(screen);
    }
}

/**
 * Error message tab controller
 */
int main_error_ctrl()
{
    if(event.type == SDL_KEYUP || event.type == SDL_MOUSEBUTTONUP || event.type == SDL_FINGERUP ||
      event.type == SDL_JOYBUTTONUP || event.type == SDL_CONTROLLERBUTTONUP) return 0;
    return 1;
}

/**
 * Error message tab view
 */
void main_error_view()
{
    SDL_Rect rect;

    main_splash();
    rect.x = 0; rect.w = scr_w;
    rect.y = win_h - scr_h; rect.h = scr_h;
    if(screen) SDL_RenderCopy(renderer, screen, NULL, &rect);
}

/**
 * Workaround a stupid iOS and Android bug
 */
int main_stupidios(void *data, SDL_Event *event)
{
    (void)data;
    switch(event->type) {
        case SDL_APP_WILLENTERBACKGROUND: main_draw = 0; break;
        case SDL_APP_WILLENTERFOREGROUND: main_draw = 1; break;
    }
    return 1;
}

/**
 * Allocate memory with error handler
 */
void *main_alloc(size_t size)
{
    void *tmp = malloc(size);
    if(!tmp) main_error(ERR_MEM);
    memset(tmp, 0, size);
    return tmp;
}

/**
 * Reallocate memory with error handler
 */
void *main_realloc(void *ptr, size_t size)
{
    void *tmp = realloc(ptr, size);
    if(!tmp) main_error(ERR_MEM);
    return tmp;
}

/**
 * Open the repository in the default browser
 */
void main_open_url(const char *url)
{
#ifdef __EMSCRIPTEN__
    EM_ASM_({document.location.href=UTF8ToString($0);}, url);
#else
#ifdef __WIN32__
    wchar_t buf[256];
    int wlen;

    wlen = MultiByteToWideChar(CP_UTF8, 0, url, strlen(url), NULL, 0);
    if(wlen > 255) wlen = 255;
    if(!wlen)
        ShellExecuteA(0, 0, url, 0, 0, SW_SHOW);
    else {
        MultiByteToWideChar(CP_UTF8, 0, url, strlen(url), buf, wlen);
        ShellExecuteW(0, 0, buf, 0, 0, SW_SHOW);
    }
#else
    char cmd[512];
    if(!fork()) {
        fclose(stdin);
        fclose(stdout);
        fclose(stderr);
        sprintf(cmd, OPEN " %s", url);
        exit(system(cmd));
    }
#endif
#endif
    if(verbose) printf("tngp: opening %s\r\n", url);
}

/**
 * Add a quest hook (different for client and server)
 */
void main_addquest(world_entity_t *ent, int idx, int complete)
{
    uint8_t buf[8];

    if(world.quests && idx >= 0 && idx < world.numqst) {
        if(world.hdr.magic[0] != '#') {
            idx = SDL_SwapLE32(idx);
            memcpy(buf, &idx, 4); buf[4] = complete;
            client_send(14, buf, 5);
        } else
            world_quest(ent, idx, complete, 1);
    }
}

/**
 * Switch game tabs
 */
void main_switchtab(int newtab)
{
    char tmp[32], *fn;

    /* in VN mode we can't switch to TAB_GAME, all we can do is running the startup script and then show the end credits */
    if(newtab == TAB_GAME && !world.hasmenu) newtab = TAB_CREDITS;
    if(newtab == TAB_INTRO && world.intro == -1) newtab = TAB_MAINMENU;
    /* normally we always call _free() and _init(), except when we switch from TAB_STARTGAME to TAB_GAME
     * or TAB_MAINMENU to TAB_GAME, because main menu might have loaded state */
    switch(tab) {
        case TAB_LAUNCHER: launcher_free(); break;
        case TAB_LICENSE: license_free(); break;
        case TAB_DOWNLOAD: client_free(); break;
        case TAB_MAINMENU: mainmenu_free(); break;
        /*case TAB_CUTSCN: cutscn_free(); break; <-- deliberately do not call */
        case TAB_CREDITS: credits_free(); break;
        case TAB_STARTGAME: if(newtab != TAB_GAME) { game_free(); } break;
        case TAB_GAME: game_free(); break;
    }
    tab = newtab;
    switch(tab) {
        case TAB_ERROR: main_error_init(); break;
        case TAB_LAUNCHER: launcher_init(); break;
        case TAB_LICENSE: license_init(); break;
        case TAB_DOWNLOAD: client_init(); break;
        case TAB_FADING: main_fade = main_fademax = (world.hdr.fps < 10 || world.hdr.fps > 120 ? 10 : world.hdr.fps) / 2; break;
        case TAB_INTRO: cutscn_init(world.intro, NULL); break;
        case TAB_MAINMENU:
            if(world.hasmenu) {
                mainmenu_init();
            } else {
                /* Visual Novel mode, no main menu and character generation */
                fn = (char*)main_alloc(strlen(files_saves) + 256 + 8);
                memcpy(tmp, world.hdr.id, 16); tmp[16] = 0;
                sprintf(fn, "%s" SEP "%s" SEP "game.sav", files_saves, tmp);
                /* try to load game state */
                if(!world_loadstate(fn)) {
                    /* register dummy player and save state */
                    if(world_register("game", (uint8_t*)tmp, NULL, NULL) != -1)
                        world_savestate(fn, NULL, 0, 0);
                }
                free(fn);
                tab = TAB_STARTGAME; game_init();
            }
        break;
        case TAB_CREDITS: credits_init(); break;
        case TAB_GAME: hud_init(); break;
    }
}

/**
 * Display splash screen
 */
void main_splash()
{
    SDL_Rect src, dst;

    src.x = src.y = 0; src.w = dst.w = 440; src.h = dst.h = 158;
    dst.x = (win_w - 440) / 2; dst.y = (win_h - 196) / 2;
    SDL_RenderCopy(renderer, logo, &src, &dst);
    if(world.hdr.magic[0] == '#' && !world.hdr.enc) {
        if(tab == TAB_FADING) SDL_SetTextureAlphaMod(logo, 255);
        if(world.gpl) { src.x = 120; src.w = 100; } else { src.x = 0; src.w = 120; }
        src.y = 158; dst.h = src.h = 42; dst.w = src.w; dst.x = 0; dst.y = win_h - src.h;
        SDL_RenderCopy(renderer, logo, &src, &dst);
    }
}

/**
 * Main game loop
 */
/* emscripten does not allow return value... */
void main_loop() {
    SDL_Rect rect;
    world_sprite_t *cur;
#ifdef __EMSCRIPTEN__
#define exit_loop() main_quit()
    /* do a very early check, because JavaScript can't list files in a directory, so no launcher in emscripten port */
    if(tab == -1) {
        /* it would be nice if we could do this in main(), but JS FileReader uses Asyncify which (undocumented!) can only be
         * called from *within* the main loop, otherwise throws an unwind error... so we use a dirty hack with tab = -1 */
        files_seek(NULL, 0);
        files_read(NULL, &world.hdr, sizeof(world.hdr));
        if(memcmp(&world.hdr.magic, TNG_MAGIC, 16)) {
            error_reason = ERR_TNG;
            main_switchtab(TAB_ERROR);
        } else
        if(!tng_compat(&world.hdr)) {
            error_reason = ERR_COMPAT;
            main_switchtab(TAB_ERROR);
        } else {
            main_fn = "null";
            main_switchtab(world.hdr.enc ? TAB_LICENSE : TAB_SPLASH);
        }
    }
#else
#define exit_loop() do{ main_ret = 0; return; }while(0)
    /* update the world from server */
    if(world.hdr.magic[0] != '#') {
        client_recv();
        client_sendupdate();
    }
#endif
    main_gettick();
    event.type = 0; main_ret = 1;
    if(main_cursorcnt > 0) {
        main_cursorcnt--;
        if(!main_cursorcnt && !main_cursors[E_PTR])
            SDL_ShowCursor(SDL_DISABLE);
    }
    switch(tab) {
        case TAB_STARTGAME: case TAB_GAME: game_run(); break;
        case TAB_CUTSCN: cutscn_run(); break;
    }
    while(SDL_PollEvent(&event)) {
        switch(event.type) {
            case SDL_QUIT: exit_loop(); break;
            case SDL_WINDOWEVENT:
                media_mic_stop();
                switch(event.window.event) {
                    case SDL_WINDOWEVENT_CLOSE: exit_loop(); break;
/*
                    case SDL_WINDOWEVENT_RESIZED: case SDL_WINDOWEVENT_SIZE_CHANGED:
                        win_w = event.window.data1;
                        win_h = event.window.data2;
                    break;
*/
                }
            break;
            case SDL_CONTROLLERDEVICEADDED:
                if(controller) SDL_GameControllerClose(controller);
                controller = SDL_GameControllerOpen(event.cdevice.which);
            break;
            case SDL_CONTROLLERDEVICEREMOVED:
                if(controller) SDL_GameControllerClose(controller);
                controller = NULL;
            break;
            case SDL_MOUSEMOTION:
                if(tab >= TAB_MAINMENU) {
                    if(!main_cursorcnt && !main_cursors[E_PTR])
                        SDL_ShowCursor(SDL_ENABLE);
                    main_cursorcnt = world.hdr.fps;
                }
                main_curx = event.motion.x;
                main_cury = event.motion.y;
            break;
            case SDL_MOUSEBUTTONDOWN:
                if(tab >= TAB_MAINMENU) {
                    if(!main_cursorcnt && !main_cursors[E_PTR])
                        SDL_ShowCursor(SDL_ENABLE);
                    main_cursorcnt = world.hdr.fps;
                }
                main_cursorlast = main_cursor;
                main_cursor = E_CLK;
            break;
            case SDL_MOUSEBUTTONUP:
                if(tab >= TAB_MAINMENU) {
                    if(!main_cursorcnt && !main_cursors[E_PTR])
                        SDL_ShowCursor(SDL_ENABLE);
                    main_cursorcnt = world.hdr.fps;
                }
                if(main_cursor == E_CLK)
                    main_cursor = main_cursorlast;
            break;
        }
        switch(tab) {
            case TAB_ERROR: if(!main_error_ctrl()) exit_loop(); break;
            case TAB_LAUNCHER: if(!launcher_ctrl()) exit_loop(); break;
            case TAB_LICENSE: if(!license_ctrl()) exit_loop(); break;
            case TAB_CREDITS: credits_ctrl(); break;
            case TAB_SPLASH: break;
            case TAB_LOADING: break;
            case TAB_MAINMENU: mainmenu_ctrl(); break;
            case TAB_CUTSCN: cutscn_ctrl(); break;
            case TAB_STARTGAME:
            case TAB_GAME: if(!game_ctrl()) exit_loop(); break;
        }
    }
    if(main_draw) {
        SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
        SDL_RenderClear(renderer);
        switch(tab) {
            case TAB_ERROR: main_error_view(); break;
            case TAB_LAUNCHER: launcher_view(); break;
            case TAB_DOWNLOAD: client_view(); break;
            case TAB_LICENSE: license_view(); break;
            case TAB_SPLASH: main_splash(); main_switchtab(TAB_LOADING); break;
            case TAB_LOADING: if(main_init2()) { main_splash(); main_switchtab(TAB_FADING); } break;
            case TAB_FADING:
                SDL_SetTextureAlphaMod(logo, 255 * main_fade / main_fademax);
                main_splash();
                SDL_SetTextureAlphaMod(logo, 255);
                if(!main_fade) {
                    SDL_RenderPresent(renderer);
                    main_switchtab(TAB_INTRO);
                } else main_fade--;
            break;
            case TAB_MAINMENU: mainmenu_view(); break;
            case TAB_CUTSCN: cutscn_view(); break;
            case TAB_CREDITS: credits_view(); break;
            case TAB_STARTGAME:
            case TAB_GAME: game_view(); break;
            case TAB_EXIT: exit_loop(); break;
        }
        /* in case main_draw got cleared during one of the _view() calls */
        if(main_draw && ui_input_str) ui_input_view();
        if(main_draw && !main_takescrshot && tab >= TAB_MAINMENU && tab != TAB_CREDITS && main_cursorcnt && main_cursors[E_PTR]) {
            cur = main_cursors[main_cursor] ? main_cursors[main_cursor] : main_cursors[E_PTR];
            if(!touch || cur != main_cursors[E_PTR])
                spr_render(main_curx - cur->w / 2, main_cury - cur->h / 2, cur, 0, 0, cur->w, cur->h, 255);
        }
        if(main_draw) {
            /* needed for local save game */
            if(main_takescrshot) {
                if(world.hdr.magic[0] == '#') {
                    if(!main_scrshot) main_scrshot = (uint16_t*)malloc(256 * 256 * sizeof(uint16_t));
                    if(main_scrshot) {
                        rect.w = 256; rect.h = 256; rect.x = (win_w - 256) / 2; rect.y = (win_h - 256) / 2 - world.hdr.deltay;
                        /* has to be called after rendering, but *before* SDL_RenderPresent */
                        SDL_RenderReadPixels(renderer, &rect, SDL_PIXELFORMAT_BGR555, main_scrshot, 512);
                    }
                }
                main_takescrshot = 0;
            }
            SDL_RenderPresent(renderer);
        }
    }
}

/**
 * Exit player
 */
void main_quit()
{
    if(verbose) printf("tngp: quitting...\r\n");
    main_switchtab(-1);
    game_free();
    script_free();
    font_free();
    spr_free();
    media_free();
    world_free();
    files_free();
    map_unload(game_scene.maps);
    map_unload(cutscn_scene.maps);
    if(main_scrshot) { free(main_scrshot); main_scrshot = NULL; }
    if(logo) { SDL_DestroyTexture(logo); logo = NULL; }
    if(inpbg) { SDL_DestroyTexture(inpbg); inpbg = NULL; }
    if(screen) { SDL_DestroyTexture(screen); screen = NULL; }
    if(renderer) { SDL_DestroyRenderer(renderer); renderer = NULL; }
    if(window) {
        SDL_DestroyWindow(window);
#ifndef __EMSCRIPTEN__
        /* restore original screen resolution */
        if(main_w != win_w || main_h != win_h) {
            window = SDL_CreateWindow("TirNanoG", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, main_w, main_h,
                SDL_WINDOW_FULLSCREEN);
            if(window) SDL_DestroyWindow(window);
        }
#endif
        window = NULL;
    }
    if(controller) { SDL_GameControllerClose(controller); controller = NULL; }
    if(ssfn_src) { free(ssfn_src); ssfn_src = NULL; }
#ifndef __EMSCRIPTEN__
    if(client_url) { free(client_url); client_url = NULL; }
    client_close();
#endif
    if(main_fn) { free(main_fn); main_fn = NULL; }
    Mix_CloseAudio();
    Mix_Quit();
    SDL_CloseAudio();
    SDL_ShowCursor(SDL_ENABLE);
    SDL_Quit();
#ifdef __EMSCRIPTEN__
    /* don't let emscripten fool you, this won't cancel the loop. it will quit... but neither of these work with asyncify! */
    emscripten_cancel_main_loop();
    /*emscripten_force_exit(0);*/
#else
    /* DO NOT call exit(), that crashes android... */
    /* exit(0); */
#endif
}

/**
 * Initialize world
 * Locate game file or parse URL
 */
int main_init(char *fn, char *prog)
{
    char *ptr = NULL;
    FILE *f;

    memset(&world.hdr, 0, sizeof(world.hdr)); world.gpl = 0;
    if(main_fn) { free(main_fn); main_fn = NULL; }
    tng_lickey(NULL, 0);
    if(!fn || !*fn) {
        ptr = (char*)main_alloc((prog ? strlen(prog) : 0) + 16);
        if(prog) {
            strcpy(ptr, prog);
            fn = strrchr(ptr, SEP[0]);
        } else
            fn = NULL;
        if(!fn) { strcpy(ptr, "." SEP); fn = ptr + 2; } else fn++;
        strcpy(fn, "game.tng");
        fn = ptr;
        /* if the player and the game file is boundled together, then according to GPL section 5c the game file must be GPL too */
        world.gpl = 1;
    }
    if(!memcmp(fn, "tng://", 6)) {
        main_fn = (char*)main_alloc(strlen(fn) + 1);
        strcpy(main_fn, fn);
        main_delay = 1000 / 30;
        main_switchtab(TAB_DOWNLOAD);
        if(ptr) free(ptr);
        return 0;
    } else {
        if(!memcmp(fn, "file://", 7)) fn += 7;
        if(*fn && (f = files_open(fn, "rb"))) {
            if(fread(&world.hdr, 1, sizeof(world.hdr), f) && !memcmp(&world.hdr.magic, TNG_MAGIC, 16)) {
                main_fn = (char*)main_alloc(strlen(fn) + 1);
                strcpy(main_fn, fn);
            }
            fclose(f);
        }
    }
    if(ptr) free(ptr);
    if(!main_fn) {
        world.gpl = 0;
        main_delay = 1000 / 30;
        main_switchtab(TAB_LAUNCHER);
        return 0;
    } else
    if(!tng_compat(&world.hdr)) {
        error_reason = ERR_COMPAT;
        main_switchtab(TAB_ERROR);
        return 0;
    } else {
        main_delay = 1000 / (world.hdr.fps < 10 || world.hdr.fps > 120 ? 10 : world.hdr.fps);
        main_switchtab(world.hdr.enc ? TAB_LICENSE : TAB_SPLASH);
    }
    return 1;
}

/**
 * Initialize world, 2nd round
 * Actually load game file(s) and assets
 */
int main_init2()
{
    int freqs[] = { TNG_FREQS }, f;

    if(!world_load(main_fn)) { error_reason = ERR_TNG; main_switchtab(TAB_ERROR); return 0; }
    world_init(detlng);
    f = world.hdr.freq < (int)(sizeof(freqs)/sizeof(freqs[0])) ? freqs[world.hdr.freq] : 44100;
    audio = Mix_OpenAudio(f, AUDIO_S16LSB, 2, 4096) >= 0;
    if(verbose > 1) printf("tngp: opening audio %uHz %s\r\n", f, audio ? "ok" : "failed");
    media_init();
    spr_init();
    font_init();
    map_init();
    return 1;
}

/**
 * Save game
 */
void main_savegame()
{
    int ok = 0;
    char *fn, tmp[17];
    unsigned long int plen = compressBound(512 * 256);
    uint8_t *prvw;

    if(world.hdr.magic[0] == '#' && main_scrshot && files_saves && world.players && world.numplayers > 0 &&
      world.players[0].name && world.players[0].name[0]) {
        prvw = (uint8_t*)malloc(plen);
        if(prvw) {
            compress2(prvw, &plen, (const Bytef*)main_scrshot, 512 * 256, 9);
            fn = (char*)malloc(strlen(files_saves) + strlen(world.players[0].name) + 8);
            if(fn) {
                world.players[0].lastd = time(NULL);
                world.players[0].lang[0] = detlng[0]; world.players[0].lang[1] = detlng[1];
                memcpy(tmp, world.hdr.id, 16); tmp[16] = 0;
                sprintf(fn, "%s" SEP "%s" SEP "%s.sav", files_saves, tmp, world.players[0].name);
                ok = world_savestate(fn, prvw, plen, main_timetotal + (main_tick - main_start) / 1000);
                if(ok && verbose) printf("tngp: game saved as %s\r\n", fn);
                free(fn);
            }
            free(prvw);
        }
    }
    if(!ok && verbose) printf("tngp: unable to save game!\r\n");
}

/**
 * Read in configuration
 */
void main_loadconfig()
{
#ifndef __EMSCRIPTEN__
    int n, l;
    char *fn, *data, *val;

    if(files_config) {
        fn = (char*)malloc(strlen(files_config) + 24);
        if(fn) {
            sprintf(fn, "%s" SEP "config.json", files_config);
            data = files_readfile(fn, &l);
            if(data) {
                n = l = 0;
                /* only load the language from the config if not specified on the command line */
                if(!forcelng) {
                    val = json_get(data, "lang");
                    if(val) { if(val[0] && val[1]) { detlng[0] = val[0]; detlng[1] = val[1]; } free(val); }
                }
                val = json_get(data, "res_w"); if(val) { n = atoi(val); free(val); }
                val = json_get(data, "res_h"); if(val) { l = atoi(val); free(val); }
                if(n > 0 && n < 16384 && l > 0 && l < 16384) { win_w = n; win_h = l; }
                val = json_get(data, "musvol"); if(val) { main_musvol = atoi(val); free(val); }
                val = json_get(data, "sfxvol"); if(val) { main_sfxvol = atoi(val); free(val); }
                val = json_get(data, "spcvol"); if(val) { main_spcvol = atoi(val); free(val); }
                val = json_get(data, "gptres"); if(val) { main_gptres = atoi(val); free(val); }
                if(main_musvol < 0) main_musvol = 0;
                if(main_musvol > MIX_MAX_VOLUME) main_musvol = MIX_MAX_VOLUME;
                if(main_sfxvol < 0) main_sfxvol = 0;
                if(main_sfxvol > MIX_MAX_VOLUME) main_sfxvol = MIX_MAX_VOLUME;
                if(main_spcvol < 0) main_spcvol = 0;
                if(main_spcvol > MIX_MAX_VOLUME) main_spcvol = MIX_MAX_VOLUME;
                if(main_gptres < 1) main_gptres = 1;
                if(main_gptres > 32767) main_gptres = 32767;
                free(data);
                if(verbose) printf("tngp: configuration loaded\r\n");
            }
            free(fn);
        }
    }
#endif
}

/**
 * Write out configuration
 */
void main_saveconfig()
{
#ifndef __EMSCRIPTEN__
    int w = win_w, h = win_h;
    char *fn;
    FILE *f;

    if(files_config) {
        fn = (char*)malloc(strlen(files_config) + 24);
        if(fn) {
            sprintf(fn, "%s" SEP "config.json", files_config);
            f = files_open(fn, "wb");
            if(f) {
                if(win_w == main_w && win_h == main_h) w = h = 0;
                fprintf(f,
                    "{\r\n"
                    "  \"lang\": \"%s\",\r\n"
                    "  \"res_w\": %u,\r\n"
                    "  \"res_h\": %u,\r\n"
                    "  \"musvol\": %u,\r\n"
                    "  \"sfxvol\": %u,\r\n"
                    "  \"spcvol\": %u\r\n"
                    "  \"gptres\": %u\r\n"
                    "}\r\n",
                    world.lang[world.langidx].id, w, h, main_musvol, main_sfxvol, main_spcvol, main_gptres);
                if(verbose) printf("tngp: configuration saved\r\n");
                fclose(f);
            }
            free(fn);
        }
    }
#endif
}

/**
 * Create window
 */
void main_win(int w, int h)
{
    int j, c;
    stbi__context s;
    stbi__result_info ri;
    uint8_t *ptr;
    void *data;
    SDL_Surface *srf;
    SDL_Rect rect;

    if(logo) { SDL_DestroyTexture(logo); logo = NULL; }
    if(screen) { SDL_DestroyTexture(screen); screen = NULL; }
    if(renderer) { SDL_DestroyRenderer(renderer); renderer = NULL; }
    if(window) { SDL_DestroyWindow(window); window = NULL; }

    win_w = w; win_h = h;
    window = SDL_CreateWindow("TirNanoG", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, win_w, win_h,
/* DO NOT try fullscreen with emscripten, that will crash the player at a later time... something about removed canvas parent */
#if defined(DEBUG) || defined(__EMSCRIPTEN__)
        0
#else
        SDL_WINDOW_FULLSCREEN
#endif
        );
    if(!window) return;
    renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
    if(!renderer) {
        renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_SOFTWARE);
        if(!renderer) return;
    }
    rect.x = 0; rect.y = 0; rect.w = w; rect.h = h;
    SDL_RenderSetClipRect(renderer, &rect);

    /* get icons */
    memset(&s, 0, sizeof(s));
    memset(&ri, 0, sizeof(ri));
    s.img_buffer = s.img_buffer_original = binary_logo_png;
    s.img_buffer_end = s.img_buffer_original_end = binary_logo_png + sizeof(binary_logo_png);
    ri.bits_per_channel = 8;
    ptr = (uint8_t*)stbi__png_load(&s, (int*)&w, (int*)&h, (int*)&c, 4, &ri);
    if(ptr) {
        logo = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ABGR8888, SDL_TEXTUREACCESS_STREAMING, w, h);
        if(logo) {
            SDL_SetTextureBlendMode(logo, SDL_BLENDMODE_BLEND);
            SDL_LockTexture(logo, NULL, &data, &j);
            memcpy(data, ptr, w * h * 4);
            SDL_UnlockTexture(logo);
        }
        srf = SDL_CreateRGBSurfaceFrom((Uint32 *)(ptr + (iconpos.y * w + iconpos.x) * 4), iconpos.w, iconpos.h, 32, w * 4,
            0xFF, 0xFF00, 0xFF0000, 0xFF000000);
        if(srf) {
            SDL_SetWindowIcon(window, srf);
            SDL_FreeSurface(srf);
        }
        free(ptr);
    }
}

/**
 * Get elapsed game time in 1/1000th secs
 */
void main_gettick()
{
    main_tick = SDL_GetTicks();
}

/**
 * Print program version and copyright
 */
void main_hdr()
{
    printf("\r\nTirNanoG Player v%s (build %u) by bzt Copyright (C) 2021 - 2022 GPLv3+\r\n\r\n", tngpver, BUILD);
}

/**
 * The real main procedure
 */
int main(int argc, char **argv)
{
    int i, j, w, h;
    char *infile = NULL, *ptr2 = NULL;
    uint8_t *ptr;
    int32_t tickdiff;
    SDL_RWops *ops = NULL;
#ifdef __EMSCRIPTEN__
    char *lng = detlng;

    i = EM_ASM_INT({ return ln.charCodeAt(1) * 256 + ln.charCodeAt(0); });
    detlng[0] = i & 0xff; detlng[1] = (i >> 8) & 0xff; detlng[2] = 0;
#else
    SDL_DisplayMode dm;
    char *lng = getenv("LANG");
#ifdef __WIN32__
    int lid;
    FILE *f;
    if(!lng) {
        lid = GetUserDefaultLangID(); /* GetUserDefaultUILanguage(); */
        /* see https://docs.microsoft.com/en-us/windows/win32/intl/language-identifier-constants-and-strings */
        switch(lid & 0xFF) {
            case 0x01: lng = "ar"; break;   case 0x02: lng = "bg"; break;   case 0x03: lng = "ca"; break;
            case 0x04: lng = "zh"; break;   case 0x05: lng = "cs"; break;   case 0x06: lng = "da"; break;
            case 0x07: lng = "de"; break;   case 0x08: lng = "el"; break;   case 0x0A: lng = "es"; break;
            case 0x0B: lng = "fi"; break;   case 0x0C: lng = "fr"; break;   case 0x0D: lng = "he"; break;
            case 0x0E: lng = "hu"; break;   case 0x0F: lng = "is"; break;   case 0x10: lng = "it"; break;
            case 0x11: lng = "jp"; break;   case 0x12: lng = "ko"; break;   case 0x13: lng = "nl"; break;
            case 0x14: lng = "no"; break;   case 0x15: lng = "pl"; break;   case 0x16: lng = "pt"; break;
            case 0x17: lng = "rm"; break;   case 0x18: lng = "ro"; break;   case 0x19: lng = "ru"; break;
            case 0x1A: lng = "hr"; break;   case 0x1B: lng = "sk"; break;   case 0x1C: lng = "sq"; break;
            case 0x1D: lng = "sv"; break;   case 0x1E: lng = "th"; break;   case 0x1F: lng = "tr"; break;
            case 0x20: lng = "ur"; break;   case 0x21: lng = "id"; break;   case 0x22: lng = "uk"; break;
            case 0x23: lng = "be"; break;   case 0x24: lng = "sl"; break;   case 0x25: lng = "et"; break;
            case 0x26: lng = "lv"; break;   case 0x27: lng = "lt"; break;   case 0x29: lng = "fa"; break;
            case 0x2A: lng = "vi"; break;   case 0x2B: lng = "hy"; break;   case 0x2D: lng = "bq"; break;
            case 0x2F: lng = "mk"; break;   case 0x36: lng = "af"; break;   case 0x37: lng = "ka"; break;
            case 0x38: lng = "fo"; break;   case 0x39: lng = "hi"; break;   case 0x3A: lng = "mt"; break;
            case 0x3C: lng = "gd"; break;   case 0x3E: lng = "ms"; break;   case 0x3F: lng = "kk"; break;
            case 0x40: lng = "ky"; break;   case 0x45: lng = "bn"; break;   case 0x47: lng = "gu"; break;
            case 0x4D: lng = "as"; break;   case 0x4E: lng = "mr"; break;   case 0x4F: lng = "sa"; break;
            case 0x53: lng = "kh"; break;   case 0x54: lng = "lo"; break;   case 0x56: lng = "gl"; break;
            case 0x5E: lng = "am"; break;   case 0x62: lng = "fy"; break;   case 0x68: lng = "ha"; break;
            case 0x6D: lng = "ba"; break;   case 0x6E: lng = "lb"; break;   case 0x6F: lng = "kl"; break;
            case 0x7E: lng = "br"; break;   case 0x92: lng = "ku"; break;   case 0x09: default: lng = "en"; break;
        }
    }
    /* restore stdout to console */
    AttachConsole(ATTACH_PARENT_PROCESS);
    f = _fdopen(_open_osfhandle((intptr_t)GetStdHandle(STD_OUTPUT_HANDLE), 0x4000/*_O_TEXT*/), "w");
    if(f) *stdout = *f;
    printf("\r\n");
#endif
    /* parse command line */
    for(i = 1; i < argc; i++) {
        if(!memcmp(argv[i], "--help", 6) || !strcmp(argv[i], CLIFLAG "h") || !strcmp(argv[i], CLIFLAG "?")) goto usage;
        if(argv[i][0] == CLIFLAG[0]) {
            for(j = 1; argv[i][j]; j++)
                switch(argv[i][j]) {
                    case 'L': if(j == 1) { lng = argv[++i]; forcelng = 1; } else goto usage; break;
                    case 't': touch++; break;
                    case 'k': ui_input_gp = 3; break;
                    case 'v': verbose++; break;
                    case 'm': media_verbose++; break;
#if DEBUG
                    case 'T': script_trace++; break;
#endif
                    default:
usage:                  main_hdr();
                        printf("  tngp [" CLIFLAG "L <xx>] [" CLIFLAG "t] [" CLIFLAG "k] [" CLIFLAG "v|" CLIFLAG "vv] "
#if DEBUG
                            "[" CLIFLAG "T|" CLIFLAG "TT|" CLIFLAG "TTT] "
#endif
                            "[tng]\r\n\r\n");
                        printf("  " CLIFLAG "L <xx>       force a language (");
                        for(i = 0; i < NUMLANGS; i++) printf("%s%s%s", i ? ", " : "",
                            i == 11 || (i > 10 && !((i - 11) % 15)) ? "\r\n                  " : "", dict[i][0]);
                        printf(")\r\n");
                        printf("  " CLIFLAG "t            touchscreen controls (default on handhelds)\r\n");
                        printf("  " CLIFLAG "k            on-screen keyboard (default if game controller detected)\r\n");
                        printf("  " CLIFLAG "v|" CLIFLAG "vv        set verbosity level\r\n");
#if DEBUG
                        printf("  " CLIFLAG "T|" CLIFLAG "TT|" CLIFLAG "TTT   enable script trace\r\n");
#endif
                        printf("  tng           game.tng file or \"tng://(ip):(port)\" url (defaults to launcher)\r\n\r\n");
                        exit(0);
                    break;
                }
        } else
            infile = argv[i];
    }

    /* get dictionary */
    if(!lng) lng = "en";
    detlng[0] = lng[0]; detlng[1] = lng[1]; detlng[2] = 0;
#endif
    for(i = 0; i < NUMLANGS; i++)
        if(!strncmp(lng, dict[i][0], strlen(dict[i][0]))) break;
    if(i >= NUMLANGS)
        for(i = 0; i < NUMLANGS; i++)
            if(!strncmp(lng, dict[i][0], 2)) break;
    if(i >= NUMLANGS) i = 0;
    lang = &dict[i][1];
    ui_input_init();

    memset(&world, 0, sizeof(world));

    main_hdr();
    for(i = 0; i < 3; i++) printf("  %s\r\n", copyright[i]);
    printf("\r\n");
    fflush(stdout);
#if DEBUG
    if(script_trace > 0) printf("tngp: script trace enabled, level %u\r\n", script_trace);
#endif
    /* initialize screen and other SDL stuff */
    if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO|SDL_INIT_TIMER|SDL_INIT_EVENTS|SDL_INIT_GAMECONTROLLER)) main_error(ERR_DISPLAY);
#ifdef __EMSCRIPTEN__
    EM_ASM({ document.getElementById("load").style.display="none";document.getElementById("canvas").style.display="block"; });
    main_w = EM_ASM_INT({ return window.innerWidth; /*Module.canvas.width;*/ });
    main_h = EM_ASM_INT({ return window.innerHeight; /*Module.canvas.height;*/ });
#else
    SDL_GetDesktopDisplayMode(0, &dm);
    main_w = dm.w; main_h = dm.h;
#endif
#if defined(__ANDROID__) || defined(__IOS__)
    touch = 1;
#endif

    win_w = main_w; win_h = main_h;
    files_init();
    main_loadconfig();
    forcelng = 0;
    /* we could have loaded a different language than we have from the config */
    if(detlng[0] != lang[-1][0] || detlng[1] != lang[-1][1]) {
        for(i = 0; i < NUMLANGS && strncmp(detlng, dict[i][0], 2); i++);
        if(i < NUMLANGS) lang = &dict[i][1];
    }
    main_win(win_w, win_h);
    if((!window || !renderer) && (win_w != main_w || win_h != main_h)) {
        win_w = main_w; win_h = main_h;
        main_win(win_w, win_h);
    }
    if(!window || !renderer) {
        SDL_Quit();
        main_error(ERR_DISPLAY);
        return 1;
    }
    SDL_ShowCursor(SDL_DISABLE);

    /* uncompress ui font */
    ptr = binary_default_sfn + 3;
    i = *ptr++; ptr += 6; if(i & 4) { w = *ptr++; w += (*ptr++ << 8); ptr += w; } if(i & 8) { while(*ptr++ != 0); }
    if(i & 16) { while(*ptr++ != 0); } j = sizeof(binary_default_sfn) - (size_t)(ptr - binary_default_sfn);
    w = 0; ssfn_src = (ssfn_font_t*)stbi_zlib_decode_malloc_guesssize_headerflag((const char*)ptr, j, 4096, (int*)&w, 0);
    if(!ssfn_src) main_error(ERR_MEM);
    memset(&ws, 0, sizeof(ws));
    ptr = (uint8_t*)ssfn_src + ssfn_src->characters_offs;
    for(ptr = (uint8_t*)ssfn_src + ssfn_src->characters_offs, w = 0; w < 0x110000; w++) {
        if(ptr[0] == 0xFF) { w += 65535; ptr++; }
        else if((ptr[0] & 0xC0) == 0xC0) { h = (((ptr[0] & 0x3F) << 8) | ptr[1]); w += h; ptr += 2; }
        else if((ptr[0] & 0xC0) == 0x80) { h = (ptr[0] & 0x3F); w += h; ptr++; }
        else {
            ws[w] = ptr[2];
            ptr += 6 + ptr[1] * (ptr[0] & 0x40 ? 6 : 5);
        }
    }
#ifndef __EMSCRIPTEN__
    /* we can't use directories or local files with wasm, so do not call main_init or files_readfile */
    main_init(infile, argv[0]);
    /* load user provided gamecontrollerdb.txt (if any) */
    j = 0;
    if(files_config) {
        infile = (char*)malloc(strlen(files_config) + 24);
        if(infile) {
            sprintf(infile, "%s" SEP "gamecontrollerdb.txt", files_config);
            ptr2 = files_readfile(infile, &w);
            if(ptr2) {
                if(SDL_GameControllerAddMappingsFromRW(SDL_RWFromMem(ptr2, w), 0) != -1) {
                    if(verbose) printf("tngp: user provided game controller db loaded from %s\r\n", infile);
                    j = 1;
                }
                free(ptr2);
            }
            free(infile);
        }
    }
    if(!j)
#endif
    {
        /* uncompress built-in gamecontrollerdb */
        ptr = binary_gamecontrollerdb + 3;
        i = *ptr++; ptr += 6; if(i & 4) { w = *ptr++; w += (*ptr++ << 8); ptr += w; } if(i & 8) { while(*ptr++ != 0); }
        if(i & 16) { while(*ptr++ != 0); } j = sizeof(binary_gamecontrollerdb) - (size_t)(ptr - binary_gamecontrollerdb);
        w = 0; ptr2 = (char*)stbi_zlib_decode_malloc_guesssize_headerflag((const char*)ptr, j, 4096, (int*)&w, 0);
        if(ptr2) {
            ops = SDL_RWFromConstMem(ptr2, w);
            SDL_GameControllerAddMappingsFromRW(ops, 0);
            SDL_FreeRW(ops);
            free(ptr2);
        }
    }
    SDL_GameControllerEventState(SDL_ENABLE);
    Mix_Init(MIX_INIT_OGG);
    srand(time(NULL));

    /* execute the main game loop */
#ifdef __EMSCRIPTEN__
    tab = -1;
    /* we should pass the fps value from the TNG file, but it is not possible to read the file outside of the main loop... */
    emscripten_set_main_loop(main_loop, 30, 1);
    /* this never reached! cancel_main_loop does not cancel the loop, it actually quits the app! */
#else
    SDL_AddEventWatch(main_stupidios, NULL);
    while(1) {
        /* emscripten does not allow return value... so we have to use a global */
        main_loop();
        if(!main_ret) break;
        tickdiff = main_delay - (SDL_GetTicks() - main_tick);
        if(tickdiff > 0 && tickdiff < 1000) SDL_Delay(tickdiff);
    }
    main_quit();
    if(verbose) printf("tngp: done.\r\n");
#endif
    return 0;
}
