/*
 * tngp/world.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 Game world functions
 *
 */

#include "tngp.h"

world_t world;
int strnatcmp(const void *a, const void *b);
const char *crdcat = "gcdafmsvtp";
const char *crds[] = { "dNicolae Berbece", "fDmitry Bolkhovityanov", "fRoman Czyborra", "fPaul Hardy",
    "pSam Lantinga", "pXiph.org Foundation", "pSean Barrett", "pJean-loup Gailly", "pMark Adler", "pbzt" };

/**
 * Seek in one of the tng files
 */
void world_seektng(int idx, int strm, uint64_t offs)
{
    if(idx < 0 || idx >= world.numtng || strm < 0 || strm > 2) return;
    world.tng[idx].offs[strm] = offs;
    files_seek(world.tng[idx].f[strm], offs);
}

/**
 * Read data from one of the tng files
 */
int world_readtng(int idx, int strm, void *buff, int len)
{
    unsigned char o, *buf = buff;
    int i;

    if(idx < 0 || idx >= world.numtng || !buf || len < 1 || strm < 0 || strm > 2) return 0;
    if((len = files_read(world.tng[idx].f[strm], buf, len)) < 1) return 0;
    if(world.tng[idx].encrypted)
        for(i = 0, o = (world.tng[idx].offs[strm] + tng_mask[64]) & 0xFF; i < len; i++, o++)
            buf[i] ^= tng_mask[(world.tng[idx].offs[strm] + i) & 63] ^ o;
    world.tng[idx].offs[strm] += len;
    return len;
}

/**
 * Return true on asset eof
 */
int world_eofasset(int strm, world_asset_t *asset)
{
    if(!asset || asset->tng < 0 || asset->tng >= world.numtng) return 1;
    return world.tng[asset->tng].offs[strm] >= asset->offs + asset->size;
}

/**
 * Add an asset to the world
 */
int world_addasset(world_asset_t **asset, int *num, int tng, uint64_t offs, uint32_t size, char *name)
{
    int i = 0;

    if(!asset || !num || !name) return 0;
    if(*asset)
        for(i = 0; i < *num && strcmp((*asset)[i].name, name); i++);
    if(i >= *num) {
        i = (*num)++;
        *asset = (world_asset_t*)main_realloc(*asset, *num * sizeof(world_asset_t));
    }
    (*asset)[i].tng = tng;
    (*asset)[i].offs = offs;
    (*asset)[i].size = size;
    (*asset)[i].name = name;
    return i;
}

/**
 * Add an asset to the world
 */
int world_add2asset(world_asset_t **asset, int *num, int tng, int size, int len, tng_asset_desc_t *desc)
{
    int i = 0;

    if(!asset || !num || len < 1 || !desc) return 0;
    *asset = (world_asset_t*)main_realloc(*asset, ((*num) + len) * sizeof(world_asset_t));
    for(i = 0; i < len; i++, desc++) {
        (*asset)[i].tng = tng;
        (*asset)[i].offs = 68 + size + SDL_SwapLE64(desc->offs);
        (*asset)[i].size = SDL_SwapLE32(desc->size);
        (*asset)[i].name = NULL;
    }
    *num += len;
    return 1;
}

/**
 * Load asset from tng
 */
void *world_loadasset(int tng, uint64_t offs, uint32_t size, int *len)
{
    int l;
    char *ptr;
    void *ret = NULL;

    if(len) *len = 0;
    if(size < 1) return NULL;
    ptr = (char*)main_alloc(size);
    world_seektng(tng, 0, offs);
    if(!world_readtng(tng, 0, ptr, size)) {
        if(verbose > 1) printf("%s: error reading asset\r\n", main_me);
        free(ptr);
        return NULL;
    }
    l = 0;
    ret = (void*)stbi_zlib_decode_malloc_guesssize_headerflag((const char*)ptr, size, 4096, &l, 1);
    free(ptr);
    if(!ret || l < 1) {
        if(verbose > 1) printf("%s: uncompression error\r\n", main_me);
        if(ret) free(ret);
        return NULL;
    }
    if(len) *len = l;
    return ret;
}

/**
 * Load sprite definitions
 */
int world_addsprs(world_sprite_t **spr, int *num, int tng, int size, int len, tng_asset_desc_t *desc)
{
    tng_sprmap_desc_t *map;
    uint8_t *ptr, *buf;
    int i = 0, j, k, l, siz, type, dir, nf, w, h;

    if(!spr || !num || len < 1 || !desc) return 0;
    *spr = (world_sprite_t*)main_realloc(*spr, ((*num) + len) * 8 * sizeof(world_sprite_t));
    memset(*spr + (*num) * 8, 0, len * 8 * sizeof(world_sprite_t));
    for(i = 0, k = (*num) * 8; i < len; i++, desc++, k += 8) {
        l = (int)(desc->name >> 24);
        ptr = buf = (uint8_t*)world_loadasset(tng, 68 + size + SDL_SwapLE64(desc->offs), SDL_SwapLE32(desc->size), &siz);
        if(ptr && siz) {
            for(j = 0; j < l && ptr < buf + siz; j++) {
                w = (ptr[1] << 8) | ptr[0]; ptr += 2;
                h = (ptr[1] << 8) | ptr[0]; ptr += 2;
                type = ptr[0] >> 6; dir = ptr[0] & 7; ptr++;
                nf = ptr[0]; ptr++;
                if(nf < 1) continue;
                (*spr)[k + dir].w = w;
                (*spr)[k + dir].h = h;
                (*spr)[k + dir].t = type;
                (*spr)[k + dir].f = nf;
                (*spr)[k + dir].data = (world_sprmap_t*)main_alloc(nf * sizeof(world_sprmap_t));
                for(w = 0; w < nf; w++, ptr += sizeof(tng_sprmap_desc_t)) {
                    map = (tng_sprmap_desc_t*)ptr;
                    (*spr)[k + dir].data[w].atlasid = SDL_SwapLE16((map->atlasid & 0xfff)) + world.tng[tng].ofs[5];
                    (*spr)[k + dir].data[w].op = (map->atlasid >> 12) & 0xf;
                    (*spr)[k + dir].data[w].x = SDL_SwapLE16(map->x);
                    (*spr)[k + dir].data[w].y = SDL_SwapLE16(map->y);
                    (*spr)[k + dir].data[w].w = SDL_SwapLE16(map->w);
                    (*spr)[k + dir].data[w].h = SDL_SwapLE16(map->h);
                    (*spr)[k + dir].data[w].l = SDL_SwapLE16(map->l);
                    (*spr)[k + dir].data[w].t = SDL_SwapLE16(map->t);
                }
            }
            free(buf);
        }
    }
    *num += len;
    return 1;
}

/**
 * Read one sprite index from stream
 */
uint8_t *world_getspridx(uint8_t *ptr, world_spridx_t *spr, int cat, int idx)
{
    spr->cat = cat;
    spr->idx = (ptr[1] << 8) | ptr[0]; ptr += 2;
    if(spr->cat > SPR_BG) spr->cat = SPR_BG;
    if(spr->idx != 0xffff) {
        spr->idx += world.tng[idx].ofs[spr->cat];
        if(spr->idx >= world.numspr[spr->cat]) spr->idx = 0xffff;
    }
    return ptr;
}

/**
 * Read one sprite spec from stream
 */
uint8_t *world_getspr(uint8_t *ptr, world_spridx_t *spr, int idx)
{
    return world_getspridx(ptr + 1, spr, ptr[0], idx);
}

/**
 * Read one asset index from stream
 */
uint8_t *world_getidx(uint8_t *ptr, int *obj, int idx, world_asset_t **asset, int num)
{
    int i;
    uint32_t c = 0;

    c = (ptr[2] << 16) | (ptr[1] << 8) | ptr[0]; ptr += 3;
    *obj = -1;
    if(c != 0xffffff && num > 0 && asset) {
        for(i = 0; i < num && (*asset)[i].name != world.tng[idx].sts + c; i++);
        if(i < num) *obj = i;
    }
    return ptr;
}

/**
 * Read one cutscene index from stream
 */
uint8_t *world_getcut(uint8_t *ptr, int *obj, int idx)
{
    int i;
    uint32_t c = 0;

    c = (ptr[2] << 16) | (ptr[1] << 8) | ptr[0]; ptr += 3;
    *obj = -1;
    if(c != 0xffffff && world.numcut > 0 && world.cutscns) {
        for(i = 0; i < world.numcut && world.cutscns[i].name != world.tng[idx].sts + c; i++);
        if(i < world.numcut) *obj = i;
    }
    return ptr;
}

/**
 * Read one font spec from stream
 */
uint8_t *world_getfnt(uint8_t *ptr, world_font_t *fnt, int idx)
{
    fnt->style = *ptr++;
    fnt->size = *ptr++;
    if(fnt->size < 8) fnt->size = 8;
    if(fnt->size > 192) fnt->size = 192;
    return world_getidx(ptr, &fnt->fnt, idx, &world.font, world.numfont);
}

/**
 * Read one attribute spec from stream
 */
uint8_t *world_getattr(uint8_t *ptr, int *attr, int idx)
{
    int i, j = (ptr[2] << 16) | (ptr[1] << 8) | ptr[0];
    ptr += 3;
    for(i = 0; i < world.numattrs && world.attrs[i].name != world.tng[idx].sts + j; i++);
    *attr = i < world.numattrs ? i : -1;
    return ptr;
}

/**
 * Read one text spec from stream
 */
uint8_t *world_gettxt(uint8_t *ptr, int *txt)
{
    int i = (ptr[2] << 16) | (ptr[1] << 8) | ptr[0];
    *txt = (i == 0xffffff ? -1 : i);
    return ptr + 3;
}

/**
 * Free character option groups
 */
void world_freechargrp(int idx)
{
    int i;
    world_charopt_t *opts;

    if(idx < 0 || idx >= world.numchar || !world.chars || !world.chars[idx].opts) return;
    opts = world.chars[idx].opts;
    for(i = 0; i < world.chars[idx].numopts; i++, opts++) {
        if(opts->atr) free(opts->atr);
        if(opts->inv) free(opts->inv);
        if(opts->spr) free(opts->spr);
    }
    free(world.chars[idx].opts);
    world.chars[idx].opts = NULL;
    world.chars[idx].numopts = 0;
}

/**
 * Add character option groups
 */
int world_addchargrp(int tng, uint64_t offs, uint32_t size, char *name)
{
    world_charopt_t *opts;
    uint8_t *ptr, *buf;
    int32_t d;
    int i, j, k, len;

    if(!name) return 0;
    for(i = 0; i < world.numchar && strcmp(world.chars[i].name, name); i++);
    if(i >= world.numchar) {
        i = world.numchar++;
        world.chars = (world_chargrp_t*)main_realloc(world.chars, world.numchar * sizeof(world_chargrp_t));
        memset(&world.chars[i], 0, sizeof(world_chargrp_t));
        world.chars[i].name = name;
    } else
        world_freechargrp(i);
    ptr = buf = (uint8_t*)world_loadasset(tng, offs, size, &len);
    if(buf) {
        if(len > 7) {
            ptr = world_gettxt(ptr, &world.chars[i].trname);
            ptr = world_getspr(ptr, &world.chars[i].pal, tng);
            world.chars[i].numopts = *ptr++;
            opts = world.chars[i].opts = (world_charopt_t*)main_alloc(world.chars[i].numopts * sizeof(world_charopt_t));
            for(j = 0; j < world.chars[i].numopts && ptr < buf + len; j++, opts++) {
                ptr = world_gettxt(ptr, &opts->name);
                ptr = world_gettxt(ptr, &opts->desc);
                opts->na = *ptr++; opts->ni = *ptr++; opts->ns = *ptr++;
                if(opts->na) {
                    opts->atr = (world_charattr_t*)main_alloc(opts->na * sizeof(world_charattr_t));
                    for(k = 0; k < opts->na && ptr < buf + len; k++) {
                        opts->atr[k].type = *ptr++;
                        ptr = world_getattr(ptr, &opts->atr[k].attr, tng);
                        memcpy(&d, ptr, 4); ptr += 4;
                        opts->atr[k].value = SDL_SwapLE32(d);
                    }
                }
                if(opts->ni) {
                    opts->inv = (world_inv_t*)main_alloc(opts->ni * sizeof(world_inv_t));
                    for(k = 0; k < opts->ni && ptr < buf + len; k++) {
                        opts->inv[k].qty = *ptr++;
                        ptr = world_getspr(ptr, &opts->inv[k].obj, tng);
                    }
                }
                if(opts->ns) {
                    opts->spr = (world_spridx_t*)main_alloc(opts->ns * sizeof(world_spridx_t));
                    for(k = 0; k < opts->ns && ptr < buf + len; k++)
                        ptr = world_getspr(ptr, &opts->spr[k], tng);
                }
            }
        }
        free(buf);
    }
    return 1;
}

/**
 * Add route chooser
 */
int world_addchooser(int tng, uint64_t offs, uint32_t size, char *name)
{
    uint8_t *ptr, *buf;
    int i, j, k, d, len;

    if(!name) return 0;
    for(i = 0; i < world.numchr && strcmp(world.choosers[i].name, name); i++);
    if(i >= world.numchr) {
        i = world.numchr++;
        world.choosers = (world_chooser_t*)main_realloc(world.choosers, world.numchr * sizeof(world_chooser_t));
        memset(&world.choosers[i], 0, sizeof(world_chooser_t));
        world.choosers[i].name = name;
    } else {
        if(world.choosers[i].opts) free(world.choosers[i].opts);
    }
    ptr = buf = (uint8_t*)world_loadasset(tng, offs, size, &len);
    if(buf) {
        if(len > 10) {
            world.choosers[i].l = (ptr[0] | (ptr[1] << 8)); ptr += 2;
            world.choosers[i].t = (ptr[0] | (ptr[1] << 8)); ptr += 2;
            world.choosers[i].h = (ptr[0] | (ptr[1] << 8)); ptr += 2;
            world.choosers[i].v = (ptr[0] | (ptr[1] << 8)); ptr += 2;
            world.choosers[i].numopts = j = (ptr[0] | (ptr[1] << 8)); ptr += 2;
            if(j) {
                world.choosers[i].opts = (world_chropt_t*)main_alloc(j * sizeof(world_chropt_t));
                for(k = 0; k < j; k++) {
                    world.choosers[i].opts[k].type = *ptr & 0x7f;
                    world.choosers[i].opts[k].keep = (*ptr >> 7); ptr++;
                    if(world.choosers[i].opts[k].type == 13) {
                        world.choosers[i].opts[k].data.coord.x1 = ptr[0];
                        world.choosers[i].opts[k].data.coord.y1 = ptr[1];
                        world.choosers[i].opts[k].data.coord.x2 = ptr[2];
                        world.choosers[i].opts[k].data.coord.y2 = ptr[3];
                        ptr += 6;
                    } else {
                        ptr = world_getspridx(ptr, &world.choosers[i].opts[k].data.spr.n, SPR_UI, tng);
                        ptr = world_getspridx(ptr, &world.choosers[i].opts[k].data.spr.s, SPR_UI, tng);
                        ptr = world_getspridx(ptr, &world.choosers[i].opts[k].data.spr.p, SPR_UI, tng);
                    }
                    world.choosers[i].opts[k].req1.type = *ptr++;
                    ptr = world_getattr(ptr, &world.choosers[i].opts[k].req1.attr, tng);
                    memcpy(&d, ptr, 4); ptr += 4;
                    world.choosers[i].opts[k].req1.value = SDL_SwapLE32(d);
                    world.choosers[i].opts[k].req2.type = *ptr++;
                    ptr = world_getattr(ptr, &world.choosers[i].opts[k].req2.attr, tng);
                    memcpy(&d, ptr, 4); ptr += 4;
                    world.choosers[i].opts[k].req2.value = SDL_SwapLE32(d);
                    world.choosers[i].opts[k].prov.type = *ptr++;
                    ptr = world_getattr(ptr, &world.choosers[i].opts[k].prov.attr, tng);
                    memcpy(&d, ptr, 4); ptr += 4;
                    world.choosers[i].opts[k].prov.value = SDL_SwapLE32(d);
                }
            }
        }
        free(buf);
    }
    return 1;
}

/**
 * Add cutscene
 */
int world_addcutscn(int tng, uint64_t offs, uint32_t size, char *name)
{
    uint8_t *ptr, *buf;
    int i, j, k, len;

    if(!name) return 0;
    for(i = 0; i < world.numcut && strcmp(world.cutscns[i].name, name); i++);
    if(i >= world.numcut) {
        i = world.numcut++;
        world.cutscns = (world_cutscn_t*)main_realloc(world.cutscns, world.numcut * sizeof(world_cutscn_t));
        memset(&world.cutscns[i], 0, sizeof(world_cutscn_t));
        world.cutscns[i].name = name;
        world.cutscns[i].pomov = world.cutscns[i].plmov = world.cutscns[i].pomus = world.cutscns[i].plmus = -1;
    } else {
        if(world.cutscns[i].img) free(world.cutscns[i].img);
        if(world.cutscns[i].spc) free(world.cutscns[i].spc);
        if(world.cutscns[i].sub) free(world.cutscns[i].sub);
        if(world.cutscns[i].bc) free(world.cutscns[i].bc);
    }
    ptr = buf = (uint8_t*)world_loadasset(tng, offs, size, &len);
    if(buf) {
        if(len > 20) {
            ptr = world_getidx(ptr, &world.cutscns[i].pomov, tng, &world.movie, world.nummovie);
            ptr = world_getidx(ptr, &world.cutscns[i].pomus, tng, &world.music, world.nummusic);
            ptr = world_getidx(ptr, &world.cutscns[i].plmov, tng, &world.movie, world.nummovie);
            ptr = world_getidx(ptr, &world.cutscns[i].plmus, tng, &world.music, world.nummusic);
            world.cutscns[i].numimg = j = (ptr[0] | (ptr[1] << 8)); ptr += 2;
            world.cutscns[i].numspc = (ptr[0] | (ptr[1] << 8)); ptr += 2;
            world.cutscns[i].numsub = (ptr[0] | (ptr[1] << 8)); ptr += 2;
            world.cutscns[i].bclen = (ptr[2] << 16) | (ptr[1] << 8) | ptr[0]; ptr += 3;
            if(j) {
                world.cutscns[i].img = (world_slide_t*)main_alloc(j * sizeof(world_slide_t));
                for(k = 0; k < j; k++) {
                    world.cutscns[i].img[k].s = (ptr[2] << 16) | (ptr[1] << 8) | ptr[0]; ptr += 3;
                    world.cutscns[i].img[k].fi = (ptr[2] << 16) | (ptr[1] << 8) | ptr[0]; ptr += 3;
                    world.cutscns[i].img[k].fo = (ptr[2] << 16) | (ptr[1] << 8) | ptr[0]; ptr += 3;
                    world.cutscns[i].img[k].e = (ptr[2] << 16) | (ptr[1] << 8) | ptr[0]; ptr += 3;
                    memcpy(&world.cutscns[i].img[k].color, ptr, 4); ptr += 4;
                    ptr = world_getspr(ptr, &world.cutscns[i].img[k].spr, tng);
                }
            }
            j = world.cutscns[i].numspc;
            if(j) {
                world.cutscns[i].spc = (world_speech_t*)main_alloc(j * sizeof(world_speech_t));
                for(k = 0; k < j; k++) {
                    world.cutscns[i].spc[k].s = (ptr[2] << 16) | (ptr[1] << 8) | ptr[0]; ptr += 3;
                    ptr = world_getidx(ptr, &world.cutscns[i].spc[k].spc, tng, &world.speech, world.numspc);
                }
            }
            j = world.cutscns[i].numsub;
            if(j) {
                world.cutscns[i].sub = (world_sub_t*)main_alloc(j * sizeof(world_sub_t));
                for(k = 0; k < j; k++) {
                    world.cutscns[i].sub[k].s = (ptr[2] << 16) | (ptr[1] << 8) | ptr[0]; ptr += 3;
                    world.cutscns[i].sub[k].fi = (ptr[2] << 16) | (ptr[1] << 8) | ptr[0]; ptr += 3;
                    world.cutscns[i].sub[k].fo = (ptr[2] << 16) | (ptr[1] << 8) | ptr[0]; ptr += 3;
                    world.cutscns[i].sub[k].e = (ptr[2] << 16) | (ptr[1] << 8) | ptr[0]; ptr += 3;
                    memcpy(&world.cutscns[i].sub[k].color, ptr, 4); ptr += 4;
                    world.cutscns[i].sub[k].x = *ptr++;
                    world.cutscns[i].sub[k].y = *ptr++;
                    world.cutscns[i].sub[k].a = *ptr++;
                    ptr = world_getfnt(ptr, &world.cutscns[i].sub[k].fnt, tng);
                    ptr = world_gettxt(ptr, &world.cutscns[i].sub[k].txt);
                }
            }
            j = world.cutscns[i].bclen;
            world.cutscns[i].bc = (uint8_t*)main_alloc(j);
            memcpy(world.cutscns[i].bc, ptr, j); ptr += j;
        }
        free(buf);
    }
    return 1;
}

/**
 * Add NPC type
 */
int world_addnpctype(int tng, uint64_t offs, uint32_t size, char *name)
{
    uint8_t *ptr, *buf;
    int i, j, k, l, d, len;

    if(!name) return 0;
    for(i = 0; i < world.numnpc && strcmp(world.npctypes[i].name, name); i++);
    if(i >= world.numnpc) {
        i = world.numnpc++;
        world.npctypes = (world_npctype_t*)main_realloc(world.npctypes, world.numnpc * sizeof(world_npctype_t));
        memset(&world.npctypes[i], 0, sizeof(world_npctype_t));
        world.npctypes[i].name = name;
    } else {
        if(world.npctypes[i].attrs) free(world.npctypes[i].attrs);
        if(world.npctypes[i].inv) free(world.npctypes[i].inv);
        if(world.npctypes[i].spr) free(world.npctypes[i].spr);
        if(world.npctypes[i].evt) {
            for(j = 0; j < world.npctypes[i].numevt; j++)
                if(world.npctypes[i].evt[j].bc) free(world.npctypes[i].evt[j].bc);
            free(world.npctypes[i].evt);
        }
    }
    ptr = buf = (uint8_t*)world_loadasset(tng, offs, size, &len);
    if(buf) {
        if(len > 12) {
            ptr = world_gettxt(ptr, &world.npctypes[i].trname);
            world.npctypes[i].behave = *ptr++;
            world.npctypes[i].speed = (ptr[0] | (ptr[1] << 8)); ptr += 2;
            world.npctypes[i].dirchange = (ptr[0] | (ptr[1] << 8)); ptr += 2;
            world.npctypes[i].patrol = *ptr++;
            world.npctypes[i].numattrs = j = *ptr++;
            world.npctypes[i].numinv = *ptr++;
            world.npctypes[i].numspr = *ptr++;
            world.npctypes[i].numevt = *ptr++;
            if(j) {
                world.npctypes[i].attrs = (world_charattr_t*)main_alloc(j * sizeof(world_charattr_t));
                for(k = 0; k < j; k++) {
                    world.npctypes[i].attrs[k].type = *ptr++;
                    ptr = world_getattr(ptr, &world.npctypes[i].attrs[k].attr, tng);
                    memcpy(&d, ptr, 4); ptr += 4;
                    world.npctypes[i].attrs[k].value = SDL_SwapLE32(d);
                }
            }
            j = world.npctypes[i].numinv;
            if(j) {
                world.npctypes[i].inv = (world_npcinv_t*)main_alloc(j * sizeof(world_npcinv_t));
                for(k = 0; k < j; k++) {
                    world.npctypes[i].inv[k].chance = *ptr++;
                    world.npctypes[i].inv[k].qty = (ptr[2] << 16) | (ptr[1] << 8) | ptr[0]; ptr += 3;
                    world.npctypes[i].inv[k].freq = (ptr[2] << 16) | (ptr[1] << 8) | ptr[0]; ptr += 3;
                    ptr = world_getspr(ptr, &world.npctypes[i].inv[k].obj, tng);
                }
            }
            j = world.npctypes[i].numspr;
            if(j) {
                world.npctypes[i].spr = (world_spridx_t*)main_alloc(j * sizeof(world_spridx_t));
                for(k = 0; k < j; k++) {
                    ptr = world_getspr(ptr, &world.npctypes[i].spr[k], tng);
                }
            }
            j = world.npctypes[i].numevt;
            if(j) {
                world.npctypes[i].evt = (world_evthandler_t*)main_alloc(j * sizeof(world_evthandler_t));
                for(k = 0; k < j; k++) {
                    world.npctypes[i].evt[k].type = *ptr++;
                    world.npctypes[i].evt[k].param = (ptr[2] << 16) | (ptr[1] << 8) | ptr[0]; ptr += 3;
                    world.npctypes[i].evt[k].bclen = l = (ptr[2] << 16) | (ptr[1] << 8) | ptr[0]; ptr += 3;
                    world.npctypes[i].evt[k].bc = (uint8_t*)main_alloc(l);
                    memcpy(world.npctypes[i].evt[k].bc, ptr, l); ptr += l;
                }
            }
        }
        free(buf);
    }
    return 1;
}

/**
 * Add spawner
 */
int world_addspawner(int tng, uint64_t offs, uint32_t size, char *name)
{
    uint8_t *ptr, *buf;
    int i, j, k, d, len;

    if(!name) return 0;
    for(i = 0; i < world.numswr && strcmp(world.spawners[i].name, name); i++);
    if(i >= world.numswr) {
        i = world.numswr++;
        world.spawners = (world_spawner_t*)main_realloc(world.spawners, world.numswr * sizeof(world_spawner_t));
        memset(&world.spawners[i], 0, sizeof(world_spawner_t));
        world.spawners[i].name = name;
    } else {
        if(world.spawners[i].kinds) free(world.spawners[i].kinds);
    }
    ptr = buf = (uint8_t*)world_loadasset(tng, offs, size, &len);
    if(buf) {
        if(len > 6) {
            world.spawners[i].num = *ptr++;
            world.spawners[i].freq = (ptr[2] << 16) | (ptr[1] << 8) | ptr[0]; ptr += 3;
            world.spawners[i].numkinds = j = (ptr[0] | (ptr[1] << 8)); ptr += 2;
            if(j) {
                world.spawners[i].kinds = (world_npckind_t*)main_alloc(j * sizeof(world_npckind_t));
                for(k = 0; k < j; k++) {
                    world.spawners[i].kinds[k].req.type = *ptr++;
                    ptr = world_getattr(ptr, &world.spawners[i].kinds[k].req.attr, tng);
                    memcpy(&d, ptr, 4); ptr += 4;
                    world.spawners[i].kinds[k].req.value = SDL_SwapLE32(d);
                    ptr = world_getattr(ptr, &world.spawners[i].kinds[k].attr, tng);
                    memcpy(&d, ptr, 4); ptr += 4;
                    world.spawners[i].kinds[k].value = SDL_SwapLE32(d);
                    ptr = world_getspr(ptr, &world.spawners[i].kinds[k].swn, tng);
                    ptr = world_getspr(ptr, &world.spawners[i].kinds[k].npc, tng);
                }
            }
        }
        free(buf);
    }
    return 1;
}

/**
 * Add conversation dialog
 */
int world_adddialog(int tng, uint64_t offs, uint32_t size, char *name)
{
    uint8_t *ptr, *buf;
    int i, j, k, d, len;

    if(!name) return 0;
    for(i = 0; i < world.numdlg && strcmp(world.dialogs[i].name, name); i++);
    if(i >= world.numdlg) {
        i = world.numdlg++;
        world.dialogs = (world_dialog_t*)main_realloc(world.dialogs, world.numdlg * sizeof(world_dialog_t));
        memset(&world.dialogs[i], 0, sizeof(world_dialog_t));
        world.dialogs[i].name = name;
    } else {
        if(world.dialogs[i].answers) free(world.dialogs[i].answers);
    }
    ptr = buf = (uint8_t*)world_loadasset(tng, offs, size, &len);
    if(buf) {
        if(len > 13) {
            ptr = world_gettxt(ptr, &world.dialogs[i].trname);
            ptr = world_gettxt(ptr, &world.dialogs[i].desc);
            ptr = world_getidx(ptr, &world.dialogs[i].spc, tng, &world.speech, world.numspc);
            ptr = world_getspr(ptr, &world.dialogs[i].spr, tng);
            world.dialogs[i].type = *ptr++;
            world.dialogs[i].numanswers = j = *ptr++;
            if(j) {
                world.dialogs[i].answers = (world_answer_t*)main_alloc(j * sizeof(world_answer_t));
                for(k = 0; k < j; k++) {
                    world.dialogs[i].answers[k].req.type = *ptr++;
                    ptr = world_getattr(ptr, &world.dialogs[i].answers[k].req.attr, tng);
                    memcpy(&d, ptr, 4); ptr += 4;
                    world.dialogs[i].answers[k].req.value = SDL_SwapLE32(d);
                    ptr = world_getattr(ptr, &world.dialogs[i].answers[k].attr, tng);
                    memcpy(&d, ptr, 4); ptr += 4;
                    world.dialogs[i].answers[k].value = SDL_SwapLE32(d);
                    ptr = world_getidx(ptr, &world.dialogs[i].answers[k].spc, tng, &world.speech, world.numspc);
                    ptr = world_gettxt(ptr, &world.dialogs[i].answers[k].trname);
                }
            }
        }
        free(buf);
    }
    return 1;
}

/**
 * Add crafting dialog
 */
int world_addcraft(int tng, uint64_t offs, uint32_t size, char *name)
{
    uint8_t *ptr, *buf;
    int i, j, k, l, len;

    if(!name) return 0;
    for(i = 0; i < world.numcft && strcmp(world.crafts[i].name, name); i++);
    if(i >= world.numcft) {
        i = world.numcft++;
        world.crafts = (world_craft_t*)main_realloc(world.crafts, world.numcft * sizeof(world_craft_t));
        memset(&world.crafts[i], 0, sizeof(world_craft_t));
        world.crafts[i].name = name;
    } else {
        if(world.crafts[i].recipies) free(world.crafts[i].recipies);
    }
    ptr = buf = (uint8_t*)world_loadasset(tng, offs, size, &len);
    if(buf) {
        if(len > 13) {
            ptr = world_gettxt(ptr, &world.crafts[i].trname);
            ptr = world_gettxt(ptr, &world.crafts[i].desc);
            ptr = world_getidx(ptr, &world.crafts[i].snd, tng, &world.sound, world.numsound);
            ptr = world_getspr(ptr, &world.crafts[i].spr, tng);
            world.crafts[i].type = *ptr++;
            world.crafts[i].numrecipies = j = *ptr++;
            if(j) {
                world.crafts[i].recipies = (world_recipie_t*)main_alloc(j * sizeof(world_recipie_t));
                for(k = 0; k < j; k++) {
                    world.crafts[i].recipies[k].qty = (ptr[0] | (ptr[1] << 8)); ptr += 2;
                    ptr = world_getspridx(ptr, &world.crafts[i].recipies[k].obj, SPR_TILE, tng);
                    for(l = 0; l < 16; l++) {
                        world.crafts[i].recipies[k].ing[l].qty = (ptr[0] | (ptr[1] << 8)); ptr += 2;
                        ptr = world_getspridx(ptr, &world.crafts[i].recipies[k].ing[l].obj, SPR_TILE, tng);
                    }
                }
            }
        }
        free(buf);
    }
    return 1;
}

/**
 * Add quest
 */
int world_addquest(int tng, uint64_t offs, uint32_t size, char *name)
{
    uint8_t *ptr, *buf;
    int i, j, len;

    if(!name) return 0;
    for(i = 0; i < world.numqst && strcmp(world.quests[i].name, name); i++);
    if(i >= world.numqst) {
        i = world.numqst++;
        world.quests = (world_quest_t*)main_realloc(world.quests, world.numqst * sizeof(world_quest_t));
        memset(&world.quests[i], 0, sizeof(world_quest_t));
        world.quests[i].name = name;
        world.quests[i].completed = -1;
    } else {
        if(world.quests[i].bc) free(world.quests[i].bc);
    }
    ptr = buf = (uint8_t*)world_loadasset(tng, offs, size, &len);
    if(buf) {
        world.quests[i].tng = tng;
        if(len > 9) {
            world.quests[i].type = *ptr++;
            ptr = world_gettxt(ptr, &world.quests[i].trname);
            ptr = world_gettxt(ptr, &world.quests[i].desc);
            world.quests[i].bclen = j = (ptr[2] << 16) | (ptr[1] << 8) | ptr[0]; ptr += 3;
            world.quests[i].bc = (uint8_t*)main_alloc(j);
            memcpy(world.quests[i].bc, ptr, j); ptr += j;
        }
        free(buf);
    }
    return 1;
}

/**
 * Free an object
 */
void world_freeobj(int idx)
{
    int i;

    if(idx < 0 || idx >= world.numspr[SPR_TILE] || !world.objects[idx]) return;
    if(world.objects[idx]->coll) free(world.objects[idx]->coll);
    if(world.objects[idx]->snd) free(world.objects[idx]->snd);
    if(world.objects[idx]->req) free(world.objects[idx]->req);
    if(world.objects[idx]->mod) free(world.objects[idx]->mod);
    if(world.objects[idx]->spr) free(world.objects[idx]->spr);
    if(world.objects[idx]->evt) {
        for(i = 0; i < world.objects[idx]->numevt; i++)
            if(world.objects[idx]->evt[i].bc) free(world.objects[idx]->evt[i].bc);
        free(world.objects[idx]->evt);
    }
    free(world.objects[idx]); world.objects[idx] = NULL;
}

/**
 * Add object
 */
int world_addobj(int tng, uint64_t offs, uint32_t size, int i)
{
    uint8_t *ptr, *buf;
    uint32_t d;
    int j, k, l, m, len;

    if(i < 0 || i >= world.numspr[SPR_TILE]) return 0;
    if(world.objects[i]) world_freeobj(i);
    world.objects[i] = (world_object_t*)main_alloc(sizeof(world_object_t));
    ptr = buf = (uint8_t*)world_loadasset(tng, offs, size, &len);
    if(buf) {
        if(len > 135) {
            ptr = world_gettxt(ptr, &world.objects[i]->trname);
            world.objects[i]->collw = (ptr[0] | (ptr[1] << 8)); ptr += 2;
            world.objects[i]->collh = (ptr[0] | (ptr[1] << 8)); ptr += 2;
            world.objects[i]->defact = *ptr++;
            ptr = world_getspr(ptr, &world.objects[i]->invspr, tng);
            ptr = world_getidx(ptr, &world.objects[i]->invsnd, tng, &world.sound, world.numsound);
            world.objects[i]->price = (ptr[2] << 16) | (ptr[1] << 8) | ptr[0]; ptr += 3;
            world_getspridx(ptr, &world.objects[i]->unit, SPR_TILE, tng); ptr += 2;
            world.objects[i]->pricecat = *ptr++;
            ptr = world_getspr(ptr, &world.objects[i]->prjspr, tng);
            world.objects[i]->prjdur = (ptr[2] << 16) | (ptr[1] << 8) | ptr[0]; ptr += 3;
            ptr = world_getspr(ptr, &world.objects[i]->ammo, tng);
            world.objects[i]->layer = *ptr++;
            memcpy(&d, ptr, 4); ptr += 4;
            world.objects[i]->eqmask = SDL_SwapLE32(d);
            world.objects[i]->numsnd = *ptr++;
            world.objects[i]->numreq = *ptr++;
            world.objects[i]->nummod = *ptr++;
            world.objects[i]->numspr = *ptr++;
            world.objects[i]->numevt = *ptr++;
            j = world.objects[i]->collw * world.objects[i]->collh;
            if(j) {
                world.objects[i]->coll = (uint8_t*)main_alloc(j);
                memcpy(world.objects[i]->coll, ptr, j); ptr += j;
            }
            for(j = 0; j < 32; j++)
                ptr = world_getspr(ptr, &world.objects[i]->eqspr[j], tng);
            j = world.objects[i]->numsnd;
            if(j) {
                world.objects[i]->snd = (int*)main_alloc(j * sizeof(int));
                for(k = 0; k < j; k++)
                    ptr = world_getidx(ptr, &world.objects[i]->snd[k], tng, &world.sound, world.numsound);
            }
            j = world.objects[i]->numreq;
            if(j) {
                world.objects[i]->req = (world_charattr_t*)main_alloc(j * sizeof(world_charattr_t));
                for(k = 0; k < j; k++) {
                    world.objects[i]->req[k].type = *ptr++;
                    ptr = world_getattr(ptr, &world.objects[i]->req[k].attr, tng);
                    memcpy(&d, ptr, 4); ptr += 4;
                    world.objects[i]->req[k].value = SDL_SwapLE32(d);
                }
            }
            j = world.objects[i]->nummod;
            if(j) {
                world.objects[i]->mod = (world_charattr_t*)main_alloc(j * sizeof(world_charattr_t));
                for(k = 0; k < j; k++) {
                    world.objects[i]->mod[k].type = *ptr++;
                    ptr = world_getattr(ptr, &world.objects[i]->mod[k].attr, tng);
                    memcpy(&d, ptr, 4); ptr += 4;
                    world.objects[i]->mod[k].value = SDL_SwapLE32(d);
                }
            }
            j = world.objects[i]->numspr;
            if(j) {
                world.objects[i]->spr = (world_sprmod_t*)main_alloc(j * sizeof(world_sprmod_t));
                for(k = 0; k < j; k++) {
                    ptr = world_gettxt(ptr, &world.objects[i]->spr[k].name);
                    m = world.hdr.sprperlyr < SPRPERLYR ? m : SPRPERLYR;
                    for(l = 0; l < m; l++)
                        ptr = world_getspr(ptr, &world.objects[i]->spr[k].spr[l], tng);
                    if(world.hdr.sprperlyr > SPRPERLYR) ptr += 3 * (world.hdr.sprperlyr - SPRPERLYR);
                    for(l = 0; l < m; l++)
                        ptr = world_getspr(ptr, &world.objects[i]->spr[k].spr[SPRPERLYR + l], tng);
                    if(world.hdr.sprperlyr > SPRPERLYR) ptr += 3 * (world.hdr.sprperlyr - SPRPERLYR);
                }
            }
            j = world.objects[i]->numevt;
            if(j) {
                world.objects[i]->evt = (world_evthandler_t*)main_alloc(j * sizeof(world_evthandler_t));
                for(k = 0; k < j; k++) {
                    world.objects[i]->evt[k].type = *ptr++;
                    world.objects[i]->evt[k].param = (ptr[2] << 16) | (ptr[1] << 8) | ptr[0]; ptr += 3;
                    world.objects[i]->evt[k].bclen = l = (ptr[2] << 16) | (ptr[1] << 8) | ptr[0]; ptr += 3;
                    world.objects[i]->evt[k].bc = (uint8_t*)main_alloc(l);
                    memcpy(world.objects[i]->evt[k].bc, ptr, l); ptr += l;
                }
            }
        }
        free(buf);
    }
    return 1;
}

/**
 * Load one tng file into the world
 */
int world_loadtng(char *fn)
{
#ifndef __EMSCRIPTEN__
#define myfclose(f) fclose(f)
#else
#define myfclose(f)
#endif
    FILE *f = NULL;
    tng_hdr_t hdr;
    tng_section_t *sec;
    tng_asset_desc_t *desc;
    uint8_t *buf, *comp, *ptr;
    int32_t size, d;
    uint32_t crc;
    int idx, len, i, j, k, l, m;
    char *c;

    if(!fn || !*fn) return 0;
#ifndef __EMSCRIPTEN__
    if(verbose > 1) printf("%s: loading %s\r\n", main_me, fn);
    f = files_open(fn, "rb");
    if(!f) return 0;
#else
    files_seek(NULL, 0);
#endif
    if(files_read(f, &hdr, sizeof(hdr)) != sizeof(hdr) || memcmp(&world.hdr, &hdr, 38) ||
      (hdr.enc && (world.hdr.magic[0] != '#' || world.hdr.enc != hdr.enc))) {
        if(verbose > 1) printf("%s: bad magic or not compatible with world\r\n", main_me);
        myfclose(f); return 0;
    }
    idx = world.numtng++;
    world.tng = (world_tng_t*)realloc(world.tng, world.numtng * sizeof(world_tng_t));
    if(!world.tng) { fclose(f); world.numtng = 0; return 0; }
    world.tng[idx].f[0] = f;                    /* for general purpose use in main thread */
#ifndef __EMSCRIPTEN__
    world.tng[idx].f[1] = files_open(fn, "rb"); /* for the theora decoder thread */
    world.tng[idx].f[2] = files_open(fn, "rb"); /* for SDL_mixer music */
#endif
    world.tng[idx].offs[0] = 64;
    world.tng[idx].encrypted = hdr.magic[0] == '#' && hdr.enc != 0;
    world.tng[idx].ofs[0] = world.numspr[SPR_UI];
    world.tng[idx].ofs[1] = world.numspr[SPR_TILE];
    world.tng[idx].ofs[2] = world.numspr[SPR_CHR];
    world.tng[idx].ofs[3] = world.numspr[SPR_PRT];
    world.tng[idx].ofs[4] = world.numspr[SPR_BG];
    world.tng[idx].ofs[5] = world.numatl;
    if(!world_readtng(idx, 0, &size, 4) || size & 0x80000000) {
szerr:  if(verbose > 1) printf("%s: compressed section table too big\r\n", main_me);
        myfclose(f); world.numtng--; return 0;
    }
    size = SDL_SwapLE32(size);
    if(size < 8) goto szerr;
    comp = (uint8_t*)main_alloc(size);
    if(!world_readtng(idx, 0, comp, size)) {
rderr:  if(verbose > 1) printf("%s: error reading section table\r\n", main_me);
        myfclose(f); free(comp); world.numtng--; return 0;
    }
    crc = crc32(0, comp, size - 4);
    crc = SDL_SwapLE32(crc);
    if(memcmp(comp + size - 4, &crc, 4)) goto rderr;
    len = 0;
    buf = (uint8_t*)stbi_zlib_decode_malloc_guesssize_headerflag((const char*)comp, size - 4, 4096, &len, 1);
    free(comp);
    if(!buf || len < 1) {
        if(verbose > 1) printf("%s: uncompression error\r\n", main_me);
        if(buf) free(buf);
        myfclose(f); world.numtng--; return 0;
    }
    sec = (tng_section_t*)buf;
    /* things to init only for the first tng file */
    if(!idx) {
        world.type = hdr.type & 0x0F;
        world.tilew = hdr.tilew | (((hdr.type >> 4) & 3) << 8);
        world.tileh = hdr.tileh | (((hdr.type >> 6) & 3) << 8);
        world.mapsize = 1 << hdr.mapsize; world.numngh = world.hdr.type < TNG_TYPE_HEXV ? 4 : 6;
        for(i = 0; i < (int)(sec[0].offs / sizeof(tng_section_t)); i++)
            if(TNG_SECTION_TYPE(&sec[i]) == TNG_SECTION_MAPS) {
                desc = (tng_asset_desc_t*)(buf + sec[i].offs);
                world.asiz = 4 + size + desc->offs;
            } else
            if(!world.numspc && TNG_SECTION_TYPE(&sec[i]) == TNG_SECTION_TRANSLATION) {
                world.numspc = (TNG_SECTION_SIZE(&sec[i]) - 2 - sizeof(tng_asset_desc_t)) / sizeof(tng_asset_desc_t);
            }
    }
    /* first, detect the strings section, other sections will need it (should be the first one, but be bullet-proof) */
    for(i = 0; i < (int)(sec[0].offs / sizeof(tng_section_t)); i++)
        if(TNG_SECTION_TYPE(&sec[i]) == TNG_SECTION_STRINGS) {
            world.tng[idx].stslen = TNG_SECTION_SIZE(&sec[i]);
            world.tng[idx].sts = (char*)main_alloc(world.tng[idx].stslen);
            memcpy(world.tng[idx].sts, buf + SDL_SwapLE32(sec[i].offs), world.tng[idx].stslen);
            break;
        }
    /* next, parse asset sections */
    for(i = 0; i < (int)(sec[0].offs / sizeof(tng_section_t)); i++)
        switch(TNG_SECTION_TYPE(&sec[i])) {
            case TNG_SECTION_TRANSLATION:
                ptr = buf + SDL_SwapLE32(sec[i].offs);
                for(j = 0; j < world.numlang && (world.lang[j].id[0] != ptr[0] || world.lang[j].id[1] != ptr[1]); j++);
                if(j < world.numlang) {
                    if(world.lang[j].spcassets) free(world.lang[j].spcassets);
                } else {
                    j = world.numlang++;
                    world.lang = (world_langopt_t*)main_realloc(world.lang, world.numlang * sizeof(world_langopt_t));
                    world.langname = (char**)main_realloc(world.langname, (world.numlang + 1) * sizeof(char*));
                }
                desc = (tng_asset_desc_t*)(ptr + 2);
                memset(&world.lang[j], 0, sizeof(world_langopt_t));
                world.langname[j + 1] = NULL;
                world.langname[j] = world.tng[idx].sts + SDL_SwapLE32(desc->name);
                world.lang[j].id[0] = ptr[0]; world.lang[j].id[1] = ptr[1];
                world.lang[j].offs = 68 + size + SDL_SwapLE64(desc->offs);
                world.lang[j].size = SDL_SwapLE32(desc->size);
                world.lang[j].tng = idx;
                k = (TNG_SECTION_SIZE(&sec[i]) - 2 - sizeof(tng_asset_desc_t)) / sizeof(tng_asset_desc_t);
                if(k > world.numspc) k = world.numspc;
                world.lang[j].spcassets = (tng_asset_desc_t*)main_alloc(world.numspc * sizeof(tng_asset_desc_t));
                for(k = 0, desc++; k < world.numspc; k++, desc++) {
                    world.lang[j].spcassets[k].offs = SDL_SwapLE64(desc->offs) + 68 + size;
                    world.lang[j].spcassets[k].size = SDL_SwapLE32(desc->size);
                    world.lang[j].spcassets[k].name = SDL_SwapLE32(desc->name);
                }
            break;
            case TNG_SECTION_FONTS:
                desc = (tng_asset_desc_t*)(buf + SDL_SwapLE32(sec[i].offs));
                k = TNG_SECTION_SIZE(&sec[i]) / sizeof(tng_asset_desc_t);
                for(j = 0; j < k; j++, desc++)
                    world_addasset(&world.font, &world.numfont, idx, 68 + size + SDL_SwapLE64(desc->offs),
                        SDL_SwapLE32(desc->size), world.tng[idx].sts + SDL_SwapLE32(desc->name));
            break;
            case TNG_SECTION_MUSIC:
                desc = (tng_asset_desc_t*)(buf + SDL_SwapLE32(sec[i].offs));
                k = TNG_SECTION_SIZE(&sec[i]) / sizeof(tng_asset_desc_t);
                for(j = 0; j < k; j++, desc++)
                    world_addasset(&world.music, &world.nummusic, idx, 68 + size + SDL_SwapLE64(desc->offs),
                        SDL_SwapLE32(desc->size), world.tng[idx].sts + SDL_SwapLE32(desc->name));
            break;
            case TNG_SECTION_SOUNDS:
                desc = (tng_asset_desc_t*)(buf + SDL_SwapLE32(sec[i].offs));
                k = TNG_SECTION_SIZE(&sec[i]) / sizeof(tng_asset_desc_t);
                for(j = 0; j < k; j++, desc++)
                    world_addasset(&world.sound, &world.numsound, idx, 68 + size + SDL_SwapLE64(desc->offs),
                        SDL_SwapLE32(desc->size), world.tng[idx].sts + SDL_SwapLE32(desc->name));
            break;
            case TNG_SECTION_MOVIES:
                desc = (tng_asset_desc_t*)(buf + SDL_SwapLE32(sec[i].offs));
                k = TNG_SECTION_SIZE(&sec[i]) / sizeof(tng_asset_desc_t);
                for(j = 0; j < k; j++, desc++)
                    world_addasset(&world.movie, &world.nummovie, idx, 68 + size + SDL_SwapLE64(desc->offs),
                        SDL_SwapLE32(desc->size), world.tng[idx].sts + SDL_SwapLE32(desc->name));
            break;
            case TNG_SECTION_SPRITEATLAS:
                desc = (tng_asset_desc_t*)(buf + SDL_SwapLE32(sec[i].offs));
                k = TNG_SECTION_SIZE(&sec[i]) / sizeof(tng_asset_desc_t);
                world_add2asset(&world.atl, &world.numatl, idx, size, k, desc);
            break;
            case TNG_SECTION_SPRITE_UI:
                desc = (tng_asset_desc_t*)(buf + SDL_SwapLE32(sec[i].offs));
                k = TNG_SECTION_SIZE(&sec[i]) / sizeof(tng_asset_desc_t);
                world_addsprs(&world.spr[SPR_UI], &world.numspr[SPR_UI], idx, size, k, desc);
            break;
            case TNG_SECTION_SPRITE_TILE:
                desc = (tng_asset_desc_t*)(buf + SDL_SwapLE32(sec[i].offs));
                k = TNG_SECTION_SIZE(&sec[i]) / sizeof(tng_asset_desc_t);
                world_addsprs(&world.spr[SPR_TILE], &world.numspr[SPR_TILE], idx, size, k, desc);
            break;
            case TNG_SECTION_SPRITE_CHARACTER:
                desc = (tng_asset_desc_t*)(buf + SDL_SwapLE32(sec[i].offs));
                k = TNG_SECTION_SIZE(&sec[i]) / sizeof(tng_asset_desc_t);
                world_addsprs(&world.spr[SPR_CHR], &world.numspr[SPR_CHR], idx, size, k, desc);
            break;
            case TNG_SECTION_SPRITE_PORTRAIT:
                desc = (tng_asset_desc_t*)(buf + SDL_SwapLE32(sec[i].offs));
                k = TNG_SECTION_SIZE(&sec[i]) / sizeof(tng_asset_desc_t);
                world_addsprs(&world.spr[SPR_PRT], &world.numspr[SPR_PRT], idx, size, k, desc);
            break;
            case TNG_SECTION_SPRITE_BACKGROUND:
                desc = (tng_asset_desc_t*)(buf + SDL_SwapLE32(sec[i].offs));
                k = TNG_SECTION_SIZE(&sec[i]) / sizeof(tng_asset_desc_t);
                world_add2asset(&world.bg, &world.numspr[SPR_BG], idx, size, k, desc);
            break;
            case TNG_SECTION_ATTRS:
                ptr = buf + SDL_SwapLE32(sec[i].offs);
                world.freetotal = (ptr[1] << 8) | ptr[0]; ptr += 2;
                world.freeattrs = (ptr[1] << 8) | ptr[0]; ptr += 2;
                l = *ptr++; ptr += l * 3;
                l = (ptr[1] << 8) | ptr[0]; ptr += 2;
                world.attrs = (world_attr_t*)main_realloc(world.attrs, (world.numattrs + l) * sizeof(world_attr_t));
                memset(&world.attrs[world.numattrs], 0, l * sizeof(world_attr_t));
                for(j = 0; j < l; j++, world.numattrs++) {
                    world.attrs[world.numattrs].type = *ptr++;
                    world.attrs[world.numattrs].name = world.tng[idx].sts + ((ptr[2] << 16) | (ptr[1] << 8) | ptr[0]); ptr += 3;
                    ptr = world_gettxt(ptr, &world.attrs[world.numattrs].trname);
                    world.attrs[world.numattrs].tng = idx;
                    switch(world.attrs[world.numattrs].type) {
                        case 1: case 3:
                            memcpy(&d, ptr, 4); ptr += 4;
                            world.attrs[world.numattrs].defval = SDL_SwapLE32(d);
                            world.attrs[world.numattrs].bclen = k = (ptr[2] << 16) | (ptr[1] << 8) | ptr[0]; ptr += 3;
                            world.attrs[world.numattrs].bc = (uint8_t*)main_alloc(k);
                            memcpy(world.attrs[world.numattrs].bc, ptr, k); ptr += k;
                        break;
                        case 2: case 4:
                            world.attrs[world.numattrs].bclen = k = (ptr[1] << 8) | ptr[0]; ptr += 2;
                            world.attrs[world.numattrs].bc = (uint8_t*)main_alloc(k);
                            memcpy(world.attrs[world.numattrs].bc, ptr, k); ptr += k;
                        break;
                    }
                }
                /* we can only parse the engine specific attributes after we've parsed the attributes themselves */
                ptr = buf + SDL_SwapLE32(sec[i].offs) + 4;
                l = *ptr++; if(l > 4) l = 4;
                for(j = 0; j < l; j++)
                    ptr = world_getattr(ptr, &world.engattr[j], idx);
            break;
            case TNG_SECTION_ACTIONS:
                desc = (tng_asset_desc_t*)(buf + SDL_SwapLE32(sec[i].offs));
                k = TNG_SECTION_SIZE(&sec[i]) / sizeof(tng_asset_desc_t);
                if(k > (int)(sizeof(world.defact)/sizeof(world.defact[0]))) k = (int)(sizeof(world.defact)/sizeof(world.defact[0]));
                for(j = 0; j < k; j++, desc++) {
                    world_getattr((uint8_t*)&desc->name, &world.defact[j].value, idx);
                    world.defact[j].bc = world_loadasset(idx, 68 + size + SDL_SwapLE64(desc->offs), SDL_SwapLE32(desc->size),
                        &world.defact[j].bclen);
                    world.defact[j].tng = idx;
                }
            break;
            case TNG_SECTION_MAPS:
                desc = (tng_asset_desc_t*)(buf + SDL_SwapLE32(sec[i].offs));
                k = TNG_SECTION_SIZE(&sec[i]) / sizeof(tng_asset_desc_t);
                for(j = 0; j < k; j++, desc++)
                    world_addasset(&world.map, &world.nummap, idx, 68 + size + SDL_SwapLE64(desc->offs), SDL_SwapLE32(desc->size),
                        world.tng[idx].sts + SDL_SwapLE32(desc->name));
            break;
        }
    /* interface */
    for(i = 0; i < (int)(sec[0].offs / sizeof(tng_section_t)); i++)
        switch(TNG_SECTION_TYPE(&sec[i])) {
            case TNG_SECTION_ALERTS:
                ptr = buf + SDL_SwapLE32(sec[i].offs);
                ptr = world_getfnt(ptr, &world.fonts[FNT_ALERT], idx);
                world.alert_dt = (ptr[1] << 8) | ptr[0]; ptr += 2;
                world.alert_fi = (ptr[1] << 8) | ptr[0]; ptr += 2;
                world.alert_fo = (ptr[1] << 8) | ptr[0]; ptr += 2;
                l = *ptr++; world.numalr = l > WORLD_NUMALRT ? WORLD_NUMALRT : l;
                for(j = 0; j < world.numalr; j++) {
                    memcpy(&world.alerts[j].color, ptr, 4); ptr += 4;
                    ptr = world_getidx(ptr, &world.alerts[j].snd, idx, &world.sound, world.numsound);
                    ptr = world_gettxt(ptr, &world.alerts[j].trname);
                }
            break;
            case TNG_SECTION_CREDITS:
                ptr = buf + SDL_SwapLE32(sec[i].offs);
                ptr = world_getidx(ptr, &world.crdmusic, idx, &world.music, world.nummusic);
                ptr = world_getspr(ptr, &world.crdimage, idx);
                memcpy(&world.colors[COLOR_CRD_HDR], ptr, 4); ptr += 4;
                ptr = world_getfnt(ptr, &world.fonts[FNT_CRD_HDR], idx);
                memcpy(&world.colors[COLOR_CRD_NAME], ptr, 4); ptr += 4;
                ptr = world_getfnt(ptr, &world.fonts[FNT_CRD_NAME], idx);
                m = 0;
                while(ptr < buf + sec[i].offs + TNG_SECTION_SIZE(&sec[i])) {
                    c = strchr(crdcat, *ptr++);
                    l = (ptr[1] << 8) | ptr[0]; ptr += 2;
                    if(c) {
                        k = c - crdcat;
                        world.crds[k].names = (char**)main_realloc(world.crds[k].names, (world.crds[k].num + l) * sizeof(char*));
                        for(j = 0; j < l; j++, world.crds[k].num++, m++, ptr += 3)
                            world.crds[k].names[world.crds[k].num] = world.tng[idx].sts + ((ptr[2] << 16) | (ptr[1] << 8) | ptr[0]);
                        qsort(world.crds[k].names, world.crds[k].num, sizeof(char*), strnatcmp);
                    } else
                        ptr += l * 3;
                }
                if(!m) world.crdmusic = world.crdimage.idx = -1;
            break;
            case TNG_SECTION_CUTSCENES:
                desc = (tng_asset_desc_t*)(buf + SDL_SwapLE32(sec[i].offs));
                k = TNG_SECTION_SIZE(&sec[i]) / sizeof(tng_asset_desc_t);
                for(j = 0; j < k; j++, desc++)
                    world_addcutscn(idx, 68 + size + SDL_SwapLE64(desc->offs),
                        SDL_SwapLE32(desc->size), world.tng[idx].sts + SDL_SwapLE32(desc->name));
            break;
            case TNG_SECTION_CHOOSERS:
                desc = (tng_asset_desc_t*)(buf + SDL_SwapLE32(sec[i].offs));
                k = TNG_SECTION_SIZE(&sec[i]) / sizeof(tng_asset_desc_t);
                for(j = 0; j < k; j++, desc++)
                    world_addchooser(idx, 68 + size + SDL_SwapLE64(desc->offs),
                        SDL_SwapLE32(desc->size), world.tng[idx].sts + desc->name);
            break;
            case TNG_SECTION_NPCS:
                desc = (tng_asset_desc_t*)(buf + SDL_SwapLE32(sec[i].offs));
                k = TNG_SECTION_SIZE(&sec[i]) / sizeof(tng_asset_desc_t);
                for(j = 0; j < k; j++, desc++)
                    world_addnpctype(idx, 68 + size + SDL_SwapLE64(desc->offs), SDL_SwapLE32(desc->size),
                        world.tng[idx].sts + SDL_SwapLE32(desc->name));
            break;
            case TNG_SECTION_SPAWNERS:
                desc = (tng_asset_desc_t*)(buf + SDL_SwapLE32(sec[i].offs));
                k = TNG_SECTION_SIZE(&sec[i]) / sizeof(tng_asset_desc_t);
                for(j = 0; j < k; j++, desc++)
                    world_addspawner(idx, 68 + size + SDL_SwapLE64(desc->offs), SDL_SwapLE32(desc->size),
                        world.tng[idx].sts + SDL_SwapLE32(desc->name));
            break;
            case TNG_SECTION_DIALOGS:
                desc = (tng_asset_desc_t*)(buf + SDL_SwapLE32(sec[i].offs));
                k = TNG_SECTION_SIZE(&sec[i]) / sizeof(tng_asset_desc_t);
                for(j = 0; j < k; j++, desc++)
                    world_adddialog(idx, 68 + size + SDL_SwapLE64(desc->offs), SDL_SwapLE32(desc->size),
                        world.tng[idx].sts + SDL_SwapLE32(desc->name));
            break;
            case TNG_SECTION_CRAFTS:
                desc = (tng_asset_desc_t*)(buf + SDL_SwapLE32(sec[i].offs));
                k = TNG_SECTION_SIZE(&sec[i]) / sizeof(tng_asset_desc_t);
                for(j = 0; j < k; j++, desc++)
                    world_addcraft(idx, 68 + size + SDL_SwapLE64(desc->offs), SDL_SwapLE32(desc->size),
                        world.tng[idx].sts + SDL_SwapLE32(desc->name));
            break;
            case TNG_SECTION_QUESTS:
                desc = (tng_asset_desc_t*)(buf + SDL_SwapLE32(sec[i].offs));
                k = TNG_SECTION_SIZE(&sec[i]) / sizeof(tng_asset_desc_t);
                for(j = 0; j < k; j++, desc++)
                    world_addquest(idx, 68 + size + SDL_SwapLE64(desc->offs), SDL_SwapLE32(desc->size),
                        world.tng[idx].sts + SDL_SwapLE32(desc->name));
            break;
        }
    /* finally, parse complex sections, referencing other sections */
    for(i = 0; i < (int)(sec[0].offs / sizeof(tng_section_t)); i++)
        switch(TNG_SECTION_TYPE(&sec[i])) {
            case TNG_SECTION_UI:
                ptr = buf + SDL_SwapLE32(sec[i].offs);
                l = *ptr++; k = l > 10 ? 10 : l; memcpy(&world.colors[COLOR_TABN], ptr, k * 4); ptr += k * 4;
                l = *ptr++; k = l > 6 ? 6 : l;
                for(j = 0; j < k; j++) {
                    ptr = world_getfnt(ptr, &world.fonts[j], idx);
                    world.padtop[j] = (ptr[1] << 8) | ptr[0]; ptr += 2;
                }
                if(k < l) ptr += (l - k) * 7;
                l = *ptr++; k = l > 3 ? 3 : l;
                world.onselsnd = world.onclksnd = world.onerrsnd = -1;
                if(l > 0) ptr = world_getidx(ptr, &world.onselsnd, idx, &world.sound, world.numsound);
                if(l > 1) ptr = world_getidx(ptr, &world.onclksnd, idx, &world.sound, world.numsound);
                if(l > 2) ptr = world_getidx(ptr, &world.onerrsnd, idx, &world.sound, world.numsound);
                if(k < l) ptr += (l - k) * 3;
                for(j = 0; j < E_LAST && ptr < buf + SDL_SwapLE32(sec[i].offs) + TNG_SECTION_SIZE(&sec[i]); j++)
                    ptr = world_getspr(ptr, &world.uispr[j], idx);
            break;
            case TNG_SECTION_MAINMENU:
                ptr = buf + SDL_SwapLE32(sec[i].offs);
                if(TNG_SECTION_SIZE(&sec[i]) >= 3)
                    ptr = world_getcut(ptr, &world.intro, idx);
                else
                    world.intro = -1;
                if(TNG_SECTION_SIZE(&sec[i]) < 4) {
                    world.hasmenu = 0;
                } else {
                    world.hasmenu = 1;
                    ptr = world_getidx(ptr, &world.bgmusic, idx, &world.music, world.nummusic);
                    ptr = world_getidx(ptr, &world.bgmovie, idx, &world.movie, world.nummovie);
                    ptr = world_getspr(ptr, &world.bgimage, idx);
                    l = *ptr++; k = l > WORLD_MAXPRX ? WORLD_MAXPRX : l;
                    for(j = 0; j < WORLD_MAXPRX; j++)
                        world.menuprx[j].spr.idx = -1;
                    for(j = 0; j < k; j++) {
                        world.menuprx[j].dx = ((int)ptr[1] << 8) | ptr[0]; ptr += 2;
                        world.menuprx[j].dy = ((int)ptr[1] << 8) | ptr[0]; ptr += 2;
                        world.menuprx[j].t = (ptr[1] << 8) | ptr[0]; ptr += 2;
                        ptr = world_getspr(ptr, &world.menuprx[j].spr, idx);
                    }
                    if(k < l) ptr += (l - k) * 9;
                    ptr = world_getspr(ptr, &world.titimg, idx);
                    world.hastitle = *ptr++;
                    memcpy(&world.colors[COLOR_TITLE], ptr, 4); ptr += 4;
                    memcpy(&world.colors[COLOR_TITSH], ptr, 4); ptr += 4;
                    ptr = world_getfnt(ptr, &world.fonts[FNT_TITLE], idx);
                    world.url = world.tng[idx].sts + ((ptr[2] << 16) | (ptr[1] << 8) | ptr[0]); ptr += 3;
                    world.padtop[PAD_MENU] = (ptr[1] << 8) | ptr[0]; ptr += 2;
                    ptr = world_getfnt(ptr, &world.fonts[FNT_MENU], idx);
                    l = *ptr++; k = l > 5 ? 5 : l;
                    for(j = 0; j < k; j++) {
                        memcpy(&world.colors[COLOR_MENUN + j], ptr, 4); ptr += 4;
                        ptr = world_getspr(ptr, &world.menubtn[j].l, idx);
                        ptr = world_getspr(ptr, &world.menubtn[j].m, idx);
                        ptr = world_getspr(ptr, &world.menubtn[j].r, idx);
                    }
                    if(k < l) ptr += (l - k) * 13;
                    memcpy(&world.colors[COLOR_GRFG], ptr, 4); ptr += 4;
                    memcpy(&world.colors[COLOR_GRBG], ptr, 4); ptr += 4;
                }
            break;
            case TNG_SECTION_HUD:
                ptr = buf + SDL_SwapLE32(sec[i].offs);
                ptr = world_getspr(ptr, &world.navcirc, idx);
                ptr = world_getspr(ptr, &world.navbtns, idx);
                ptr = world_getspr(ptr, &world.itembarbg, idx);
                ptr = world_getspr(ptr, &world.itembarfg, idx);
                world.item_n = *ptr++; world.item_w = *ptr++; world.item_h = *ptr++; world.item_l = *ptr++;
                world.item_t = *ptr++; world.item_g = *ptr++; world.item_p = *ptr++; world.item_c = *ptr++;
                if(world.item_n > 32) world.item_n = 32;
                ptr = world_getspr(ptr, &world.statusbarbg, idx);
                ptr = world_getspr(ptr, &world.statusbarfg, idx);
                ptr = world_getspr(ptr, &world.invicon, idx);
                ptr = world_getspr(ptr, &world.inviconp, idx);
                world.inv_w = *ptr++; world.inv_h = *ptr++; world.inv_l = *ptr++; world.inv_t = *ptr++;
                ptr = world_getfnt(ptr, &world.fonts[FNT_INV], idx);
                ptr = world_getspr(ptr, &world.invslot, idx);
                ptr = world_getspr(ptr, &world.invslots, idx);
                ptr = world_getspr(ptr, &world.equipimg, idx);
                l = *ptr++; k = l > 32 ? 32 : l;
                world.pbars = (world_pbar_t*)main_realloc(world.pbars, (world.numpbar + k) * sizeof(world_pbar_t));
                for(j = 0; j < k; j++, world.numpbar++) {
                    world.pbars[world.numpbar].type = *ptr++;
                    world.pbars[world.numpbar].l = (ptr[1] << 8) | ptr[0]; ptr += 2;
                    world.pbars[world.numpbar].t = (ptr[1] << 8) | ptr[0]; ptr += 2;
                    ptr = world_getattr(ptr, &world.pbars[world.numpbar].valueattr, idx);
                    ptr = world_getattr(ptr, &world.pbars[world.numpbar].maxattr, idx);
                    ptr = world_getspr(ptr, &world.pbars[world.numpbar].fg, idx);
                    ptr = world_getspr(ptr, &world.pbars[world.numpbar].bg, idx);
                }
            break;
            case TNG_SECTION_EQUIPSLOTS:
                ptr = buf + SDL_SwapLE32(sec[i].offs);
                l = TNG_SECTION_SIZE(&sec[i]); if(l > 128) l = 128; /* 32 2 * * sizeof(uint16_t) */
                for(j = 0; j < l / (int)sizeof(uint16_t); j++, ptr += 2)
                    world.equippos[j] = (ptr[1] << 8) | ptr[0];
            break;
            case TNG_SECTION_STARTUP:
                desc = (tng_asset_desc_t*)(buf + SDL_SwapLE32(sec[i].offs));
                k = TNG_SECTION_SIZE(&sec[i]) / sizeof(tng_asset_desc_t);
                if(k > WORLD_NUMCRON) k = WORLD_NUMCRON;
                for(j = 0; j < k; j++, desc++) {
                    world.cron[j].value = SDL_SwapLE32(desc->name);
                    world.cron[j].bc = world_loadasset(idx, 68 + size + SDL_SwapLE64(desc->offs), SDL_SwapLE32(desc->size),
                        &world.cron[j].bclen);
                    world.cron[j].tng = idx;
                }
            break;
            case TNG_SECTION_CHARS:
                desc = (tng_asset_desc_t*)(buf + SDL_SwapLE32(sec[i].offs));
                k = TNG_SECTION_SIZE(&sec[i]) / sizeof(tng_asset_desc_t);
                for(j = 0; j < k; j++, desc++)
                    world_addchargrp(idx, 68 + size + SDL_SwapLE64(desc->offs),
                        SDL_SwapLE32(desc->size), world.tng[idx].sts + SDL_SwapLE32(desc->name));
            break;
            case TNG_SECTION_TILES:
                ptr = buf + SDL_SwapLE32(sec[i].offs);
                if(world.hdr.numtrn > 0 && world.numspr[SPR_TILE]) {
                    world.tiles = (world_tile_t**)realloc(world.tiles, world.numspr[SPR_TILE] * sizeof(world_tile_t*));
                    memset(&world.tiles[world.tng[idx].ofs[1]], 0, (world.numspr[SPR_TILE] - world.tng[idx].ofs[1]) * sizeof(world_tile_t*));
                    m = 2 + world.hdr.numtrn * 5;
                    k = TNG_SECTION_SIZE(&sec[i]) / m;
                    for(j = 0; j < k; j++) {
                        l = world.tng[idx].ofs[1] + ((ptr[1] << 8) | ptr[0]); ptr += 2;
                        if(l >= world.numspr[SPR_TILE]) break;
                        world.tiles[l] = (world_tile_t*)main_alloc(world.hdr.numtrn * sizeof(world_tile_t));
                        for(m = 0; m < world.hdr.numtrn; m++) {
                            world.tiles[l][m].speed = (ptr[1] << 8) | ptr[0]; ptr += 2;
                            ptr = world_getidx(ptr, &world.tiles[l][m].snd, idx, &world.sound, world.numsound);
                        }
                    }
                }
            break;
            case TNG_SECTION_OBJECTS:
                if(world.numspr[SPR_TILE]) {
                    desc = (tng_asset_desc_t*)(buf + SDL_SwapLE32(sec[i].offs));
                    k = TNG_SECTION_SIZE(&sec[i]) / sizeof(tng_asset_desc_t);
                    world.objects = (world_object_t**)realloc(world.objects, world.numspr[SPR_TILE] * sizeof(world_object_t*));
                    memset(&world.objects[world.tng[idx].ofs[2]], 0, (world.numspr[SPR_TILE] - world.tng[idx].ofs[2]) * sizeof(world_object_t*));
                    for(j = 0; j < k; j++, desc++)
                        world_addobj(idx, 68 + size + SDL_SwapLE64(desc->offs), SDL_SwapLE32(desc->size),
                            world.tng[idx].ofs[2] + SDL_SwapLE32(desc->name));
                }
            break;
        }
    free(buf);
    return 1;
}

/**
 * Load translated texts and speech
 */
void world_loadtext(int lng)
{
    tng_asset_desc_t *desc;
    char *ptr;
    int j, len;

    if(!world.lang || lng < 0 || lng >= world.numlang) return;
    /* free previous dictionary if any */
    if(world.textbuf) { free(world.textbuf); world.textbuf = NULL; }
    if(world.text) { free(world.text); world.text = NULL; }
    world.numtext = 0; world.langidx = lng;
    if(world.speech) memset(world.speech, 0, world.numspc * sizeof(world_asset_t));
    else world.speech = (world_asset_t*)main_alloc(world.numspc * sizeof(world_asset_t));
    /* load speech audio references */
    desc = world.lang[lng].spcassets;
    for(j = 0; j < world.numspc; j++, desc++)
        if(desc->size) {
            world.speech[j].tng = world.lang[lng].tng;
            world.speech[j].offs = desc->offs;
            world.speech[j].size = desc->size;
            world.speech[j].name = world.tng[world.lang[lng].tng].sts + desc->name;
        } else {
            world.speech[j].tng = world.lang[0].tng;
            world.speech[j].offs = world.lang[0].spcassets[j].offs;
            world.speech[j].size = world.lang[0].spcassets[j].size;
            world.speech[j].name = world.tng[world.lang[0].tng].sts + world.lang[0].spcassets[j].name;
        }
    if(world.lang[lng].offs && world.lang[lng].size > 0) {
        /* load translated texts */
        world.textbuf = world_loadasset(world.lang[lng].tng, world.lang[lng].offs, world.lang[lng].size, &len);
        if(world.textbuf) {
            for(ptr = world.textbuf, world.numtext = 0; ptr < world.textbuf + len; ptr++)
                if(!*ptr) world.numtext++;
            world.text = (char**)main_alloc((world.numtext + 1) * sizeof(char*));
            for(ptr = world.textbuf, j = 0; j < world.numtext && ptr < world.textbuf + len; j++) {
                world.text[j] = ptr;
                ptr += strlen(ptr) + 1;
            }
        }
    }
    if(verbose > 1) printf("%s: loading translation '%s' %s\n", main_me, world.lang[lng].id, world.text ? "ok" : "failed");
}

/**
 * Load world, load one or more tng files into the world
 */
int world_load(char *fn)
{
#ifndef __EMSCRIPTEN__
    char **extensions, *s, *path;
    int i;
#endif
    if(!fn || !*fn || !world_loadtng(fn)) return 0;
#ifndef __EMSCRIPTEN__
    path = (char*)main_alloc(strlen(fn) + FILENAME_MAX + 1);
    strcpy(path, fn);
    s = strrchr(path, SEP[0]);
    if(!s) { strcpy(path, "."); s = path + 1; } else *s = 0;
    extensions = files_getdir(path, "tng", NULL);
    *s++ = SEP[0];
    if(extensions)
        for(i = 0; extensions[i]; i++) {
            if(!strcmp(extensions[i], "game.tng")) continue;
            strcpy(s, extensions[i]);
            if(!world_loadtng(path) && verbose)
                printf("%s: unable to load extension '%s'\r\n", main_me, path);
        }
    files_freedir(&extensions, NULL);
    free(path);
#endif
    return 1;
}

/**
 * Initialize world
 */
void world_init(char *detlng)
{
    int i, j, k;

    if(detlng && *detlng) {
        if(world.lang) {
            if(verbose > 1) printf("%s: detected language '%s'\r\n", main_me, detlng);
            for(i = 0; i < world.numlang && (world.lang[i].id[0] != detlng[0] || world.lang[i].id[1] != detlng[1]); i++);
            if(i >= world.numlang) {
                if(verbose > 1) printf("%s: dictionary not found, fallback\r\n", main_me);
                for(i = 0; i < world.numlang && (world.lang[i].id[0] != 'e' || world.lang[i].id[1] != 'n'); i++);
                if(i >= world.numlang) i = 0;
            }
            world_loadtext(i);
        } else
            if(verbose > 1) printf("%s: no dictionaries in game\r\n", main_me);
    }
    world.numglobals = world.numpris = world.numchrattrs = 0;
    if(world.attrs) {
        for(i = 0; i < world.numattrs; i++) {
            if(world.attrs[i].type >= ATTR_GLOBVAR) world.numglobals++;
            if(world.attrs[i].type == ATTR_PRI) world.numpris++;
        }
        if(world.numglobals > 0)
            world.globals = (int*)main_alloc(world.numglobals * sizeof(int));
        for(world.numglobals = world.numchrattrs = i = 0; i < world.numattrs; i++)
            if(world.attrs[i].type >= ATTR_GLOBVAR) {
                world.globals[world.numglobals] = world.attrs[i].defval;
                world.attrs[i].idx = world.numglobals++;
            } else
                world.attrs[i].idx = world.numchrattrs++;
        for(i = 0; i < world.numchar; i++) {
            /* make sure no primary value options are bigger than world.freeattrs */
            for(j = 0; j < world.chars[i].numopts; j++) {
                for(k = 0; k < world.chars[i].opts[j].na; k++)
                    if(world.chars[i].opts[j].atr[k].type == REL_SET &&
                      world.attrs[world.chars[i].opts[j].atr[k].attr].type == ATTR_PRI &&
                      world.chars[i].opts[j].atr[k].value > world.freeattrs)
                        world.freeattrs = world.chars[i].opts[j].atr[k].value;
            }
        }
        /* set calculated global attributes */
        for(i = 0; i < world.numattrs; i++)
            if(world.attrs[i].type == ATTR_GLOBCALC) {
                j = script_load(SCRIPT_CALLER(SCRIPT_ATTR, i), world.attrs[i].bc, world.attrs[i].bclen, world.attrs[i].tng,
                    NULL, NULL);
                world.globals[world.attrs[i].idx] = j > SCRIPT_AGAIN ?
                    (j < -WORLD_ATTRMAX ? -WORLD_ATTRMAX : (j > WORLD_ATTRMAX ? WORLD_ATTRMAX : j)) : 0;
            }
    }
    if(world.numpris * world.freeattrs < world.freetotal)
        world.freetotal = world.numpris * world.freeattrs;
    if(world.nummap)
        world.objlyr = (uint16_t**)main_alloc(world.nummap * sizeof(uint16_t*));
    for(k = 0; k < WORLD_NUMCRDS; k++) {
        for(i = j = 0; i < (int)(sizeof(crds)/sizeof(crds[0])); i++) if(crds[i][0] == crdcat[k]) j++;
        if(j < 1) continue;
        world.crds[k].names = (char**)main_realloc(world.crds[k].names, (world.crds[k].num + j) * sizeof(char*));
        for(i = j = 0; i < (int)(sizeof(crds)/sizeof(crds[0])); i++)
            if(crds[i][0] == crdcat[k]) world.crds[k].names[world.crds[k].num++] = (char*)crds[i] + 1;
        qsort(world.crds[k].names, world.crds[k].num, sizeof(char*), strnatcmp);
    }
}

/**
 * Free all resources
 */
void world_free()
{
    int i, j;

    if(world.tng) {
        for(i = 0; i < world.numtng; i++) {
            if(world.tng[i].sts) free(world.tng[i].sts);
#ifndef __EMSCRIPTEN__
            for(j = 0; j < 3; j++)
                if(world.tng[i].f[j]) fclose(world.tng[i].f[j]);
#endif
        }
        free(world.tng);
    }
    if(world.lang) {
        for(i = 0; i < world.numlang; i++) { if(world.lang[i].spcassets) free(world.lang[i].spcassets); }
        free(world.lang);
    }
    if(world.langname) free(world.langname);
    if(world.textbuf) free(world.textbuf);
    if(world.text) free(world.text);
    if(world.speech) free(world.speech);
    if(world.font) free(world.font);
    if(world.music) free(world.music);
    if(world.sound) free(world.sound);
    if(world.movie) free(world.movie);
    for(j = SPR_UI; j <= SPR_PRT; j++)
        if(world.spr[j]) {
            for(i = 0; i < world.numspr[j] * 8; i++) { if(world.spr[j][i].data) free(world.spr[j][i].data); }
            free(world.spr[j]);
        }
    if(world.bg) free(world.bg);
    if(world.atl) free(world.atl);
    if(world.map) free(world.map);
    if(world.pbars) free(world.pbars);
    if(world.attrs) {
        for(i = 0; i < world.numattrs; i++) { if(world.attrs[i].bc) free(world.attrs[i].bc); }
        free(world.attrs);
    }
    if(world.globals) free(world.globals);
    if(world.chars) {
        for(i = 0; i < world.numchar; i++) world_freechargrp(i);
        free(world.chars);
    }
    if(world.tiles) {
        for(i = 0; i < world.numspr[SPR_TILE]; i++)
            if(world.tiles[i]) free(world.tiles[i]);
        free(world.tiles);
    }
    if(world.objects) {
        for(i = 0; i < world.numspr[SPR_TILE]; i++)
            if(world.objects[i]) world_freeobj(i);
        free(world.objects);
    }
    for(j = 0; j < 10; j++)
        if(world.crds[j].names) free(world.crds[j].names);
    for(j = 0; j < (int)(sizeof(world.cron)/sizeof(world.cron[0])); j++)
        if(world.cron[j].bc) free(world.cron[j].bc);
    for(j = 0; j < (int)(sizeof(world.defact)/sizeof(world.defact[0])); j++)
        if(world.defact[j].bc) free(world.defact[j].bc);
    if(world.choosers) {
        for(i = 0; i < world.numchr; i++) { if(world.choosers[i].opts) free(world.choosers[i].opts); }
        free(world.choosers);
    }
    if(world.cutscns) {
        for(i = 0; i < world.numcut; i++) {
            if(world.cutscns[i].img) free(world.cutscns[i].img);
            if(world.cutscns[i].spc) free(world.cutscns[i].spc);
            if(world.cutscns[i].sub) free(world.cutscns[i].sub);
            if(world.cutscns[i].bc) free(world.cutscns[i].bc);
        }
        free(world.cutscns);
    }
    if(world.npctypes) {
        for(i = 0; i < world.numnpcs; i++) {
            if(world.npctypes[i].attrs) free(world.npctypes[i].attrs);
            if(world.npctypes[i].inv) free(world.npctypes[i].inv);
            if(world.npctypes[i].spr) free(world.npctypes[i].spr);
            if(world.npctypes[i].evt) {
                for(j = 0; j < world.npctypes[i].numevt; j++)
                    if(world.npctypes[i].evt[j].bc) free(world.npctypes[i].evt[j].bc);
                free(world.npctypes[i].evt);
            }
        }
        free(world.npctypes);
    }
    if(world.spawners) {
        for(i = 0; i < world.numswr; i++) { if(world.spawners[i].kinds) free(world.spawners[i].kinds); }
        free(world.spawners);
    }
    if(world.dialogs) {
        for(i = 0; i < world.numdlg; i++) { if(world.dialogs[i].answers) free(world.dialogs[i].answers); }
        free(world.dialogs);
    }
    if(world.crafts) {
        for(i = 0; i < world.numcft; i++) { if(world.crafts[i].recipies) free(world.crafts[i].recipies); }
        free(world.crafts);
    }
    if(world.quests) {
        for(i = 0; i < world.numqst; i++) { if(world.quests[i].bc) free(world.quests[i].bc); }
        free(world.quests);
    }
    if(world.mapngh) free(world.mapngh);
    world_flush();
    memset(&world, 0, sizeof(world));
}

/**
 * Free a map
 */
void world_freemap(world_map_t *map)
{
    int i;
    if(!map) return;
    if(map->prx) free(map->prx);
    if(map->paths) {
        for(i = 0; i < map->numpaths; i++)
            if(map->paths[i]) free(map->paths[i]);
        free(map->paths);
    }
    if(map->tiles) free(map->tiles);
    if(map->coll) free(map->coll);
    free(map);
}

/**
 * Load all map neightbors (server only)
 */
void world_loadneightbors()
{
    uint8_t *buf, *ptr, *unc;
    char *sts;
    int i, j, k, l;

    if(world.mapngh) { free(world.mapngh); world.mapngh = NULL; }
    if(world.nummap < 1 || world.numngh < 1) return;
    world.mapngh = (int*)malloc(world.nummap * world.numngh * sizeof(int));
    if(!world.mapngh) return;

    for(i = 0; i < world.nummap; i++) {
        for(l = 0; l < world.numngh; l++) world.mapngh[i * world.numngh + l] = -1;
        buf = (uint8_t*)malloc(world.map[i].size);
        if(!buf) continue;
        world_seektng(world.map[i].tng, 0, world.map[i].offs);
        if(!world_readtng(world.map[i].tng, 0, buf, world.map[i].size)) {
            if(verbose > 1) printf("%s: error reading asset, map %u\r\n", main_me, i);
            free(buf);
            continue;
        }
        l = 0;
        unc = ptr = (void*)stbi_zlib_decode_malloc_guesssize_headerflag((const char*)buf, world.map[i].size, 4096, &l, 1);
        if(!ptr || l < 34) {
            if(verbose > 1) printf("%s: uncompression error, map %u\r\n", main_me, i);
            if(ptr) free(ptr);
            free(buf);
            continue;
        }
        free(buf);
        ptr += 3;
        sts = world.tng[world.map[i].tng].sts;
        for(l = 0; l < world.numngh; l++) {
            j = (ptr[2] << 16) | (ptr[1] << 8) | ptr[0]; ptr += 3; k = -1;
            if(j) {
                for(k = 0; k < world.nummap && strcmp(world.map[k].name, sts + j); k++);
                if(k >= world.nummap) k = -1;
            }
            world.mapngh[i * world.numngh + l] = k;
        }
        free(unc);
    }
}

/**
 * Serialize and compress inventory
 */
int world_serializeinv(int idx, uint8_t *buf, int maxlen)
{
    uLongf cl;
    uint8_t *ptr, *unc;
    int i, j, l;

    if(idx < 0 || idx >= world.numplayers || !world.players || !world.players[idx].data.inv ||
      world.players[idx].data.numinv < 1) return 0;
    l = 2 + 6 * world.players[idx].data.numinv;
    unc = ptr = (uint8_t*)malloc(l);
    if(!unc) return 0;
    *ptr++ = world.players[idx].data.numinv & 0xff; *ptr++ = (world.players[idx].data.numinv >> 8) & 0xff;
    for(i = 0; i < world.players[idx].data.numinv; i++) {
        j = SDL_SwapLE32(world.players[idx].data.inv[i].qty);
        memcpy(ptr, &j, 4); ptr += 4;
        *ptr++ = world.players[idx].data.inv[i].obj.idx & 0xff; *ptr++ = (world.players[idx].data.inv[i].obj.idx >> 8) & 0xff;
    }
    cl = maxlen;
    if(compress2(buf, &cl, unc, l, 9) != Z_OK) cl = 0;
    free(unc);
    return cl;
}

/**
 * Serialize and compress a map
 */
uint8_t *world_serializemap(world_map_t *map, int *len)
{
    /* do not use main_alloc() here, that would exit on error */
    uint16_t *tiles, t;
    uint8_t *comp, *buf;
    int i, j, k, l, m = world.mapsize * world.mapsize, o = 3;
    uLongf cl;

    if(!map || !len) return NULL;
    *len = 0;
    /* failsafes */
    if(!map->prx) map->numprx = 0;
    if(!map->paths) map->numpaths = 0;
    l = 34 + map->numprx * 10 + MAPS_LYR_NUM * m * 3;
    if(map->paths && map->numpaths)
        for(i = 0; i < map->numpaths; i++)
            l += 3 + map->paths[i][1] * 4;
    buf = (uint8_t*)malloc(l);
    if(!buf) return NULL;
    buf[0] = map->trname & 0xff; buf[1] = (map->trname >> 8) & 0xff;  buf[2] = (map->trname >> 16) & 0xff;
    for(i = 0; i < 6; i++) {
        j = map->neightbors[i] == -1 ? 0 : (int)(world.map[map->neightbors[i]].name - world.tng[world.map[map->neightbors[i]].tng].sts);
        buf[o++] = j & 0xff; buf[o++] = (j >> 8) & 0xff;  buf[o++] = (j >> 16) & 0xff;
    }
    buf[o++] = map->daylen & 0xff; buf[o++] = (map->daylen >> 8) & 0xff;
    buf[o++] = map->dayspr.cat; buf[o++] = map->dayspr.idx & 0xff; buf[o++] = (map->dayspr.idx >> 8) & 0xff;
    j = map->bgmus == -1 ? 0 : (int)(world.music[map->bgmus].name - world.tng[world.music[map->bgmus].tng].sts);
    buf[o++] = j & 0xff; buf[o++] = (j >> 8) & 0xff; buf[o++] = (j >> 16) & 0xff;
    buf[o++] = map->numprx;
    buf[o++] = map->numpaths;
    o += 3;
    if(map->prx && map->numprx)
        for(i = 0; i < map->numprx; i++) {
            buf[o++] = map->prx[i].type;
            buf[o++] = map->prx[i].sx;
            buf[o++] = map->prx[i].dx;
            buf[o++] = map->prx[i].sy;
            buf[o++] = map->prx[i].dy;
            buf[o++] = map->prx[i].t & 0xff; buf[o++] = (map->prx[i].t >> 8) & 0xff;
            buf[o++] = map->prx[i].spr.cat; buf[o++] = map->prx[i].spr.idx & 0xff; buf[o++] = (map->prx[i].spr.idx >> 8) & 0xff;
        }
    if(map->paths && map->numpaths)
        for(i = 0; i < map->numpaths; i++) {
            buf[o++] = map->paths[i][0];
            l = map->paths[i][1];
            buf[o++] = l & 0xff; buf[o++] = (l >> 8) & 0xff;
            for(j = 0; j < l * 2; j++) {
                buf[o++] = map->paths[i][2 + j] & 0xff; buf[o++] = (map->paths[i][2 + j] >> 8) & 0xff;
            }
        }
    if(map->tiles) {
        tiles = (uint16_t*)malloc(MAPS_LYR_NUM * m * sizeof(uint16_t));
        if(tiles) {
            for(j = k = 0; j < MAPS_LYR_NUM; j++)
                for(i = 0; i < m; i++, k++) {
                    t = map->tiles[k];
                    if(t != 0xffff)
                        t -= world.tng[map->id].ofs[j == MAPS_LYR_NPCS || j == MAPS_LYR_SPWN ? 0 : SPR_TILE];
                    tiles[k] = SDL_SwapLE16(t);
                }
            tng_rle_enc16(tiles, MAPS_LYR_NUM * m, buf + o, &l);
            free(tiles);
        } else l = 0;
        buf[31] = l & 0xff; buf[32] = (l >> 8) & 0xff; buf[33] = (l >> 16) & 0xff;
        o += l;
    }
    cl = compressBound(o);
    comp = (uint8_t*)malloc(cl);
    if(comp) compress2(comp, &cl, buf, o, 9);
    free(buf);
    *len = comp ? cl : 0;
    return comp;
}

/**
 * Deserialize a compressed map asset
 */
world_map_t *world_deserializemap(uint8_t *buf, int len, int idx)
{
    /* do not use main_alloc() here, that would exit on error */
    world_map_t *map;
    uint8_t *ptr, *unc;
    int i, j, k, l, m = world.mapsize * world.mapsize;
    char *sts;

    if(!buf || len < 1 || idx < 0 || idx >= world.nummap) return NULL;
    l = 0;
    unc = ptr = (void*)stbi_zlib_decode_malloc_guesssize_headerflag((const char*)buf, len, 4096, &l, 1);
    if(!ptr || l < 34) {
        if(verbose > 1) printf("%s: uncompression error\r\n", main_me);
        if(ptr) free(ptr);
        return NULL;
    }
    map = (world_map_t*)malloc(sizeof(world_map_t));
    if(!map) return NULL;
    memset(map, 0, sizeof(world_map_t));
    map->id = idx;
    ptr = world_gettxt(ptr, &map->trname);
    sts = world.tng[world.map[idx].tng].sts;
    for(i = 0; i < 6; i++) {
        j = (ptr[2] << 16) | (ptr[1] << 8) | ptr[0]; ptr += 3; k = -1;
        if(j) {
            for(k = 0; k < world.nummap && strcmp(world.map[k].name, sts + j); k++);
            if(k >= world.nummap) k = -1;
        }
        map->neightbors[i] = k;
    }
    map->daylen = (ptr[1] << 8) | ptr[0]; ptr += 2;
    ptr = world_getspr(ptr, &map->dayspr, world.map[idx].tng);
    ptr = world_getidx(ptr, &map->bgmus, world.map[idx].tng, &world.music, world.nummusic);
    map->numprx = *ptr++;
    map->numpaths = *ptr++;
    l = (ptr[2] << 16) | (ptr[1] << 8) | ptr[0]; ptr += 3;
    if(map->numprx) {
        map->prx = (world_mapprx_t*)malloc(map->numprx * sizeof(world_mapprx_t));
        if(!map->prx) { ptr += map->numprx * 10; map->numprx = 0; }
        else
            for(i = 0; i < map->numprx; i++) {
                map->prx[i].type = *ptr++;
                map->prx[i].sx = (int)ptr[0]; ptr++;
                map->prx[i].dx = ptr[0]; ptr++;
                map->prx[i].sy = (int)ptr[0]; ptr++;
                map->prx[i].dy = ptr[0]; ptr++;
                map->prx[i].t = (ptr[1] << 8) | ptr[0]; ptr += 2;
                ptr = world_getspr(ptr, &map->prx[i].spr, world.map[idx].tng);
            }
    }
    if(map->numpaths) {
        map->paths = (uint16_t**)malloc(map->numpaths * sizeof(uint16_t*));
        if(!map->paths) {
            for(i = 0; i < map->numpaths; i++) { ptr++; k = (ptr[1] << 8) | ptr[0]; ptr += 2 + k * 4; }
            map->numpaths = 0;
        } else
            for(i = 0; i < map->numpaths; i++) {
                j = *ptr++; k = (ptr[1] << 8) | ptr[0]; ptr += 2;
                map->paths[i] = (uint16_t*)malloc((k * 1) * 2 * sizeof(uint16_t));
                if(!map->paths[i]) { ptr += k * 4; }
                else {
                    map->paths[i][0] = j; map->paths[i][1] = k;
                    for(j = 0; j < 2 * k; j++) {
                        map->paths[i][2 + k] = (ptr[1] << 8) | ptr[0]; ptr += 2;
                    }
                }
            }
    }
    if(l) {
        k = MAPS_LYR_NUM * m;
        map->tiles = (uint16_t*)malloc(k * sizeof(uint16_t));
        if(map->tiles) {
            memset(map->tiles, 0xff, k * sizeof(uint16_t));
            tng_rle_dec16(ptr, l, map->tiles, &k);
            for(j = k = 0; j < MAPS_LYR_NUM; j++)
                for(i = 0; i < m; i++, k++) {
                    map->tiles[k] = SDL_SwapLE16(map->tiles[k]);
                    if(map->tiles[k] != 0xffff)
                        map->tiles[k] += world.tng[world.map[map->id].tng].ofs[j == MAPS_LYR_NPCS || j == MAPS_LYR_SPWN ? 0 : SPR_TILE];
                }
        }
    }
    free(unc);
    map->coll = (uint8_t*)malloc(m);
    if(map->coll) memset(map->coll, 0, m);
    return map;
}

/**
 * Load one map
 */
world_map_t *world_loadmap(int id)
{
    /* do not use main_alloc() here, that would exit on error */
    world_map_t *map = NULL;
    uint8_t *buf;

    if(id < 0 || id >= world.nummap || !world.map || !world.map[id].offs || !world.map[id].size) return NULL;
    buf = (uint8_t*)malloc(world.map[id].size);
    if(!buf) return NULL;
    world_seektng(world.map[id].tng, 0, world.map[id].offs);
    if(!world_readtng(world.map[id].tng, 0, buf, world.map[id].size)) {
        if(verbose > 1) printf("%s: error reading asset\r\n", main_me);
        free(buf);
        return NULL;
    }
    map = world_deserializemap(buf, world.map[id].size, id);
    free(buf);
    return map;
}

/**
 * Get one serialized map
 */
uint8_t *world_getmap(int id, int *len)
{
    /* do not use main_alloc() here, that would exit on error */
    world_map_t *map = NULL;
    uint8_t *buf;
    int l, m = world.mapsize * world.mapsize;

    if(len) *len = 0;
    if(id < 0 || id >= world.nummap || !world.map[id].offs || !world.map[id].size) return NULL;
    buf = (uint8_t*)malloc(world.map[id].size);
    if(!buf) return NULL;
    world_seektng(world.map[id].tng, 0, world.map[id].offs);
    if(!world_readtng(world.map[id].tng, 0, buf, world.map[id].size)) {
        if(verbose > 1) printf("%s: error reading asset\r\n", main_me);
        free(buf);
        return NULL;
    }
    /* if objects layer unmodified, then return as-is, without doing anything */
    if(!world.objlyr || !world.objlyr[id]) {
        if(len) *len = world.map[id].size;
        return buf;
    }
    /* otherwise uncompress, deserialize, update object layer, re-serialize, re-compress */
    map = world_deserializemap(buf, world.map[id].size, id);
    free(buf); buf = NULL;
    if(map) {
        memcpy(map->tiles + MAPS_LYR_OBJECTS * m, world.objlyr[id], m * sizeof(uint16_t));
        buf = world_serializemap(map, &l);
        world_freemap(map);
        if(len) *len = l;
    }
    return buf;
}

/**
 * Free dynamic resources only
 */
void world_flush()
{
    int i;

    if(world.denylist) {
        free(world.denylist); world.denylist = NULL;
    }
    if(world.players) {
        for(i = 0; i < world.numplayers; i++)
            world_playerdel(i);
        free(world.players); world.players = NULL;
    }
    if(world.npcs) {
        for(i = 0; i < world.numnpcs; i++) {
            if(world.npcs[i].data.attrs) free(world.npcs[i].data.attrs);
            if(world.npcs[i].data.inv) free(world.npcs[i].data.inv);
            if(world.npcs[i].data.quests) free(world.npcs[i].data.quests);
        }
        free(world.npcs); world.npcs = NULL;
    }
    if(world.objlyr) {
        for(i = 0; i < world.nummap; i++) { if(world.objlyr[i]) free(world.objlyr[i]); }
        free(world.objlyr); world.objlyr = NULL;
    }
    world.numplayers = world.numnpcs = world.numdenylist = 0;
}

/**
 * Remove a player record
 */
void world_playerdel(int idx)
{
    if(!world.players || idx < 0 || idx >= world.numplayers) return;
    if(world.players[idx].name) free(world.players[idx].name);
    if(world.players[idx].opts) free(world.players[idx].opts);
    if(world.players[idx].data.attrs) free(world.players[idx].data.attrs);
    if(world.players[idx].data.inv) free(world.players[idx].data.inv);
    if(world.players[idx].data.quests) free(world.players[idx].data.quests);
    /*netq_free(&world.players[idx].nq);*/
    memset(&world.players[idx], 0, sizeof(world_player_t));
}

/**
 * Clean up pending registrations and timed out clients
 * Returns number of clients currently connected
 */
int world_playercleanup(uint64_t t)
{
    int i, ncon = 0;

    if(!world.players) { world.numplayers = 0; return 0; }
    if(t < 1) t = time(NULL);
    for(i = 0; i < world.numplayers; i++) {
        if(!*((uint64_t*)&world.players[i].pass) && world.players[i].created + 60 * 60 * 24 < t) {
            world_playerdel(i);
            memcpy(&world.players[i], &world.players[i + 1], (world.numplayers - i) * sizeof(world_player_t));
            world.numplayers--; i--; continue;
        }
        if(!world.players[i].logoutd && world.players[i].lastd + 10 < t) {
            world.players[i].logoutd = t; memset(&world.players[i].remote_ip, 0, 16);
            world.players[i].fam = 0; world.players[i].remote_port = 0;
        }
        if(!world.players[i].logoutd) ncon++;
    }
    return ncon;
}

/**
 * Generate password hash for world_register
 */
void world_hashpass(char *pass, uint8_t *hash)
{
    mbedtls_sha256_context sha;
    const char *salt = "TirNanoG Server Password Hash Salt";

    if(!pass || !hash) return;
    mbedtls_sha256_init(&sha);
    mbedtls_sha256_starts_ret(&sha, 0);
    mbedtls_sha256_update_ret(&sha, (uint8_t*)pass, strlen(pass));
    mbedtls_sha256_finish_ret(&sha, hash);
    mbedtls_sha256_free(&sha);
    mbedtls_sha256_init(&sha);
    mbedtls_sha256_starts_ret(&sha, 0);
    mbedtls_sha256_update_ret(&sha, (uint8_t*)salt, strlen(salt));
    mbedtls_sha256_update_ret(&sha, hash, 32);
    mbedtls_sha256_update_ret(&sha, (uint8_t*)world.hdr.id, sizeof(world.hdr.id));
    mbedtls_sha256_finish_ret(&sha, hash);
    mbedtls_sha256_free(&sha);
}

/**
 * Register a player in world
 * pass: hash
 * opts: (option value << 8 | option palette) x world.numchar
 * attrs: attribute values x world.numchrattrs
 *
 * Possibilities:
 * 1. user already in list with non-empty pass: deny registration, no matter what
 * 2. user already exists with empty pass and arg pass empty: just refresh time
 * 3. user already exists with empty pass and arg pass non-empty: use that slot
 * 4. user doesn't exists and arg pass empty: add a new empty slot, no matter the noreg flag
 * 5. user doesn't exists and arg pass non-empty: only add if noreg flag is clear
 */
int world_register(char *user, uint8_t *pass, int *opts, int *attrs)
{
    world_entity_t ent = { 0 };
    world_charopt_t *opt;
    world_inv_t *inv = NULL;
    int idx, i, j, l, ok, numinv = 0;
    uint64_t t = time(NULL);

    if(!user || !*user || (!pass && (opts || attrs))) return -1;
    for(i = 0; user[i]; i++) if(user[i] < ' ' || user[i] == '\"') return -1;
    /* check if a user by this name already exists */
    for(idx = 0; world.players && idx < world.numplayers; idx++)
        if(!strcmp(user, world.players[idx].name)) {
            /* exists and with a password, this is a valid record, we have no chance, deny */
            if(*((uint64_t*)&world.players[idx].pass)) return -1;
            /* if we don't have a password either, then just bump the creation date */
            if(!pass) { world.players[idx].created = t; return idx; }
            break;
        }
    /* check attributes */
    if(attrs && world.attrs) {
        ent.attrs = attrs;
        for(i = l = 0; i < world.numattrs; i++) {
            if(world.attrs[i].type == ATTR_PRI) {
                if(world.freetotal > 0 && attrs[world.attrs[i].idx] > world.freeattrs) return -1;
                l += attrs[world.attrs[i].idx];
            } else
            if(world.attrs[i].type == ATTR_CHRCALC) {
                j = script_load(SCRIPT_CALLER(SCRIPT_ATTR, i), world.attrs[i].bc, world.attrs[i].bclen, world.attrs[i].tng,
                    &ent, NULL);
                attrs[world.attrs[i].idx] = j > SCRIPT_AGAIN ?
                    (j < -WORLD_ATTRMAX ? -WORLD_ATTRMAX : (j > WORLD_ATTRMAX ? WORLD_ATTRMAX : j)) : 0;
            }
        }
        if(world.freetotal > 0 && l < world.freetotal) return -1;
    }
    /* check if all character option rules obeyed */
    if(opts) {
        for(j = 0; j < world.numchar; j++) {
            i = opts[j] >> 8;
            if(i < 0 || i >= world.chars[j].numopts) return -1;
            opt = &world.chars[j].opts[i];
            ok = 1;
            if(opt->na > 0 && attrs && world.attrs)
                for(l = 0; ok && l < opt->na; l++)
                    if(world.attrs[opt->atr[l].attr].type < ATTR_GLOBVAR)
                        switch(opt->atr[l].type) {
                            case REL_SET:
                            case REL_EQ: ok &= (attrs[world.attrs[opt->atr[l].attr].idx] == opt->atr[l].value); break;
                            case REL_NE: ok &= (attrs[world.attrs[opt->atr[l].attr].idx] != opt->atr[l].value); break;
                            case REL_GE: ok &= (attrs[world.attrs[opt->atr[l].attr].idx] >= opt->atr[l].value); break;
                            case REL_GT: ok &= (attrs[world.attrs[opt->atr[l].attr].idx] > opt->atr[l].value); break;
                            case REL_LE: ok &= (attrs[world.attrs[opt->atr[l].attr].idx] <= opt->atr[l].value); break;
                            case REL_LT: ok &= (attrs[world.attrs[opt->atr[l].attr].idx] < opt->atr[l].value); break;
                        }
            if(!ok) { if(inv) { free(inv); } return -1; }
            if(opt->ni) {
                inv = (world_inv_t*)realloc(inv, (numinv + opt->ni) * sizeof(world_inv_t));
                if(!inv) { numinv = 0; continue; }
                memcpy(&inv[numinv], opt->inv, opt->ni * sizeof(world_inv_t));
                numinv += opt->ni;
            }
        }
    }
    /* add new record */
    if(idx >= world.numplayers) {
        /* with noreg we only add a new record if it already existed with an empty password */
        if(world.noreg && pass) { if(inv) { free(inv); } return -1; }
        idx = world.numplayers++;
        world.players = (world_player_t*)realloc(world.players, world.numplayers * sizeof(world_player_t));
        if(!world.players) { world.numplayers = 0; if(inv) { free(inv); } return -1; }
        memset(&world.players[idx], 0, sizeof(world_player_t));
    }
    world.players[idx].created = t;
    if(!world.players[idx].name) {
        world.players[idx].name = (char*)malloc(strlen(user) + 1);
        if(world.players[idx].name)
            strcpy(world.players[idx].name, user);
        else { world.numplayers--; if(inv) { free(inv); } return -1; }
    }
    if(pass) memcpy(&world.players[idx].pass, pass, 32);
    if(opts) {
        world.players[idx].opts = (int*)malloc(world.numchar * sizeof(int));
        if(world.players[idx].opts)
            memcpy(world.players[idx].opts, opts, world.numchar * sizeof(int));
        else { world.numplayers--; free(world.players[idx].name); if(inv) { free(inv); } return -1; }
    }
    memset(world.players[idx].equip, 0xff, sizeof(world.players[0].equip));
    memset(world.players[idx].belt, 0xff, sizeof(world.players[0].belt));
    if(attrs) {
        world.players[idx].data.attrs = (int*)malloc(world.numchrattrs * sizeof(int));
        if(world.players[idx].data.attrs)
            memcpy(world.players[idx].data.attrs, attrs, world.numchrattrs * sizeof(int));
        else { world.numplayers--; free(world.players[idx].name); free(world.players[idx].opts); if(inv) { free(inv); } return -1; }
    }
    if(inv) {
        world.players[idx].data.numinv = numinv;
        world.players[idx].data.inv = inv;
    }
    if(verbose > 2) printf("%s: %s '%s', idx %u\r\n", main_me, pass ? "registering" : "adding", user, idx);
    return idx;
}

/**
 * Duplicate string into one which has attribute references parsed
 * {} = username
 * {attribute} = attribute value
 * {9attribute} = attribute value padded to the right
 */
char *world_parsestr(char *str, char *user, int *attrs)
{
    char *ret, *s, *d, *a, tmp[32], fmt[4] = { '%', '1', 'd', 0 };
    int l, i;

    if(!str || !*str) return NULL;
    if(!user && world.text) user = world.text[GAME_USERNAME];
    l = strlen(str);
    for(s = str; *s; s++)
        if(*s == '{') l += (s[1] == '}' ? 256 : 10);
    s = str;
    ret = d = (char*)malloc(l + 1);
    if(ret) {
        while(*s) {
            if(*s == '{') {
                s++;
                if(*s == '}') {
                    if(user && *user) {
                        l = strlen(user);
                        memcpy(d, user, l);
                        d += l;
                    }
                    s++;
                    continue;
                }
                fmt[1] = '1';
                if(*s > '1' && *s <= '9')
                    fmt[1] = *s++;
                a = s; while(*s && *s != '}') s++;
                sprintf(tmp, fmt, 0);
                if(attrs && world.attrs && *s) {
                    *s = 0;
                    for(i = 0; i < world.numattrs && strcmp(a, world.attrs[i].name); i++);
                    *s = '}';
                    if(world.attrs[i].type >= ATTR_GLOBVAR) {
                        if(world.globals) sprintf(tmp, fmt, world.globals[world.attrs[i].idx]);
                    } else
                        sprintf(tmp, fmt, attrs[world.attrs[i].idx]);
                }
                l = strlen(tmp);
                memcpy(d, tmp, l);
                d += l;
                if(*s) s++;
            } else
                *d++ = *s++;
        }
        *d = 0;
    }
    return ret;
}

/**
 * Get serialized world entity size
 */
int world_entity_size(world_entity_t *ent)
{
    int ret = 22 + 4 * world.numchrattrs;
    if(ent->inv    && ent->numinv > 0)  ret += ent->numinv * 8;
    if(ent->skills && ent->numskl > 0)  ret += ent->numskl * 8;
    if(ent->quests && ent->numqst > 0)  ret += ent->numqst * 4;
    return ret;
}

/**
 * Write out serialized world entity
 */
void world_entity_wr(uint8_t *buf, world_entity_t *ent)
{
    int o = 0, j, s;

    s = SDL_SwapLE32((ent->map & 0xffffff) | (ent->d << 24)); memcpy(buf, &s, 4); o = 4;
    s = SDL_SwapLE32(ent->x); memcpy(buf + o, &s, 4); o += 4;
    s = SDL_SwapLE32(ent->y); memcpy(buf + o, &s, 4); o += 4;
    buf[o++] = ent->behave; buf[o++] = ent->transport; buf[o++] = ent->altitude;
    buf[o++] = world.numchrattrs;
    j = (ent->inv && ent->numinv > 0 ? ent->numinv : 0);    buf[o++] = j & 0xff; buf[o++] = (j >> 8) & 0xff;
    j = (ent->skills && ent->numskl > 0 ? ent->numskl : 0); buf[o++] = j & 0xff; buf[o++] = (j >> 8) & 0xff;
    j = (ent->quests && ent->numqst > 0 ? ent->numqst : 0); buf[o++] = j & 0xff; buf[o++] = (j >> 8) & 0xff;
    if(ent->attrs) {
        for(j = 0; j < world.numchrattrs; j++) {
            s = SDL_SwapLE32(ent->attrs[j]);
            memcpy(buf + o + j * 4, &s, 4);
        }
    } else
        memset(buf + o, 0, world.numchrattrs * 4);
    o += world.numchrattrs * 4;
    if(ent->inv && ent->numinv > 0) {
        for(j = 0; j < ent->numinv; j++) {
            s = SDL_SwapLE32(ent->inv[j].qty);
            memcpy(buf + o + j * 8, &s, 4);
            s = SDL_SwapLE32(ent->inv[j].obj.idx);
            memcpy(buf + o + j * 8 + 4, &s, 4);
        }
        o += ent->numinv * 8;
    }
    if(ent->skills && ent->numskl > 0) {
        for(j = 0; j < ent->numskl; j++) {
            s = SDL_SwapLE32(ent->skills[j].qty);
            memcpy(buf + o + j * 8, &s, 4);
            s = SDL_SwapLE32(ent->skills[j].obj.idx);
            memcpy(buf + o + j * 8 + 4, &s, 4);
        }
        o += ent->numskl * 8;
    }
    if(ent->quests && ent->numqst > 0) {
        for(j = 0; j < ent->numqst; j++) {
            s = ent->quests[j * 2 + 1] | (ent->quests[j * 2] << 31);
            s = SDL_SwapLE32(s);
            memcpy(buf + o + j * 4, &s, 4);
        }
        o += ent->numqst * 8;
    }
}

/**
 * Read in serialized world entity
 */
void world_entity_rd(int player, uint8_t *buf, world_entity_t *ent)
{
    int o = 0, j, l, s = 0;

    memcpy(&s, buf, 3); ent->map = SDL_SwapLE32(s); ent->d = buf[3]; o = 4;
    memcpy(&s, buf + o, 4); ent->x = SDL_SwapLE32(s); o += 4;
    memcpy(&s, buf + o, 4); ent->y = SDL_SwapLE32(s); o += 4;
    ent->behave = buf[o++]; ent->transport = buf[o++]; ent->altitude = buf[o++];
    l = buf[o++];
    ent->numinv = buf[o] | (buf[o + 1] << 8); o += 2;
    ent->numskl = buf[o] | (buf[o + 1] << 8); o += 2;
    ent->numqst = buf[o] | (buf[o + 1] << 8); o += 2;
    ent->attrs = (int*)malloc(world.numchrattrs * sizeof(int));
    if(ent->attrs)
        for(j = 0; j < (l < world.numchrattrs ? l : world.numchrattrs); j++) { memcpy(&s, buf + o + j * 4, 4); ent->attrs[j] = SDL_SwapLE32(s); }
    o += l * 4;
    if(ent->numinv) {
        ent->inv = (world_inv_t*)malloc(ent->numinv * sizeof(world_inv_t));
        if(ent->inv)
            for(j = 0; j < ent->numinv; j++, o += 8) {
                memcpy(&s, buf + o, 4); ent->inv[j].qty = SDL_SwapLE32(s);
                memcpy(&s, buf + o + 4, 4); ent->inv[j].obj.idx = SDL_SwapLE32(s); ent->inv[j].obj.cat = SPR_TILE;
                if(ent->inv[j].obj.idx >= world.numspr[SPR_TILE]) { ent->numinv--; j--; continue; }
            }
        else { o += ent->numinv * 8; ent->numinv = 0; }
    }
    if(ent->numskl) {
        ent->skills = (world_inv_t*)malloc(ent->numskl * sizeof(world_inv_t));
        if(ent->skills)
            for(j = 0; j < ent->numskl; j++, o += 8) {
                memcpy(&s, buf + o, 4); ent->skills[j].qty = SDL_SwapLE32(s);
                memcpy(&s, buf + o + 4, 4); ent->skills[j].obj.idx = SDL_SwapLE32(s); ent->skills[j].obj.cat = SPR_TILE;
                if(ent->skills[j].obj.idx >= world.numspr[SPR_TILE]) { ent->numskl--; j--; continue; }
            }
        else { o += ent->numskl * 8; ent->numskl = 0; }
    }
    if(player >= 0 && player < world.numplayers && world.players && ent->numqst) {
        ent->quests = (int*)malloc(ent->numqst * 2 * sizeof(int));
        if(ent->quests)
            for(j = 0; j < ent->numqst; j++, o += 5) {
                memcpy(&s, buf + o, 4); s = SDL_SwapLE32(s) & 0x7fffffff;
                if(s >= world.numqst || (world.quests[s].type &&
                  world.quests[s].completed != -1 && world.quests[s].completed != player)) {
                    ent->numqst--; j--; continue;
                }
                ent->quests[j * 2] = buf[o] >> 7;
                ent->quests[j * 2 + 1] = s;
            }
        else { o += ent->numqst * 4; ent->numqst = 0; }
    }
}

/**
 * Create a serialized world state buffer
 */
uint8_t *world_serializestate(int type, int useridx, int *len)
{
    uint8_t *ret = NULL, *comp, *buf = NULL;
    uint16_t *be;
    uint32_t crc;
    uint64_t S;
    int i, j, k, l, m, o, s, siz = 0, first, last;

    if(!len) return NULL;
    *len = 0;

    if(type == SERTYPE_SERVER && world.denylist && world.numdenylist > 0) {
        l = 8 + world.numdenylist * 17; s = SDL_SwapLE32(l);
        buf = (uint8_t*)realloc(buf, siz + l);
        if(!buf) return NULL;
        memcpy(buf + siz, "DENY", 4); memcpy(buf + siz + 4, &s, 4);
        memcpy(buf + siz + 8, world.denylist, world.numdenylist * 17);
        siz += l;
    }

    if(world.globals && world.numglobals > 0) {
        l = 8 + world.numglobals * 4; s = SDL_SwapLE32(l);
        buf = (uint8_t*)realloc(buf, siz + l);
        if(!buf) return NULL;
        memcpy(buf + siz, "GLBL", 4); memcpy(buf + siz + 4, &s, 4);
        for(i = 0; i < world.numglobals; i++) {
            s = SDL_SwapLE32(world.globals[i]);
            memcpy(buf + siz + 8 + i * 4, &s, 4);
        }
        siz += l;
    }

    if(world.quests && world.numqst > 0) {
        for(i = l = 0; i < world.numqst; i++)
            if(world.quests[i].type) l += 4;
        if(l) {
            l += 8; s = SDL_SwapLE32(l);
            buf = (uint8_t*)realloc(buf, siz + l);
            if(!buf) return NULL;
            memcpy(buf + siz, "QSTS", 4); memcpy(buf + siz + 4, &s, 4);
            for(i = 0; i < world.numqst; i++)
                if(world.quests[i].type) {
                    s = SDL_SwapLE32(world.quests[i].completed);
                    memcpy(buf + siz + 8 + i * 4, &s, 4);
                }
            siz += l;
        }
    }

    if(world.players && world.numplayers > 0 && useridx < world.numplayers) {
        if(useridx < 0) { first = 0; last = world.numplayers; } else { first = useridx; last = useridx + 1; }
        for(i = first; i < last; i++) {
            o = strlen(world.players[i].name) + 1;
            l = 76 + 128 + o + world.numchar * 4 + world_entity_size(&world.players[i].data);
            s = SDL_SwapLE32(l);
            buf = (uint8_t*)realloc(buf, siz + l);
            if(!buf) return NULL;
            memcpy(buf + siz, "USER", 4); memcpy(buf + siz + 4, &s, 4);
            memcpy(buf + siz + 8, world.players[i].pass, 32);
            S = SDL_SwapLE64(world.players[i].created); memcpy(buf + siz + 40, &S, 8);
            S = SDL_SwapLE64(world.players[i].logind); memcpy(buf + siz + 48, &S, 8);
            S = SDL_SwapLE64(world.players[i].logoutd); memcpy(buf + siz + 56, &S, 8);
            S = SDL_SwapLE64(world.players[i].lastd); memcpy(buf + siz + 64, &S, 8);
            buf[siz + 72] = world.players[i].lang[0];
            buf[siz + 73] = world.players[i].lang[1];
            buf[siz + 74] = world.players[i].banned;
            buf[siz + 75] = world.numchar;
            memcpy(buf + siz + 76, world.players[i].name, o);
            o += siz + 76;
            if(world.players[i].opts) {
                for(j = 0; j < world.numchar; j++) {
                    s = SDL_SwapLE32(world.players[i].opts[j]);
                    memcpy(buf + o + j * 4, &s, 4);
                }
            } else
                memset(buf + o, 0, world.numchar * 4);
            o += world.numchar * 4;
            for(j = 0; j < 32; j++) {
                buf[o++] = world.players[i].equip[j] & 0xff;
                buf[o++] = (world.players[i].equip[j] >> 8) & 0xff;
            }
            for(j = 0; j < 32; j++) {
                buf[o++] = world.players[i].belt[j] & 0xff;
                buf[o++] = (world.players[i].belt[j] >> 8) & 0xff;
            }
            world_entity_wr(buf + o, &world.players[i].data);
            siz += l;
        }
    }

    if(world.npcs && world.numnpcs > 0) {
        for(i = 0; i < world.numnpcs; i++) {
            l = 16 + world_entity_size(&world.npcs[i].data);
            s = SDL_SwapLE32(l);
            buf = (uint8_t*)realloc(buf, siz + l);
            if(!buf) return NULL;
            memcpy(buf + siz, "NPC", 4); memcpy(buf + siz + 4, &s, 4);
            s = SDL_SwapLE32(world.npcs[i].type); memcpy(buf + siz + 8, &s, 4);
            s = SDL_SwapLE32(world.npcs[i].spawner); memcpy(buf + siz + 12, &s, 4);
            world_entity_wr(buf + siz + 16, &world.players[i].data);
            siz += l;
        }
    }

    if(world.objlyr && world.nummap > 0) {
        for(i = 0; i < world.nummap; i++) {
            if(!world.objlyr[i]) continue;
            m = world.mapsize * world.mapsize; l = m * 3;
            comp = (uint8_t*)malloc(l);
            if(comp) {
                be = (uint16_t*)malloc(m * sizeof(uint16_t));
                if(be) {
                    for(k = 0; k < m; k++) be[k] = SDL_SwapLE16(world.objlyr[i][k]);
                    tng_rle_enc16(be, m, comp, &l);
                    free(be);
                    l += 12;
                    s = SDL_SwapLE32(l);
                    buf = (uint8_t*)realloc(buf, siz + l);
                    if(!buf) { free(comp); return NULL; }
                    memcpy(buf + siz, "MAP", 4); memcpy(buf + siz + 4, &s, 4);
                    s = SDL_SwapLE32(i); memcpy(buf + siz + 8, &s, 4);
                    memcpy(buf + siz + 12, comp, l - 12);
                    free(comp);
                    siz += l;
                } else { free(comp); return NULL; }
            }
        }
    }

    /* compress and add crc */
    if(buf) {
        S = compressBound(siz);
        ret = (uint8_t*)malloc(S + 4);
        if(ret) {
            memset(ret, 0, S + 4);
            compress2(ret, (uLongf*)&S, buf, siz, 9);
            if(S) {
                crc = crc32(0, ret, S);
                crc = SDL_SwapLE32(crc);
                memcpy(ret + S, &crc, 4);
                *len = S + 4;
            } else {
                free(ret); ret = NULL;
            }
        }
        free(buf);
    }
    return ret;
}

/**
 * Serialize world state to a saved game file
 */
int world_savestate(char *fn, uint8_t *prvw, int plen, uint32_t playedsec)
{
#ifndef __EMSCRIPTEN__
    char *path;
    uint8_t tmp[32], *data;
    FILE *f;
    int i, len = 0;

    data = world_serializestate(prvw && plen > 0 ? SERTYPE_CLIENT : SERTYPE_SERVER, -1, &len);
    if(!fn || !*fn || !data || len < 4) { if(data) { free(data); } return 0; }
    path = (char*)malloc(strlen(fn) + 1);
    if(path) {
        for(i = 0; fn[i]; i++) {
            if(fn[i] == SEP[0]) { path[i] = 0; files_mkdir(path); }
            path[i] = fn[i];
        }
        free(path);
    }
    if(!(f = files_open(fn, "wb"))) { free(data); return 0; }
    fwrite(WORLD_STATEMAGIC, 1, 16, f);
    fwrite(world.hdr.id, 1, 16, f);
    if(prvw && plen > 0) {
        i = plen + 32;
        memset(tmp, 0, sizeof(tmp)); memcpy(tmp, "PRVW", 4);
        *((uint32_t*)(tmp + 4)) = SDL_SwapLE32(i);
        *((uint32_t*)(tmp + 8)) = SDL_SwapLE32(playedsec);
        tmp[31] = 1;
        fwrite(tmp, 1, 32, f);
        fwrite(prvw, 1, plen, f);
    }
    fwrite(data, 1, len, f);
    fclose(f);
    free(data);
#else
    (void)fn; (void)prvw; (void)plen; (void)playedsec;
#endif
    return 1;
}

/**
 * Parse a serialized world state buffer
 */
int world_deserializestate(uint8_t *comp, int size)
{
    uint8_t *buf = NULL, *ptr, *end;
    uint32_t n;
    uint64_t S;
    int i, j, k, l, s, len = 0, m = world.mapsize * world.mapsize;

    if(!comp || size < 8) goto rderr;
    n = crc32(0, comp, size - 4);
    n = SDL_SwapLE32(n);
    if(memcmp(comp + size - 4, &n, 4)) goto rderr;
    buf = (uint8_t*)stbi_zlib_decode_malloc_guesssize_headerflag((const char*)comp, size - 4, 4096, &len, 1);
    if(!buf || len < 1) {
        if(buf) free(buf);
rderr:  if(verbose > 1) printf("%s: uncompression error\r\n", main_me);
        return 0;
    }
    for(i = 0; world.spawners && i < world.numswr; i++)
        world.spawners[i].curr = 0;
    ptr = buf; end = ptr + len;
    while(ptr + 8 < end) {
        /* TODO, parse chunks */
        memcpy(&len, ptr + 4, 4);
        len = SDL_SwapLE32(len);

        if(!memcmp(ptr, "DENY", 4)) {
            if(world.denylist) free(world.denylist);
            world.numdenylist = len / 17;
            world.denylist = (uint8_t*)malloc(world.numdenylist * 17);
            if(world.denylist) {
                memcpy(world.denylist, ptr + 8, world.numdenylist * 17);
            } else
                world.numdenylist = 0;
        } else

        if(!memcmp(ptr, "GLBL", 4)) {
            j = world.numglobals < len / 4 ? world.numglobals : len / 4;
            if(!world.globals) world.globals = (int*)malloc(j * sizeof(int));
            if(world.globals) {
                memset(world.globals, 0, j * sizeof(int));
                for(i = 0; i < j; i++) {
                    memcpy(&s, ptr + 8 + i * 4, sizeof(int));
                    world.globals[i] = SDL_SwapLE32(s);
                }
            }
        } else

        if(!memcmp(ptr, "QSTS", 4)) {
            if(world.quests) {
                for(i = j = 0; i < world.numqst; i++) {
                    world.quests[i].completed = -1;
                    if(world.quests[i].type) {
                        memcpy(&s, ptr + 8 + j * 4, sizeof(int)); j++;
                        world.quests[i].completed = SDL_SwapLE32(s);
                    }
                }
            }
        } else

        if(!memcmp(ptr, "USER", 4)) {
            i = world.numplayers++;
            world.players = (world_player_t*)realloc(world.players, world.numplayers * sizeof(world_player_t));
            if(world.players) {
                memset(&world.players[i], 0, sizeof(world_player_t));
                memcpy(world.players[i].pass, ptr + 8, 32);
                memcpy(&S, ptr + 40, sizeof(int)); world.players[i].created = SDL_SwapLE64(S);
                memcpy(&S, ptr + 48, sizeof(int)); world.players[i].logind = SDL_SwapLE64(S);
                memcpy(&S, ptr + 56, sizeof(int)); world.players[i].logoutd = SDL_SwapLE64(S);
                memcpy(&S, ptr + 64, sizeof(int)); world.players[i].lastd = SDL_SwapLE64(S);
                world.players[i].lang[0] = ptr[72];
                world.players[i].lang[1] = ptr[73];
                world.players[i].banned = ptr[74];
                l = ptr[75];
                j = strlen((char*)ptr + 76) + 1;
                world.players[i].name = (char*)malloc(j);
                if(!world.players[i].name) { world.numplayers--; ptr += len; continue; }
                memcpy(world.players[i].name, ptr + 76, j);
                j += 76;
                if(world.numchar > 0 && l > 0) {
                    world.players[i].opts = (int*)malloc(world.numchar * sizeof(int));
                    if(world.players[i].opts)
                        for(k = 0; k < (l < world.numchar ? l : world.numchar); k++) {
                            memcpy(&s, ptr + j + k * 4, 4);
                            world.players[i].opts[k] = SDL_SwapLE32(s);
                        }
                    j += l * 4;
                }
                memset(world.players[i].equip, 0xff, sizeof(world.players[0].equip));
                for(k = 0; k < 32; k++, j += 2)
                    world.players[i].equip[k] = (ptr[j + 1] << 8) | ptr[j];
                memset(world.players[i].belt, 0xff, sizeof(world.players[0].belt));
                for(k = 0; k < 32; k++, j += 2)
                    world.players[i].belt[k] = (ptr[j + 1] << 8) | ptr[j];
                world_entity_rd(i, ptr + j, &world.players[i].data);
            } else
                world.numplayers = 0;
        } else

        if(!memcmp(ptr, "NPC", 4)) {
            i = world.numnpcs++;
            world.npcs = (world_npc_t*)realloc(world.npcs, world.numnpcs * sizeof(world_npc_t));
            if(world.npcs) {
                memcpy(&s, ptr + 8, sizeof(int)); world.npcs[i].type = SDL_SwapLE32(s);
                memcpy(&s, ptr + 12, sizeof(int)); world.npcs[i].spawner = SDL_SwapLE32(s);
                if(world.npcs[i].spawner >= 0 && world.npcs[i].spawner < world.numswr && world.spawners)
                    world.spawners[world.npcs[i].spawner].curr++;
                world_entity_rd(-1, ptr + 16, &world.npcs[i].data);
            } else
                world.numnpcs = 0;
        } else

        if(!memcmp(ptr, "MAP", 4)) {
            if(world.nummap > 0) {
                memcpy(&s, ptr + 4, sizeof(int)); j = SDL_SwapLE32(s) - 12;
                memcpy(&s, ptr + 8, sizeof(int)); i = SDL_SwapLE32(s);
                if(i >= 0 && i < world.nummap) {
                    if(!world.objlyr) world.objlyr = (uint16_t**)malloc(world.nummap * sizeof(uint16_t*));
                    if(world.objlyr) {
                        if(!world.objlyr[i]) world.objlyr[i] = (uint16_t*)malloc(m * sizeof(uint16_t));
                        if(world.objlyr[i]) {
                            memset(world.objlyr[i], 0xff, m * sizeof(uint16_t));
                            k = m; tng_rle_dec16(ptr + 12, j, world.objlyr[i], &k);
                            for(j = 0; j < k; j++) world.objlyr[i][j] = SDL_SwapLE16(world.objlyr[i][j]);
                        }
                    }
                }
            }
        }

        /* skip over chunk */
        ptr += len;
    }
    free(buf);
    return 1;
}

/**
 * Deserialize world state from saved game file
 */
int world_loadstate(char *fn)
{
#ifndef __EMSCRIPTEN__
    char *data, *ptr;
    int size, n;

    if(!fn || !*fn) return 0;
    data = files_readfile(fn, &size);
    if(!data || size < 48 || memcmp(data, WORLD_STATEMAGIC, 16) || memcmp(data + 16, world.hdr.id, 16)) {
        if(verbose > 1) printf("%s: not a valid world state file\r\n", main_me);
        if(data) free(data);
        return 0;
    }
    /* skip header */
    ptr = data + 32; size -= 32;
    /* skip optional preview chunk */
    if(!memcmp(ptr, "PRVW", 4)) {
        memcpy(&n, ptr + 4, 4);
        n = SDL_SwapLE32(n);
        ptr += n; size -= n;
    }
    world_flush();
    n = world_deserializestate((uint8_t *)ptr, size);
    free(data);
    return n;
#else
    (void)fn;
    return 0;
#endif
}

/**
 * Add a quest to user
 */
void world_quest(world_entity_t *ent, int idx, int complete, int script)
{
    int i;

    if(world.quests && idx >= 0 && idx < world.numqst && (!world.quests[idx].type || world.quests[idx].completed == -1)) {
        for(i = 0; i < ent->numqst && ent->quests[i * 2 + 1] != idx; i++);
        if(i >= ent->numqst) {
            i = ent->numqst++;
            ent->quests = (int*)realloc(ent->quests, ent->numqst * 2 * sizeof(int));
            if(ent->quests) {
                ent->quests[i * 2] = complete;
                ent->quests[i * 2 + 1] = idx;
                alert_init(complete);
            } else
                ent->numqst = 0;
        } else
        if(ent->quests[i * 2] < complete) {
            ent->quests[i * 2] = complete;
            alert_init(complete);
        }
        if(ent->numqst && complete && script) {
            script_load(SCRIPT_CALLER(SCRIPT_QUEST, idx), world.quests[idx].bc, world.quests[idx].bclen,
                world.quests[idx].tng, ent, NULL);
        }
    }
}

/**
 * Add (or take) an item to user
 */
int world_item(int plr, int dqty, int idx)
{
    world_entity_t *ent;
    int i, j;

    if(plr < 0 || plr >= world.numplayers || idx < 0 || idx >= world.numspr[SPR_TILE]) return 0;
    ent = &world.players[plr].data;
    if(world.objects && world.objects[idx] && world.objects[idx]->eqmask == 0xffffffff) {
        if(!ent->skills) ent->numskl = 0;
        for(i = 0; i < ent->numskl && ent->skills[i].obj.idx != idx; i++);
        if(i < ent->numskl) {
            ent->skills[i].qty += dqty;
            if(ent->skills[i].qty > WORLD_QTYMAX) ent->skills[i].qty = WORLD_QTYMAX;
            if(ent->skills[i].qty < 1) {
                for(j = 0; j < 32; j++) {
                    if(world.players[plr].belt[j] == (0x8000 | i)) { client_eqchange = 1; world.players[plr].belt[j] = 0xffff; }
                    if(world.players[plr].belt[j] != 0xffff && world.players[plr].belt[j] > (0x8000 | i)) {
                        client_eqchange = 1; world.players[plr].belt[j]--;
                    }
                }
                memcpy(&ent->skills[i], &ent->skills[i + 1], (ent->numskl - i) * sizeof(world_inv_t));
                ent->numskl--; if(!ent->numskl) { free(ent->skills); ent->skills = NULL; }
            }
        } else if(dqty > 0) {
            i = ent->numskl++;
            ent->skills = (world_inv_t*)realloc(ent->skills, ent->numskl * sizeof(world_inv_t));
            if(!ent->skills) { ent->numskl = 0; return 0; }
            else {
                ent->skills[i].qty = dqty > WORLD_QTYMAX ? WORLD_QTYMAX : dqty;
                ent->skills[i].obj.cat = SPR_TILE;
                ent->skills[i].obj.idx = idx;
            }
        }
    } else {
        if(!ent->inv) ent->numinv = 0;
        for(i = 0; i < ent->numinv && ent->inv[i].obj.idx != idx; i++);
        if(i < ent->numinv) {
            ent->inv[i].qty += dqty;
            if(ent->inv[i].qty > WORLD_QTYMAX) ent->inv[i].qty = WORLD_QTYMAX;
            if(ent->inv[i].qty < 1) {
                for(j = 0; j < 32; j++) {
                    if(world.players[plr].equip[j] == idx) { client_eqchange = 1; world.players[plr].equip[j] = 0xffff; }
                    if(world.players[plr].belt[j] == i) { client_eqchange = 1; world.players[plr].belt[j] = 0xffff; }
                    if(world.players[plr].belt[j] != 0xffff && world.players[plr].belt[j] > i) {
                        client_eqchange = 1; world.players[plr].belt[j]--;
                    }
                }
                memcpy(&ent->inv[i], &ent->inv[i + 1], (ent->numinv - i) * sizeof(world_inv_t));
                ent->numinv--; if(!ent->numinv) { free(ent->inv); ent->inv = NULL; }
            }
        } else if(dqty > 0) {
            i = ent->numinv++;
            ent->inv = (world_inv_t*)realloc(ent->inv, ent->numinv * sizeof(world_inv_t));
            if(!ent->inv) { ent->numinv = 0; return 0; }
            else {
                ent->inv[i].qty = dqty > WORLD_QTYMAX ? WORLD_QTYMAX : dqty;
                ent->inv[i].obj.cat = SPR_TILE;
                ent->inv[i].obj.idx = idx;
            }
        }
    }
    return 1;
}
