/*
 * tngp/script.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 Scripting support
 *
 */

#include "tngp.h"
#include <pthread.h>    /* do not use SDL_Thread here, this code is shared with tngs */
int nanosleep(const struct timespec *rqtp, struct timespec *rmtp);

#if TNG_BC_FUNC_MAX != 43
#error "Number of script functions mismatch, this code only handles 43"
#endif

typedef struct {
    /* no need to reference world.globals here */
    world_entity_t *this;       /* this object's / player's / NPC's attributes */
    world_entity_t *oppo;       /* opponent's attributes */
    uint8_t *bc;                /* script bytecode */
    int bclen;                  /* bytecode length */
    int tng;                    /* extension reference */
    int sp;                     /* stack pointer */
    int pc;                     /* program counter */
    int regs[26];               /* registers (local variables) */
    int stack[SCRIPT_STACKSIZE];/* stack */
    int caller;                 /* caller reference */
    int actor;                  /* current actor */
    int obj;                    /* current object */
    int await;                  /* script is waiting (for dialog or for npc) */
    uint32_t next;              /* when to run script next */
} script_t;

script_t callstack[SCRIPT_CALLSIZE];
int numcallstack = 0, badtrace = 0;

typedef struct {
    script_t script;
    uint32_t next, repeat;
    pthread_t th;
} cron_t;
cron_t **crons = NULL;
int numcrons = 0;

#if DEBUG
int script_trace = 0;

/**
 * Disassemble bytecode
 */
const char *cmd_types[] = { "", "", "", "", "", "exit", "delay", "music", "sound", "speak", "chooser", "cutscene", "dialog",
    "location", "quest", "canim", "oanim", "npc", "actor", "behave", "market", "south", "southwest", "west", "northwest",
    "north", "northeast", "east", "southeast", "remove", "drop", "delete", "add", "replace", "give", "take", "event",
    "transport", "scene", "waitnpc", "craft", "match", "alert", "altitude"
};

void script_disasm(int pc, int len, char *sts, int stslen, uint8_t *ptr)
{
    int i, m, n = 0;
    char *str;
    uint32_t c;
    uint8_t t;

    printf("%s: -- trace -- %06X: ", main_me, pc);
    if(pc >= len) { printf("***out of bounds ***\r\n"); return; }
    t = ptr[0];
    if(t == TNG_BC_SWITCH) n = ptr[1];
    switch(t) {
        case TNG_BC_FNC0: case TNG_BC_FNC1: case TNG_BC_FNC2: case TNG_BC_FNC3:
        case TNG_BC_MIN: case TNG_BC_MAX: case TNG_BC_POP: case TNG_BC_PUSH: case TNG_BC_PUSH8: i = 2; break;
        case TNG_BC_CNT1: case TNG_BC_CNTO1: case TNG_BC_PUSH16: i = 3; break;
        case TNG_BC_JMP: case TNG_BC_JZ: case TNG_BC_SUM: case TNG_BC_SUMO: case TNG_BC_POPA: case TNG_BC_POPO:
        case TNG_BC_PUSHA: case TNG_BC_PUSHO: case TNG_BC_PUSH24: case TNG_BC_PUSHSPR: case TNG_BC_PUSHMUS:
        case TNG_BC_PUSHSND: case TNG_BC_PUSHSPC: case TNG_BC_PUSHCHR: case TNG_BC_PUSHCUT: case TNG_BC_PUSHDLG:
        case TNG_BC_PUSHCFT: case TNG_BC_PUSHQST: case TNG_BC_PUSHTXT: i = 4; break;
        case TNG_BC_SWITCH: case TNG_BC_RND: case TNG_BC_PUSH32: i = 5; break;
        case TNG_BC_PUSHMAP:case TNG_BC_PUSHMAP|0x80: i = 8; break;
        default: i = 1; break;
    }
    for(m = 0; m < 8; m++)
        printf(m < i ? "%02X " : (t == TNG_BC_SWITCH && m == i ? "..." : "   "), m < (len - pc > 8 ? 8 : len - pc) ? ptr[m] : 0);
    printf("  ");
    ptr++; c = 0;
    switch(len - pc - 1) {
        case 0: break;
        case 1: c = ptr[0]; break;
        case 2: c = (ptr[1] << 8) | ptr[0]; break;
        default: c = (ptr[2] << 16) | (ptr[1] << 8) | ptr[0]; break;
    }
    str = sts && c < (uint32_t)stslen ? sts + c : "NULL";
    switch(t) {
        case TNG_BC_END:    printf("END"); break;
        case TNG_BC_SWITCH: printf("SWITCH "); ptr++; for(i = 0; i < n; i++, ptr += 3) printf(" %02X%02X%02X", ptr[2], ptr[1], ptr[0]); break;
        case TNG_BC_JMP:    printf("JMP     %02X%02X%02X", ptr[2], ptr[1], ptr[0]); break;
        case TNG_BC_JZ:     printf("JZ      %02X%02X%02X", ptr[2], ptr[1], ptr[0]); break;
        case TNG_BC_FNC0:
        case TNG_BC_FNC1:
        case TNG_BC_FNC2:
        case TNG_BC_FNC3:   printf("CALL    %s(%d)", ptr[0] < 5 || ptr[0] > TNG_BC_FUNC_MAX ? "???" : cmd_types[ptr[0]], t - TNG_BC_FNC0); break;
        case TNG_BC_CNT0:   printf("CNT0"); break;
        case TNG_BC_CNT1:   printf("CNT1    1.%02X%02X", ptr[1], ptr[0]); break;
        case TNG_BC_SUM:    printf("SUM     %s", str); break;
        case TNG_BC_SUM|0x80:printf("SUM     %u %s", c, c < (uint32_t)world.numattrs ? world.attrs[c].name : "???"); break;
        case TNG_BC_CNTO0:  printf("CNTO0"); break;
        case TNG_BC_CNTO1:  printf("CNTO1   1.%02X%02X", ptr[1], ptr[0]); break;
        case TNG_BC_SUMO:   printf("SUMO    %s", str); break;
        case TNG_BC_SUMO|0x80:printf("SUMO    %u %s", c, c < (uint32_t)world.numattrs ? world.attrs[c].name : "???"); break;
        case TNG_BC_RND:    printf("RND     %u", *((uint32_t*)ptr)); break;
        case TNG_BC_MIN:    printf("MIN     (%u)", ptr[0]); break;
        case TNG_BC_MAX:    printf("MAX     (%u)", ptr[0]); break;
        case TNG_BC_ADD:    printf("ADD     (2)"); break;
        case TNG_BC_SUB:    printf("SUB     (2)"); break;
        case TNG_BC_MUL:    printf("MUL     (2)"); break;
        case TNG_BC_DIV:    printf("DIV     (2)"); break;
        case TNG_BC_MOD:    printf("MOD     (2)"); break;
        case TNG_BC_EQ:     printf("EQ      (2)"); break;
        case TNG_BC_NE:     printf("NE      (2)"); break;
        case TNG_BC_GE:     printf("GE      (2)"); break;
        case TNG_BC_GT:     printf("GT      (2)"); break;
        case TNG_BC_LE:     printf("LE      (2)"); break;
        case TNG_BC_LT:     printf("LT      (2)"); break;
        case TNG_BC_NOT:    printf("NOT     (1)"); break;
        case TNG_BC_OR:     printf("OR      (2)"); break;
        case TNG_BC_AND:    printf("AND     (2)"); break;
        case TNG_BC_POP:    printf("POP     %c", ptr[0] < 26 ? ptr[0] + 'a' : '?'); break;
        case TNG_BC_POPA:   printf("POPA    %s", str); break;
        case TNG_BC_POPA|0x80:printf("POPA    %u %s", c, c < (uint32_t)world.numattrs ? world.attrs[c].name : "???"); break;
        case TNG_BC_POPO:   printf("POPO    %s", str); break;
        case TNG_BC_POPO|0x80:printf("POPO    %u %s", c, c < (uint32_t)world.numattrs ? world.attrs[c].name : "???"); break;
        case TNG_BC_PUSH:   printf("PUSH    %c", ptr[0] < 26 ? ptr[0] + 'a' : '?'); break;
        case TNG_BC_PUSHA:  printf("PUSHA   %s", str); break;
        case TNG_BC_PUSHA|0x80:printf("PUSHA   %u %s", c, c < (uint32_t)world.numattrs ? world.attrs[c].name : "???"); break;
        case TNG_BC_PUSHO:  printf("PUSHO   %s", str); break;
        case TNG_BC_PUSHO|0x80:printf("PUSHO   %u %s", c, c < (uint32_t)world.numattrs ? world.attrs[c].name : "???"); break;
        case TNG_BC_PUSH8:  printf("PUSH8   %u", ptr[0]); break;
        case TNG_BC_PUSH16: printf("PUSH16  %u", *((uint16_t*)ptr)); break;
        case TNG_BC_PUSH24: printf("PUSH24  %u", c); break;
        case TNG_BC_PUSH32: printf("PUSH32  %u", *((uint32_t*)ptr)); break;
        case TNG_BC_PUSHMAP:printf("PUSHMAP %s,%u,%u", str, (ptr[4] << 8) | ptr[3], (ptr[6] << 8) | ptr[5]); break;
        case TNG_BC_PUSHMAP|0x80:printf("PUSHMAP %u %s,%u,%u", c, c < (uint32_t)world.nummap ? world.map[c].name : "???",
            (ptr[4] << 8) | ptr[3], (ptr[6] << 8) | ptr[5]); break;
        case TNG_BC_PUSHSPR:printf("PUSHSPR %u.%02X%02X", ptr[0], ptr[2], ptr[1]); break;
        case TNG_BC_PUSHMUS:printf("PUSHMUS %s", str); break;
        case TNG_BC_PUSHSND:printf("PUSHSND %s", str); break;
        case TNG_BC_PUSHSPC:printf("PUSHSPC %s", str); break;
        case TNG_BC_PUSHCHR:printf("PUSHCHR %s", str); break;
        case TNG_BC_PUSHCUT:printf("PUSHCUT %s", str); break;
        case TNG_BC_PUSHDLG:printf("PUSHDLG %s", str); break;
        case TNG_BC_PUSHCFT:printf("PUSHCFT %s", str); break;
        case TNG_BC_PUSHQST:printf("PUSHQST %s", str); break;
        case TNG_BC_PUSHTXT:printf("PUSHTXT %u \"%s\"", c, world.text && c < (uint32_t)world.numtext ? world.text[c] : "???"); break;
        default: printf("???"); break;
    }
    printf("\r\n");
}
#endif

/**
 * Decode caller
 */
static const char *evthandlers[] = { "onclick", "ontouch", "onleave", "onapproach", "onaction1", "onaction2", "onaction3",
    "onaction4", "onaction5", "onaction6", "onaction7", "onaction8", "onaction9", "onusing1", "onusing2", "onusing3",
    "ontimer", "onsell", "onbuy" };
void script_caller(int caller)
{
    int par;
    /* decode caller into a human readable string.
     * This is problematic, but absolute necessary to provide this info for game creators. Devs who create
     * apps with error messages like "Somewhere bad happened with something" should and will rotten in Hell. */
    par = caller >> 8; if(par < 0) par = 2147483647;
    switch(caller & 7) {
        case SCRIPT_CUTSCN:
            if(par < world.numcut && world.cutscns && world.cutscns[par].name) printf("cutscene %s", world.cutscns[par].name);
            else { printf("cutscene #%d", par); } break;
        case SCRIPT_STARTUP: par = world.cron[par].value; if(par) { printf("timer%d", par); } else { printf("start"); } break;
        case SCRIPT_ATTR:
            if(par < world.numattrs && world.attrs && world.attrs[par].name)
                printf("attribute %s onchange", world.attrs[par].name);
            else { printf("attribute #%d onchange", par); } break;
        case SCRIPT_DEFACT: printf("default action #%d", par); break;
        case SCRIPT_NPC:
            if(par < world.numnpc && world.npctypes && world.npctypes[par].name) printf("NPC %s", world.npctypes[par].name);
            else { printf("NPC #%d", par); } break;
            goto func;
        case SCRIPT_OBJ:
            printf("object #%04X", par);
            goto func;
        case SCRIPT_OBJCALL:
            printf("event() object #%04X", par);
func:       par = (caller >> 3) & 31;
            if(par >= 0 && par < (int)(sizeof(evthandlers)/sizeof(evthandlers[0]))) printf(" %s", evthandlers[par]);
            else printf(" event%d", par);
        break;
        case SCRIPT_QUEST:
            if(par < world.numqst && world.quests && world.quests[par].name)
                printf("quest %s oncomplete", world.quests[par].name);
            else { printf("quest #%d oncomplete", par); } break;
    }
}

/**
 * Report error
 */
void script_report(script_t *script, char *fmt, ...)
{
    char tmp[256];
    int i;
    __builtin_va_list args;
    if(verbose && !badtrace) {
        __builtin_va_start(args, fmt);
        vsprintf(tmp, fmt, args);
        __builtin_va_end(args);
        printf("%s: script error, %s. Callstack:\r\n", main_me, tmp);
        if(script) {
            printf("%s:    pc %06X in ", main_me, script->pc);
            script_caller(script->caller);
            printf("\r\n");
        }
        if(numcallstack < 1) { printf("  unknown???\r\n"); badtrace = 1; }
        else {
            for(i = numcallstack - 1; i >= 0; i--)
                if(script != &callstack[i]) {
                    printf("%s:    pc %06X in ", main_me, callstack[i].pc);
                    script_caller(callstack[i].caller);
                    printf("\r\n");
                }
        }
    }
}

/**
 * Push value onto stack
 */
void script_push(script_t *script, int value)
{
    if(!script || script->sp >= SCRIPT_STACKSIZE) {
#if DEBUG
        if(script_trace > 0) script_report(script, "stack overflow (attempt)");
#endif
        return;
    }
    script->stack[script->sp++] = value;
}

/**
 * Pop value from stack
 */
int script_pop(script_t *script)
{
    if(!script || !script->sp) {
#if DEBUG
        if(script_trace > 0) script_report(script, "stack underflow (attempt)");
#endif
        return 0;
    }
    return script->stack[--script->sp];
}

/**
 * Return the top element of the stack
 */
int script_top()
{
    if(callstack[numcallstack].sp < 1) {
#if DEBUG
        if(script_trace > 0) script_report(&callstack[numcallstack], "stack underflow (attempt)");
#endif
        return 0;
    }
    return callstack[numcallstack].stack[callstack[numcallstack].sp - 1];
}

/**
 * Set attribute
 */
void script_setattr(world_entity_t *data, int attr, int value)
{
    int i, j;

    if(attr >= 0 && attr < world.numattrs && world.attrs) {
        if(value < -WORLD_ATTRMAX) value = -WORLD_ATTRMAX;
        if(value > WORLD_ATTRMAX) value = WORLD_ATTRMAX;
        switch(world.attrs[attr].type) {
            case ATTR_PRI: case ATTR_CHRVAR:
                if(!data || !data->attrs) { script_report(NULL, "no this reference"); }
                else if(data->attrs[world.attrs[attr].idx] != value) {
                    data->attrs[world.attrs[attr].idx] = value;
                    /* refresh calculated character attributes */
                    for(i = 0; i < world.numattrs; i++)
                        if(i != attr && 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, data, NULL);
                            data->attrs[world.attrs[i].idx] = j > SCRIPT_AGAIN ?
                                (j < -WORLD_ATTRMAX ? -WORLD_ATTRMAX : (j > WORLD_ATTRMAX ? WORLD_ATTRMAX : j)) : 0;
                        }
                    /* call onchange handler */
                    if(world.attrs[attr].bc)
                        script_load(SCRIPT_CALLER(SCRIPT_ATTR, attr), world.attrs[attr].bc, world.attrs[attr].bclen,
                            world.attrs[attr].tng, data, NULL);
                }
            break;
            case ATTR_GLOBVAR:
                if(!world.globals) return;
                if(world.globals[world.attrs[attr].idx] != value) {
                    world.globals[world.attrs[attr].idx] = value;
                    /* refresh calculated global attributes */
                    for(i = 0; i < world.numattrs; i++)
                        if(i != attr && 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, data);
                            world.globals[world.attrs[i].idx] = j > SCRIPT_AGAIN ?
                                (j < -WORLD_ATTRMAX ? -WORLD_ATTRMAX : (j > WORLD_ATTRMAX ? WORLD_ATTRMAX : j)) : 0;
                        }
                    /* call onchange handler */
                    if(world.attrs[attr].bc)
                        script_load(SCRIPT_CALLER(SCRIPT_ATTR, attr), world.attrs[attr].bc, world.attrs[attr].bclen,
                            world.attrs[attr].tng, NULL, data);
                }
            break;
            default: script_report(NULL, "trying to set calculated attribute"); break;
        }
    } else
        script_report(NULL, "trying to set invalid attribute");
}

/**
 * Get attribute
 */
int script_getattr(world_entity_t *data, int attr)
{
    if(attr >= 0 && attr < world.numattrs && world.attrs) {
        switch(world.attrs[attr].type) {
            case ATTR_PRI: case ATTR_CHRVAR: case ATTR_CHRCALC:
                if(!data || !data->attrs) { script_report(NULL, "no this reference"); return 0; }
                return data->attrs[world.attrs[attr].idx];
            break;
            case ATTR_GLOBVAR: case ATTR_GLOBCALC:
                if(!world.globals) return 0;
                return world.globals[world.attrs[attr].idx];
            break;
            default: break;
        }
    }
    script_report(NULL, "invalid attribute reference");
    return 0;
}

/**
 * Execute a script
 * DO NOT call it directly, only via script_load() or cron
 */
int script_exec(script_t *script)
{
    world_entity_t *ent;
    world_entity_t *this;
    world_entity_t *oppo;
    uint8_t *pc;
    char *str, *sts = world.tng[script->tng].sts;
    int i, j, k, l, arg, sp, map_id = 0, map_x = 0, map_y = 0, numjmps = 0, stslen = world.tng[script->tng].stslen;

    if(script->await == 2 /*&& numnpcanim < 1*/) script->await = 0;
    if(script->next > main_tick || script->await) return SCRIPT_AGAIN;
#if DEBUG
    if(script_trace > 0) { printf("%s: -- trace -- script_exec(", main_me); script_caller(script->caller); printf(")\r\n"); }
#endif
    this = script->this; oppo = script->oppo;
    while(script->pc < script->bclen) {
#if DEBUG
        if(script_trace > 1) {
            if(script_trace > 2) {
                printf("%s: -- trace -- regs sp: %u", main_me, script->sp);
                for(i = 0; i < 26; i++) printf(" %c: %u", 'a' + i, script->regs[i]);
                printf("\r\n");
                if(script->sp > 0) {
                    printf("%s: -- trace -- stack:", main_me);
                    for(i = 0; i < script->sp; i++) printf(" %d", script->stack[i]);
                    printf("\r\n");
                }
            }
            script_disasm(script->pc, script->bclen, sts, stslen, script->bc + script->pc);
        }
#endif
        pc = script->bc + script->pc++;
        /* this is a self-optimizing virtual machine. Whenever it encounters an opcode which is interpreted in
         * O(n) time, then it replaces that opcode with another that's only O(1), so next time it'll be faster */
        switch(*pc) {
            case TNG_BC_END: script->pc = script->bclen; goto end;
            case TNG_BC_SWITCH:
                j = pc[1]; arg = script_pop(script);
                if(arg >= 0 && arg < j) {
                    arg = (j - 1 - arg) * 3 + 2; script->pc = pc[arg] | (pc[arg + 1] << 8) | (pc[arg + 2] << 16);
                } else
                    script->pc += 1 + j * 3;
            break;
            case TNG_BC_JMP:
                script->pc = pc[1] | (pc[2] << 8) | (pc[3] << 16);
                numjmps++; if(numjmps > 99) return SCRIPT_AGAIN;
            break;
            case TNG_BC_JZ:
                if(script_pop(script)) { script->pc += 3; } else { script->pc = pc[1] | (pc[2] << 8) | (pc[3] << 16); }
            break;
            case TNG_BC_FNC0: case TNG_BC_FNC1: case TNG_BC_FNC2: case TNG_BC_FNC3:
                arg = pc[0] - TNG_BC_FNC0;
                if(arg > script->sp) {
                    script_report(script, "not enough arguments on stack (need %d)", arg);
                    script->sp = 0;
                } else {
                    sp = script->sp - arg;
                    /* function calls */
                    switch(pc[1]) {
                        case  5: /* exit */
                            numcallstack = 0;
                            return SCRIPT_EXIT;
                        break;
                        case  6: /* delay */
                            script->next = main_tick + (uint32_t)script_pop(script) * 10;
                            script->pc++;
                            return SCRIPT_AGAIN;
                        break;
                        case  7: /* music */
                            arg = script_pop(script);
                            if(arg >= 0 && arg < world.nummusic)
                                media_playmus(NULL, &world.music[arg]);
                            else
                                script_report(script, "bad music reference %u", arg);
                        break;
                        case  8: /* sound */
                            arg = script_pop(script);
                            if(arg >= 0 && arg < world.numsound) {
                                if(this && world.players && world.numplayers > 0) {
                                    map_dist(cutscn_idx != -1 ? cutscn_scene.maps : game_scene.maps,
                                        &world.players[0].data, this, &i, &j);
                                } else
                                    i = j = 0;
                                media_playsnd(arg, i, -j);
                            } else
                                script_report(script, "bad sound reference %u", arg);
                        break;
                        case  9: /* speak */
                            arg = script_pop(script);
                            if(arg >= 0 && arg < world.numspc) {
                                if(this && world.players && world.numplayers > 0) {
                                    map_dist(cutscn_idx != -1 ? cutscn_scene.maps : game_scene.maps,
                                        &world.players[0].data, this, &i, &j);
                                } else
                                    i = j = 0;
                                media_playspc(arg, i, -j);
                            } else
                                script_report(script, "bad speech reference %u", arg);
                        break;
                        case 10: /* chooser */
                            arg = script_pop(script);
                            if(arg >= 0 && arg < world.numchr) {
                                chooser_init(arg, &script->await, script->regs);
                                script->pc++;
                                return SCRIPT_AGAIN;
                            } else
                                script_report(script, "bad chooser reference %u", arg);
                        break;
                        case 11: /* cutscn */
                            arg = script_pop(script);
                            if(arg >= 0 && arg < world.numcut) {
                                j = -1; script->pc++; pc = script->bc + script->pc;
                                /* tricky, a cutscene followed by a chooser has to be displayed together, during cutscene loop */
                                if(script->pc + 6 < script->bclen && pc[4] == TNG_BC_FNC1 && pc[5] == 10) {
                                    k = (pc[3] << 16) | (pc[2] << 8) | pc[1];
                                    if(pc[0] == TNG_BC_PUSH24) { j = k; }
                                    else if(!sts || k >= stslen) { script_report(script, "no string table reference"); } else {
                                        str = sts + k;
                                        if(world.choosers) {
                                            for(i = 0; i < world.numchr && (!world.choosers[i].name || strcmp(str, world.choosers[i].name)); i++);
                                            if(i < world.numchr) { j = i; } else { script_report(script, "no such chooser '%s'", str); }
                                        }
                                        pc[0] = TNG_BC_PUSH24; pc[1] = j & 0xff; pc[2] = (j >> 8) & 0xff; pc[3] = (j >> 16) & 0xff;
                                    }
                                    script->pc += 6;
                                    if(j >= 0 && j < world.numchr)
                                        chooser_init(j, NULL, script->regs);
                                    else
                                        script_report(script, "bad chooser reference %u", j);
                                }
                                cutscn_init(arg, &script->await);
                                return SCRIPT_AGAIN;
                            } else
                                script_report(script, "bad cutscene reference %u", arg);
                        break;
                        case 12: /* dialog */
                            j = script_pop(script);
                            arg = script_pop(script);
                            if(arg >= 0 && arg < world.numdlg) {
                                dialog_stop = main_tick + j * 10;
                                dialog_init(arg, &script->await, script->regs);
                                script->pc++;
                                return SCRIPT_AGAIN;
                            } else
                                script_report(script, "bad dialog reference %u", arg);
                        break;
                        case 13: /* location */
                            j = script_pop(script);     /* map reference isn't actually stored on stack, wouldn't fit */
                            arg = script_pop(script);   /* get direction */
                            if(!this) { script_report(script, "no this reference"); }
                            else if(map_id < 0 || map_id >= world.nummap || map_x < 0 || map_x >= world.mapsize ||
                              map_y < 0 || map_y >= world.mapsize)
                                script_report(script, "invalid map reference");
                            else {
                                this->d = arg > MAP_S && arg <= MAP_SE ? arg : MAP_S;
                                map_tile2map(map_x, map_y, &this->x, &this->y);
                                if(this->map != map_id || !game_scene.maps[MAP_MAIN]) {
                                    this->map = map_id;
                                    game_loadmap(map_id, this->x, this->y);
                                }
                            }
                        break;
                        case 14: /* quest */
                            j = script_pop(script);
                            arg = script_pop(script);
                            ent = oppo ? oppo : this;
                            if(!ent) { script_report(script, "no opponent nor this reference"); }
                            else main_addquest(ent, arg, j);
                        break;
                        case 15: /* canim */
                        break;
                        case 16: /* oanim */
                        break;
                        case 17: /* npc */
                            j = script_pop(script);     /* map reference isn't actually stored on stack, wouldn't fit */
                            arg = script_pop(script);   /* get npc */
                            if((arg & 0xff) != SPR_NPC) script_report(script, "not an NPC reference");
                            else if(map_id < 0 || map_id >= world.nummap || map_x < 0 || map_x >= world.mapsize ||
                              map_y < 0 || map_y >= world.mapsize)
                                script_report(script, "invalid map reference");
                            else {
                                map_tile2map(map_x, map_y, &k, &l);
                                script->actor = cutscn_placenpc(arg >> 8, k, l, MAP_S);
                            }
                        break;
                        case 18: /* actor */
                            script->actor = script_pop(script);
                        break;
                        case 19: /* behave */
                            arg = script_pop(script);
                            if(!this) { script_report(script, "no this reference"); }
                            else if(arg >= 0 && arg < 4) this->behave = arg;
                            else { script_report(script, "invalid behaviour %u", arg); }
                        break;
                        case 20: /* market */
                            if(world.players && world.numplayers > 0) {
                                ent = this == &world.players[0].data ? oppo : this;
                                if(!ent) { script_report(script, "no opponent or this reference"); }
                                else {
                                    /* lookup spawned npc */
                                    for(i = 0; i < game_scene.numnpc && &game_scene.npc[i].data == ent; i++);
                                    if(i >= game_scene.numnpc) script_report(script, "no NPC found");
                                    else hud_market(i);
                                }
                            }
                        break;
                        case 21: /* south */
                        break;
                        case 22: /* southwest */
                        break;
                        case 23: /* west */
                        break;
                        case 24: /* northwest */
                        break;
                        case 25: /* north */
                        break;
                        case 26: /* northeast */
                        break;
                        case 27: /* east */
                        break;
                        case 28: /* southwest */
                        break;
                        case 29: /* remove */
                        break;
                        case 30: /* drop */
                        break;
                        case 31: /* delete */
                        break;
                        case 32: /* add */
                        break;
                        case 33: /* replace */
                        break;
                        case 34: /* give */
                            arg = script_pop(script);
                            k = script_pop(script);
                            j = arg >> 8;
                            if((arg & 0xff) != SPR_TILE || j < 0 || j >= world.numspr[SPR_TILE] || !world.objects ||
                              !world.objects[j]) script_report(script, "not an object reference");
                            else if(world.players && world.numplayers > 0)
                                world_item(0, k, j);
                        break;
                        case 35: /* take */
                            arg = script_pop(script);
                            k = script_pop(script);
                            j = arg >> 8;
                            if((arg & 0xff) != SPR_TILE || j < 0 || j >= world.numspr[SPR_TILE] || !world.objects ||
                              !world.objects[j]) script_report(script, "not an object reference");
                            else if(world.players && world.numplayers > 0)
                                world_item(0, -k, j);
                        break;
                        case 36: /* event */
                            arg = script_pop(script);
                            j = arg >> 8; script->regs[0] = 0;
                            if((arg & 0xff) != SPR_TILE || j < 0 || j >= world.numspr[SPR_TILE] || !world.objects ||
                              !world.objects[j] || world.objects[j]->numevt < 1 || !world.objects[j]->evt)
                                script_report(script, "not a valid object reference");
                            else
                                for(k = 0; k < world.objects[j]->numevt; k++)
                                    if(world.objects[j]->evt[k].type == EVT_ONCLICK && world.objects[j]->evt[k].bc &&
                                      world.objects[j]->evt[k].bclen > 0) {
                                        if(script_load(SCRIPT_CALLERFUNC(SCRIPT_OBJCALL, j, EVT_ONCLICK),
                                          world.objects[j]->evt[k].bc, world.objects[j]->evt[k].bclen,
                                          world.objects[j]->evt[k].tng, this, oppo) > SCRIPT_AGAIN)
                                            script->regs[0] = callstack[numcallstack].regs[0];
                                        break;
                                    }
                        break;
                        case 37: /* transport */
                            arg = script_pop(script);
                            if(!this) { script_report(script, "no this reference"); }
                            else if(arg >= 0 && arg < 6) this->transport = arg;
                            else { script_report(script, "invalid transport %u", arg); }
                        break;
                        case 38: /* scene */
                            j = script_pop(script);     /* map reference isn't actually stored on stack, wouldn't fit */
                            arg = script_pop(script);   /* get daytime */
                            if(map_id < 0 || map_id >= world.nummap || map_x < 0 || map_x >= world.mapsize ||
                              map_y < 0 || map_y >= world.mapsize)
                                script_report(script, "invalid map reference");
                            else {
                                map_tile2map(map_x, map_y, &map_x, &map_y);
                                if(!cutscn_loadmap(map_id, map_x, map_y, arg)) { script_report(script, "no cutscene map"); }
                            }
                        break;
                        case 39: /* waitnpc */
                            script->await = 2;
                            script->pc++;
                            return SCRIPT_AGAIN;
                        break;
                        case 40: /* craft */
                            arg = script_pop(script);
                            if(arg >= 0 && arg < world.numcft)
                                hud_craft(arg);
                            else
                                script_report(script, "bad crafting dialog reference %u", arg);
                        break;
                        case 41: /* match */
                            j = script_pop(script);     /* map reference isn't actually stored on stack, wouldn't fit */
                            arg = script_pop(script);   /* get object sprite */
                            j = arg; arg >>= 8; script->regs[0] = 0;
                            if((j & 0xff) != SPR_TILE || arg < 0 || arg >= world.numspr[SPR_TILE])
                                script_report(script, "not an object reference");
                            else if(map_id < 0 || map_id >= world.nummap || map_x < 0 || map_x >= world.mapsize ||
                              map_y < 0 || map_y >= world.mapsize)
                                script_report(script, "invalid map reference");
                            else {
                                k = map_y * world.mapsize + map_x;
                                /* if we have a modified object layer for this map, use that */
                                if(world.objlyr && world.objlyr[map_id][k] == arg)
                                    script->regs[0] = 1;
                                else {
                                    /* otherwise check on neightbouring maps too */
                                    k += MAPS_LYR_OBJECTS * world.mapsize * world.mapsize;
                                    for(j = 0; j < 9; j++)
                                        if(game_scene.maps[j] && game_scene.maps[j]->id == map_id &&
                                          game_scene.maps[j]->tiles[k] == arg) {
                                            script->regs[0] = 1; break;
                                        }
                                }
                            }
                        break;
                        case 42: /* alert */
                            arg = script_pop(script);
                            if(arg >= 2 && arg < world.numalr)
                                alert_init(arg);
                            else
                                script_report(script, "bad alert reference %u", arg);
                        break;
                        case 43: /* altitude */
                            arg = script_pop(script);
                            if(!this) { script_report(script, "no this reference"); }
                            else if(arg >= -128 && arg < 128) this->altitude = arg;
                            else { script_report(script, "invalid altitude %d", arg); }
                        break;
                        default: script_report(script, "invalid function #%u", pc[1]); break;
                    }
                    script->sp = sp;
                }
                script->pc++;
            break;
            case TNG_BC_CNT0: if(!this) { script_report(script, "no this reference"); } else { script_push(script, this->numinv); } break;
            case TNG_BC_CNT1:
                if(!this) { script_report(script, "no this reference"); script_push(script, 0); }
                else {
                    arg = pc[1] | (pc[2] << 8);
                    for(i = j = 0; this->inv && i < this->numinv; i++)
                        if(this->inv[i].obj.idx == arg) j += this->inv[i].qty;
                    script_push(script, j);
                }
                script->pc += 2;
            break;
            case TNG_BC_SUM:
                k = (pc[1] | (pc[2] << 8) | (pc[3] << 16));
                if(!this) { script_report(script, "no this reference"); script_push(script, 0); } else
                if(!sts || k >= stslen) { script_report(script, "no string table reference"); script_push(script, 0); } else {
                    /* look up in string table and replace with optimized opcode */
                    str = sts + k; arg = 0;
                    if(world.attrs) {
                        for(i = 0; i < world.numattrs && (!world.attrs[i].name || strcmp(str, world.attrs[i].name)); i++);
                        if(i < world.numattrs) { arg = world.attrs[i].idx; } else { script_report(script, "no such attribute '%s'", str); }
                    }
                    pc[0] |= 0x80;
                    pc[1] = arg & 0xff;
                    pc[2] = (arg >> 8) & 0xff;
                    pc[3] = (arg >> 16) & 0xff;
                    goto bc_sum;
                }
                script->pc += 3;
            break;
            case 0x80 | TNG_BC_SUM:
                if(!this) { script_report(script, "no this reference"); script_push(script, 0); }
                else {
                    arg = pc[1] | (pc[2] << 8) | (pc[3] << 16);
bc_sum:             for(i = j = 0; this->inv && i < this->numinv; i++) {
                        /* FIXME: use object's entity, not player's */
                        /* j += obj[this->inv[i].obj.idx].attrs[arg]; */
                        j += this->attrs[arg];
                    }
                    script_push(script, j);
                }
                script->pc += 3;
            break;
            case TNG_BC_CNTO0: if(!oppo) { script_report(script, "no opponent reference"); } else { script_push(script, oppo->numinv); } break;
            case TNG_BC_CNTO1:
                if(!oppo) { script_report(script, "no opponent reference"); script_push(script, 0); }
                else {
                    arg = pc[1] | (pc[2] << 8);
                    for(i = j = 0; oppo->inv && i < oppo->numinv; i++)
                        if(oppo->inv[i].obj.idx == arg) j += oppo->inv[i].qty;
                    script_push(script, j);
                }
                script->pc += 2;
            break;
            case TNG_BC_SUMO:
                k = (pc[1] | (pc[2] << 8) | (pc[3] << 16));
                if(!oppo) { script_report(script, "no opponent reference"); script_push(script, 0); } else
                if(!sts || k >= stslen) { script_report(script, "no string table reference"); script_push(script, 0); } else {
                    /* look up in string table and replace with optimized opcode */
                    str = sts + k; arg = 0;
                    if(world.attrs) {
                        for(i = 0; i < world.numattrs && (!world.attrs[i].name || strcmp(str, world.attrs[i].name)); i++);
                        if(i < world.numattrs) { arg = world.attrs[i].idx; } else { script_report(script, "no such attribute '%s'", str); }
                    }
                    pc[0] |= 0x80;
                    pc[1] = arg & 0xff;
                    pc[2] = (arg >> 8) & 0xff;
                    pc[3] = (arg >> 16) & 0xff;
                    goto bc_sumo;
                }
                script->pc += 3;
            break;
            case 0x80 | TNG_BC_SUMO:
                if(!oppo) { script_report(script, "no opponent reference"); script_push(script, 0); }
                else {
                    arg = pc[1] | (pc[2] << 8) | (pc[3] << 16);
bc_sumo:            for(i = j = 0; oppo->inv && i < oppo->numinv; i++) {
                        /* FIXME: use object's entity, not player's */
                        /* j += obj[oppo->inv[i].obj.idx].attrs[arg]; */
                        j += oppo->attrs[arg];
                    }
                    script_push(script, j);
                }
                script->pc += 3;
            break;
            case TNG_BC_RND:
                arg = pc[1] | (pc[2] << 8) | (pc[3] << 16) | (pc[4] << 24);
                script_push(script, rand() % arg);
                script->pc += 4;
            break;
            case TNG_BC_MIN:
                arg = pc[1];
                if(arg > script->sp) { script_report(script, "not enough arguments on stack (need %d)", arg); arg = pc[1] = script->sp; }
                for(i = 1, j = script->stack[script->sp - 1]; i < arg; i++)
                    if(script->stack[script->sp - 1 - i] < j) j = script->stack[script->sp - 1 - i];
                script_push(script, j);
                script->pc++;
            break;
            case TNG_BC_MAX:
                arg = pc[1];
                if(arg > script->sp) { script_report(script, "not enough arguments on stack (need %d)", arg); arg = pc[1] = script->sp; }
                for(i = 1, j = script->stack[script->sp - 1]; i < arg; i++)
                    if(script->stack[script->sp - 1 - i] > j) j = script->stack[script->sp - 1 - i];
                script_push(script, j);
                script->pc++;
            break;
            case TNG_BC_ADD: script->sp--; script->stack[script->sp - 1] += script->stack[script->sp]; break;
            case TNG_BC_SUB: script->sp--; script->stack[script->sp - 1] -= script->stack[script->sp]; break;
            case TNG_BC_MUL: script->sp--; script->stack[script->sp - 1] *= script->stack[script->sp]; break;
            case TNG_BC_DIV:
                script->sp--;
                if(script->stack[script->sp]) script->stack[script->sp - 1] /= script->stack[script->sp];
                else { script->stack[script->sp - 1] = 0; script_report(script, "divison by zero"); }
            break;
            case TNG_BC_MOD:
                script->sp--;
                if(script->stack[script->sp]) script->stack[script->sp - 1] %= script->stack[script->sp];
                else script->stack[script->sp - 1] = 0;
            break;
            case TNG_BC_EQ: script->sp--; script->stack[script->sp - 1] = (script->stack[script->sp - 1] == script->stack[script->sp]); break;
            case TNG_BC_NE: script->sp--; script->stack[script->sp - 1] = (script->stack[script->sp - 1] != script->stack[script->sp]); break;
            case TNG_BC_GE: script->sp--; script->stack[script->sp - 1] = (script->stack[script->sp - 1] >= script->stack[script->sp]); break;
            case TNG_BC_GT: script->sp--; script->stack[script->sp - 1] = (script->stack[script->sp - 1] > script->stack[script->sp]); break;
            case TNG_BC_LE: script->sp--; script->stack[script->sp - 1] = (script->stack[script->sp - 1] <= script->stack[script->sp]); break;
            case TNG_BC_LT: script->sp--; script->stack[script->sp - 1] = (script->stack[script->sp - 1] < script->stack[script->sp]); break;
            case TNG_BC_NOT: script->stack[script->sp - 1] = (script->stack[script->sp - 1] ? 0 : 1); break;
            case TNG_BC_OR: script->sp--; script->stack[script->sp - 1] = (script->stack[script->sp - 1] || script->stack[script->sp]); break;
            case TNG_BC_AND: script->sp--; script->stack[script->sp - 1] = (script->stack[script->sp - 1] && script->stack[script->sp]); break;
            case TNG_BC_POP:
                arg = pc[1]; j = script_pop(script);
                if(arg >= 0 && arg < 26) script->regs[arg] = j; else script_report(script, "invalid register r%d", arg);
                script->pc++;
            break;
            case TNG_BC_POPA:
                j = script_pop(script);
                k = (pc[1] | (pc[2] << 8) | (pc[3] << 16));
                if(!this || !this->attrs) { script_report(script, "no this reference"); } else
                if(!sts || k >= stslen) { script_report(script, "no string table reference"); } else {
                    /* look up in string table and replace with optimized opcode */
                    str = sts + k; arg = 0;
                    if(world.attrs) {
                        for(i = 0; i < world.numattrs && (!world.attrs[i].name || strcmp(str, world.attrs[i].name)); i++);
                        if(i < world.numattrs) { arg = i; } else { script_report(script, "no such attribute '%s'", str); }
                    }
                    pc[0] |= 0x80;
                    pc[1] = arg & 0xff;
                    pc[2] = (arg >> 8) & 0xff;
                    pc[3] = (arg >> 16) & 0xff;
                    script_setattr(this, arg, j);
                }
                script->pc += 3;
            break;
            case 0x80 | TNG_BC_POPA:
                j = script_pop(script);
                if(!this || !this->attrs) { script_report(script, "no this reference"); }
                else { arg = pc[1] | (pc[2] << 8) | (pc[3] << 16); script_setattr(this, arg, j); }
                script->pc += 3;
            break;
            case TNG_BC_POPO:
                j = script_pop(script);
                k = (pc[1] | (pc[2] << 8) | (pc[3] << 16));
                if(!oppo || !oppo->attrs) { script_report(script, "no opponent reference"); } else
                if(!sts || k >= stslen) { script_report(script, "no string table reference"); } else {
                    /* look up in string table and replace with optimized opcode */
                    str = sts + k; arg = 0;
                    if(world.attrs) {
                        for(i = 0; i < world.numattrs && (!world.attrs[i].name || strcmp(str, world.attrs[i].name)); i++);
                        if(i < world.numattrs) { arg = i; } else { script_report(script, "no such attribute '%s'", str); }
                    }
                    pc[0] |= 0x80;
                    pc[1] = arg & 0xff;
                    pc[2] = (arg >> 8) & 0xff;
                    pc[3] = (arg >> 16) & 0xff;
                    script_setattr(oppo, arg, j);
                }
                script->pc += 3;
            break;
            case 0x80 | TNG_BC_POPO:
                j = script_pop(script);
                if(!oppo || !oppo->attrs) { script_report(script, "no opponent reference"); }
                else { arg = pc[1] | (pc[2] << 8) | (pc[3] << 16); script_setattr(oppo, arg, j); }
                script->pc += 3;
            break;
            case TNG_BC_PUSH:
                arg = pc[1];
                if(arg >= 0 && arg < 26) j = script->regs[arg]; else script_report(script, "invalid register r%d", arg);
                script_push(script, j);
                script->pc++;
            break;
            case TNG_BC_PUSHA:
                k = (pc[1] | (pc[2] << 8) | (pc[3] << 16));
                if(!this || !this->attrs) { script_report(script, "no this reference"); script_push(script, 0); } else
                if(!sts || k >= stslen) { script_report(script, "no string table reference"); script_push(script, 0); } else {
                    /* look up in string table and replace with optimized opcode */
                    str = sts + k; arg = 0;
                    if(world.attrs) {
                        for(i = 0; i < world.numattrs && (!world.attrs[i].name || strcmp(str, world.attrs[i].name)); i++);
                        if(i < world.numattrs) { arg = i; } else { script_report(script, "no such attribute '%s'", str); }
                    }
                    pc[0] |= 0x80;
                    pc[1] = arg & 0xff;
                    pc[2] = (arg >> 8) & 0xff;
                    pc[3] = (arg >> 16) & 0xff;
                    script_push(script, script_getattr(this, arg));
                }
                script->pc += 3;
            break;
            case 0x80 | TNG_BC_PUSHA:
                if(!this || !this->attrs) { script_report(script, "no this reference"); script_push(script, 0); }
                else { arg = pc[1] | (pc[2] << 8) | (pc[3] << 16); script_push(script, script_getattr(this, arg)); }
                script->pc += 3;
            break;
            case TNG_BC_PUSHO:
                k = (pc[1] | (pc[2] << 8) | (pc[3] << 16));
                if(!oppo || !oppo->attrs) { script_report(script, "no opponent reference"); script_push(script, 0); } else
                if(!sts || k >= stslen) { script_report(script, "no string table reference"); script_push(script, 0); } else {
                    /* look up in string table and replace with optimized opcode */
                    str = sts + k; arg = 0;
                    if(world.attrs) {
                        for(i = 0; i < world.numattrs && (!world.attrs[i].name || strcmp(str, world.attrs[i].name)); i++);
                        if(i < world.numattrs) { arg = i; } else { script_report(script, "no such attribute '%s'", str); }
                    }
                    pc[0] |= 0x80;
                    pc[1] = arg & 0xff;
                    pc[2] = (arg >> 8) & 0xff;
                    pc[3] = (arg >> 16) & 0xff;
                    script_push(script, script_getattr(oppo, arg));
                }
                script->pc += 3;
            break;
            case 0x80 | TNG_BC_PUSHO:
                j = script_pop(script);
                if(!oppo || !oppo->attrs) { script_report(script, "no opponent reference"); script_push(script, 0); }
                else { arg = pc[1] | (pc[2] << 8) | (pc[3] << 16); script_push(script, script_getattr(oppo, arg)); }
                script->pc += 3;
            break;
            case TNG_BC_PUSH8: arg = pc[1]; script_push(script, arg); script->pc++; break;
            case TNG_BC_PUSH16: arg = pc[1] | (pc[2] << 8); script_push(script, arg); script->pc += 2; break;
            case TNG_BC_PUSH24: arg = pc[1] | (pc[2] << 8) | (pc[3] << 16); script_push(script, arg); script->pc += 3; break;
            case TNG_BC_PUSH32: arg = pc[1] | (pc[2] << 8) | (pc[3] << 16) | (pc[4] << 24); script_push(script, arg); script->pc += 4; break;
            case TNG_BC_PUSHMAP:
                map_id = map_x = map_y = 0;
                k = (pc[1] | (pc[2] << 8) | (pc[3] << 16));
                if(!sts || k >= stslen) { script_report(script, "no string table reference"); script_push(script, 0); } else {
                    /* look up in string table and replace with optimized opcode */
                    str = sts + k; arg = 0;
                    if(world.map) {
                        for(i = 0; i < world.nummap && (!world.map[i].name || strcmp(str, world.map[i].name)); i++);
                        if(i < world.nummap) { arg = i; } else { script_report(script, "no such map '%s'", str); }
                    }
                    pc[0] |= 0x80;
                    pc[1] = arg & 0xff;
                    pc[2] = (arg >> 8) & 0xff;
                    pc[3] = (arg >> 16) & 0xff;
                    goto bc_pushmap;
                }
                script->pc += 7;
            break;
            case 0x80 | TNG_BC_PUSHMAP:
bc_pushmap:     map_id = (pc[1] | (pc[2] << 8) | (pc[3] << 16));
                map_x = pc[4] | (pc[5] << 8);
                map_y = pc[6] | (pc[7] << 8);
                /* we actually don't use the stack here, but make the sanity checker happy */
                script_push(script, -1);
                script->pc += 7;
            break;
            case TNG_BC_PUSHSPR:
                arg = 0; j = pc[2] | (pc[3] << 8);
                if(pc[1] <= SPR_SWR) {
                    j += world.tng[script->tng].ofs[(int)pc[1]];
                    if(j < world.numspr[(int)pc[1]])
                        arg = pc[1] | (j << 8);
                }
                pc[0] = TNG_BC_PUSH24;
                pc[1] = arg & 0xff;
                pc[2] = (arg >> 8) & 0xff;
                pc[3] = (arg >> 16) & 0xff;
                script_push(script, arg); script->pc += 3;
            break;
            case TNG_BC_PUSHMUS:
                k = (pc[1] | (pc[2] << 8) | (pc[3] << 16));
                if(!sts || k >= stslen) { script_report(script, "no string table reference"); script_push(script, 0); } else {
                    /* look up in string table and replace with optimized opcode */
                    str = sts + k; arg = 0;
                    if(world.music) {
                        for(i = 0; i < world.nummusic && (!world.music[i].name || strcmp(str, world.music[i].name)); i++);
                        if(i < world.nummusic) { arg = i; } else { script_report(script, "no such music '%s'", str); }
                    }
                    pc[0] = TNG_BC_PUSH24;
                    pc[1] = arg & 0xff;
                    pc[2] = (arg >> 8) & 0xff;
                    pc[3] = (arg >> 16) & 0xff;
                    script_push(script, arg);
                }
                script->pc += 3;
            break;
            case TNG_BC_PUSHSND:
                k = (pc[1] | (pc[2] << 8) | (pc[3] << 16));
                if(!sts || k >= stslen) { script_report(script, "no string table reference"); script_push(script, 0); } else {
                    /* look up in string table and replace with optimized opcode */
                    str = sts + k; arg = 0;
                    if(world.sound) {
                        for(i = 0; i < world.numsound && (!world.sound[i].name || strcmp(str, world.sound[i].name)); i++);
                        if(i < world.numsound) { arg = i; } else { script_report(script, "no such sound '%s'", str); }
                    }
                    pc[0] = TNG_BC_PUSH24;
                    pc[1] = arg & 0xff;
                    pc[2] = (arg >> 8) & 0xff;
                    pc[3] = (arg >> 16) & 0xff;
                    script_push(script, arg);
                }
                script->pc += 3;
            break;
            case TNG_BC_PUSHSPC:
                k = (pc[1] | (pc[2] << 8) | (pc[3] << 16));
                if(!sts || stslen) { script_report(script, "no string table reference"); script_push(script, 0); } else {
                    /* look up in string table and replace with optimized opcode */
                    str = sts + k; arg = 0;
                    if(world.speech) {
                        for(i = 0; i < world.numspc && (!world.speech[i].name || strcmp(str, world.speech[i].name)); i++);
                        if(i < world.numspc) { arg = i; } else { script_report(script, "no such speech '%s'", str); }
                    }
                    pc[0] = TNG_BC_PUSH24;
                    pc[1] = arg & 0xff;
                    pc[2] = (arg >> 8) & 0xff;
                    pc[3] = (arg >> 16) & 0xff;
                    script_push(script, arg);
                }
                script->pc += 3;
            break;
            case TNG_BC_PUSHCHR:
                k = (pc[1] | (pc[2] << 8) | (pc[3] << 16));
                if(!sts || k >= stslen) { script_report(script, "no string table reference"); script_push(script, 0); } else {
                    /* look up in string table and replace with optimized opcode */
                    str = sts + k; arg = 0;
                    if(world.choosers) {
                        for(i = 0; i < world.numchr && (!world.choosers[i].name || strcmp(str, world.choosers[i].name)); i++);
                        if(i < world.numchr) { arg = i; } else { script_report(script, "no such chooser '%s'", str); }
                    }
                    pc[0] = TNG_BC_PUSH24;
                    pc[1] = arg & 0xff;
                    pc[2] = (arg >> 8) & 0xff;
                    pc[3] = (arg >> 16) & 0xff;
                    script_push(script, arg);
                }
                script->pc += 3;
            break;
            case TNG_BC_PUSHCUT:
                k = (pc[1] | (pc[2] << 8) | (pc[3] << 16));
                if(!sts || k >= stslen) { script_report(script, "no string table reference"); script_push(script, 0); } else {
                    /* look up in string table and replace with optimized opcode */
                    str = sts + k; arg = 0;
                    if(world.cutscns) {
                        for(i = 0; i < world.numcut && (!world.cutscns[i].name || strcmp(str, world.cutscns[i].name)); i++);
                        if(i < world.numcut) { arg = i; } else { script_report(script, "no such cutscene '%s'", str); }
                    }
                    pc[0] = TNG_BC_PUSH24;
                    pc[1] = arg & 0xff;
                    pc[2] = (arg >> 8) & 0xff;
                    pc[3] = (arg >> 16) & 0xff;
                    script_push(script, arg);
                }
                script->pc += 3;
            break;
            case TNG_BC_PUSHDLG:
                k = (pc[1] | (pc[2] << 8) | (pc[3] << 16));
                if(!sts || k >= stslen) { script_report(script, "no string table reference"); script_push(script, 0); } else {
                    /* look up in string table and replace with optimized opcode */
                    str = sts + k; arg = 0;
                    if(world.dialogs) {
                        for(i = 0; i < world.numdlg && (!world.dialogs[i].name || strcmp(str, world.dialogs[i].name)); i++);
                        if(i < world.numdlg) { arg = i; } else { script_report(script, "no such dialog '%s'", str); }
                    }
                    pc[0] = TNG_BC_PUSH24;
                    pc[1] = arg & 0xff;
                    pc[2] = (arg >> 8) & 0xff;
                    pc[3] = (arg >> 16) & 0xff;
                    script_push(script, arg);
                }
                script->pc += 3;
            break;
            case TNG_BC_PUSHCFT:
                k = (pc[1] | (pc[2] << 8) | (pc[3] << 16));
                if(!sts || k >= stslen) { script_report(script, "no string table reference"); script_push(script, 0); } else {
                    /* look up in string table and replace with optimized opcode */
                    str = sts + k; arg = 0;
                    if(world.crafts) {
                        for(i = 0; i < world.numcft && (!world.crafts[i].name || strcmp(str, world.crafts[i].name)); i++);
                        if(i < world.numcft) { arg = i; } else { script_report(script, "no such crafting dialog '%s'", str); }
                    }
                    pc[0] = TNG_BC_PUSH24;
                    pc[1] = arg & 0xff;
                    pc[2] = (arg >> 8) & 0xff;
                    pc[3] = (arg >> 16) & 0xff;
                    script_push(script, arg);
                }
                script->pc += 3;
            break;
            case TNG_BC_PUSHQST:
                k = (pc[1] | (pc[2] << 8) | (pc[3] << 16));
                if(!sts || k >= stslen) { script_report(script, "no string table reference"); script_push(script, 0); } else {
                    /* look up in string table and replace with optimized opcode */
                    str = sts + k; arg = 0;
                    if(world.quests) {
                        for(i = 0; i < world.numqst && (!world.quests[i].name || strcmp(str, world.quests[i].name)); i++);
                        if(i < world.numqst) { arg = i; } else { script_report(script, "no such quest '%s'", str); }
                    }
                    pc[0] = TNG_BC_PUSH24;
                    pc[1] = arg & 0xff;
                    pc[2] = (arg >> 8) & 0xff;
                    pc[3] = (arg >> 16) & 0xff;
                    script_push(script, arg);
                }
                script->pc += 3;
            break;
            case TNG_BC_PUSHTXT:
                arg = (pc[1] | (pc[2] << 8) | (pc[3] << 16));
                if(!world.text) { script_report(script, "no translation table"); script_push(script, 0); } else
                if(arg < 0 || arg >= world.numtext) { script_report(script, "no such translation string #%d", arg); script_push(script, 0); }
                else { script_push(script, arg); }
                script->pc += 3;
            break;
            default: script_report(script, "invalid opcode %02X", *pc); return SCRIPT_ERR;
        }
        if(script->pc > script->bclen) {
#if DEBUG
            if(script_trace > 0) script_report(script, "out of code execution (attempt)");
#endif
            if(script->caller == SCRIPT_STARTUP) {
                numcallstack = 0;
                return SCRIPT_EXIT;
            }
        }
    }
end:return script->sp > 0 ? script->stack[script->sp - 1] : script->regs[0];
}

/**
 * Load a script
 * @param caller: bitcoded position of the script, used for callstack trace
 * @param bc: bytecode (may be altered, optimization take place as code runs)
 * @param bclen: length of bytecode
 * @param tng: expansion set reference
 * @param this: reference for executor's attribute list
 * @param oppo: opponent's attribute list
 */
int script_load(int caller, uint8_t *bc, int bclen, int tng, world_entity_t *this, world_entity_t *oppo)
{
    int ret = SCRIPT_ERR, i;

    if(!bc || bclen < 1) return SCRIPT_ERR;
    if(!this && world.players && world.numplayers == 1) this = &world.players[0].data;

    /* if this is a new script, add to the callstack, otherwise continue execution where it left off */
    if(!numcallstack || callstack[numcallstack - 1].bc != bc || callstack[numcallstack - 1].this != this) {
        if(numcallstack >= SCRIPT_CALLSIZE) { script_report(NULL, "too many recursions"); return SCRIPT_ERR; }
        memset(&callstack[numcallstack], 0, sizeof(script_t));
        callstack[numcallstack].caller = caller;
        callstack[numcallstack].bc = bc;
        callstack[numcallstack].bclen = bclen;
        callstack[numcallstack].tng = tng;
        callstack[numcallstack].this = this;
        callstack[numcallstack].oppo = oppo;
        callstack[numcallstack].actor = callstack[numcallstack].obj = -1;
        if(numcallstack)
            memcpy(&callstack[numcallstack].regs, callstack[numcallstack - 1].regs, sizeof(callstack[0].regs));
        if(this) {
            /* copy entity's coordinates to registers */
            callstack[numcallstack].regs[23] = this->x;
            callstack[numcallstack].regs[24] = this->y;
            callstack[numcallstack].regs[25] = this->d;
            /* on object sell and buy, also copy object's price to reg `a` and category to reg `b` */
            i = (caller >> 3) & 31;
            if((caller & 7) == SCRIPT_OBJ && (i == EVT_ONSELL || i == EVT_ONBUY)) {
                i = caller >> 8;
                if(world.objects && i >= 0 && i < world.numspr[SPR_TILE] && world.objects[i]) {
                    callstack[numcallstack].regs[0] = world.objects[i]->price;
                    callstack[numcallstack].regs[1] = world.objects[i]->pricecat;
                }
            }
        }
        numcallstack++;
    }
    if(numcallstack) {
        badtrace = 0;
        ret = script_exec(&callstack[numcallstack - 1]);
        if(ret != SCRIPT_AGAIN && numcallstack > 0) numcallstack--;
    }
    return ret;
}

/**
 * Run a cron script (multi-threaded mode)
 */
static void *script_cronthread(void *data)
{
    struct timespec delay;
    cron_t *cron = data;
    int ret;

    if(!data) return NULL;
    while(1) {
        while(cron->next > main_tick) {
            delay.tv_sec = (cron->next - main_tick) / 1000;
            delay.tv_nsec = ((cron->next - main_tick) % 1000) * 1000000;
            /*pthread_delay_np(&delay); <- not POSIX, and nanosleep() should delay the current thread only */
            nanosleep(&delay, NULL);
        }
        while((ret = script_exec(&cron->script)) == SCRIPT_AGAIN);
        if(ret < SCRIPT_AGAIN || !cron->repeat) break;
        if(verbose > 0 && cron->next + cron->repeat < main_tick)
            printf("%s: cron job timer%u run too long\r\n", main_me, cron->repeat);
        while(cron->next + cron->repeat < main_tick) cron->next += cron->repeat;
    }
    cron->script.bclen = 0;
    cron->th = 0;
    pthread_exit(NULL);
    return NULL;
}

/**
 * Add a cron script (by defalt, multi-threaded)
 */
int script_cron(int caller, uint8_t *bc, int bclen, int tng, world_entity_t *this, world_entity_t *oppo, int interval)
{
    int i;

    if(!bc || bclen < 1 || interval < 0 || interval > 600 * 1000) return -1;
    for(i = 0; i < numcrons && (!crons[i] || crons[i]->script.bc != bc); i++);
    if(i < numcrons) return 1;
    crons = (cron_t**)realloc(crons, (numcrons + 1) * sizeof(cron_t*));
    if(!crons) { numcrons = 0; return -1; }
    crons[numcrons] = (cron_t*)malloc(sizeof(cron_t));
    if(!crons[numcrons]) return -1;
    i = numcrons++;
    memset(crons[i], 0, sizeof(cron_t));
    crons[i]->script.caller = caller;
    crons[i]->script.bc = bc;
    crons[i]->script.bclen = bclen;
    crons[i]->script.tng = tng;
    crons[i]->script.this = this;
    crons[i]->script.oppo = oppo;
    crons[i]->script.actor = crons[i]->script.obj = -1;
    if(this) {
        crons[i]->script.regs[23] = this->x;
        crons[i]->script.regs[24] = this->y;
        crons[i]->script.regs[25] = this->d;
    }
    crons[i]->next = main_tick;
    crons[i]->repeat = interval;
#ifndef __EMSCRIPTEN__
    pthread_create(&crons[i]->th, NULL, script_cronthread, crons[i]);
#endif
    return i;
}

/**
 * Initialize cron scripts
 */
void script_init()
{
    int i;

    for(i = 0; i < WORLD_NUMCRON; i++)
        if(world.cron[i].value && world.cron[i].bc && world.cron[i].bclen)
            script_cron(SCRIPT_CALLER(SCRIPT_STARTUP, i), world.cron[i].bc, world.cron[i].bclen, world.cron[i].tng, NULL, NULL,
                world.cron[i].value);
}

/**
 * Free cron scripts
 */
void script_free()
{
    int i;
    if(crons) {
        for(i = 0; i < numcrons; i++)
            if(crons[i]) {
                if(crons[i]->th) {
                    pthread_cancel(crons[i]->th);
                    pthread_join(crons[i]->th, NULL);
                }
                free(crons[i]);
            }
        free(crons); crons = NULL;
    }
    numcrons = 0;
}

/**
 * Run cron jobs (clean up and failsafe single threaded mode)
 */
void script_crons()
{
    int i, ret;

    for(i = 0; i < numcrons; i++) {
        if(!crons[i] || !crons[i]->script.bc || !crons[i]->script.bclen) {
            if(crons[i]) {
                if(crons[i]->th) {
                    pthread_cancel(crons[i]->th);
                    pthread_join(crons[i]->th, NULL);
                }
                free(crons[i]);
            }
            memcpy(&crons[i], &crons[i + 1], (numcrons - i) * sizeof(cron_t*));
            numcrons--; i--; continue;
        }
        /* this should never be used, only failsafe. Single threaded run */
        if(!crons[i]->th && crons[i]->next <= main_tick) {
            ret = script_exec(&crons[i]->script);
            if(ret == SCRIPT_AGAIN) continue;
            if(ret > SCRIPT_AGAIN && crons[i]->repeat > 0) {
                /* we are running in the main thread, so main_tick probably isn't refreshed... */
                main_gettick();
                while(crons[i]->next + crons[i]->repeat < main_tick) crons[i]->next += crons[i]->repeat;
            } else
                crons[i--]->script.bclen = 0;
        }
    }
}

/**
 * Update actor positions and animate
 */
void script_animactors()
{
}
