/* debugger.c: Glulxe debugger functions.
    Designed by Andrew Plotkin <erkyrath@eblong.com>
    http://eblong.com/zarf/glulx/index.html
*/

/* This module is a low-level debugger for Inform 6 (and 7) games.

   This code is only compiled in if the "#define VM_DEBUGGER" line in
   glulxe.h is uncommented. You will then have to compile with a Glk
   library that supports the debug feature. (See gi_debug.h as distributed
   with CheapGlk.) You will also need to link the libxml2 library.

   When debugging is compiled in, glulxe looks for a "Dbug" chunk in the
   Blorb file. This should contain the "gameinfo.dbg" file generated by
   the I6 compiler. (Must be the new XML-based debug format.) It also
   offers a bunch of new command-line options:
   
   --gameinfo filename: Read the "gameinfo.dbg" from an external file.
     (If you're not using Blorb or have not pulled the debug info
     into it.) 
   --cpu: Display time and VM CPU cycles used for each input.
   --starttrap: Enter debug mode as soon as the game starts up. (Before
     any code has executed.)
   --quittrap: Enter debug mode when the game quits.
   --crashtrap: Enter debug mode if any fatal error occurs (including
     Glk library fatal errors).

   You do not need "gameinfo.dbg" data to use the debugger. However, that
   file contains the I6-level names of all functions and variables. So
   without it, you'll see a lot of "???" in the debug output.

   The debug model assumes that you have a "debug console" available, which
   can accept debug commands and display responses. (The implementation
   of this depends on the Glk library.) You can enter commands whenever
   the game is awaiting input, or if it pauses because of the --starttrap
   flag, the --crashtrap flag, a breakpoint, or a @debugtrap opcode.

   The debugger is currently pretty minimal. You can display the stack
   (including local variables), display global variables, and set
   breakpoints (only at the beginning of a function). Type "help" into
   the debug console for the command list.

   (If you've compiled with CheapGlk, then the "debug console" is just
   standard output. Enter a line beginning with "/" to send a debug
   command -- "/help", etc.) (This "/" convention is *only* used in
   CheapGlk. Other libraries will have debug interfaces that don't
   suck.)
*/

#include "glk.h"
#include "glulxe.h"

#if VM_DEBUGGER

#include <string.h>
#include <sys/time.h>
#include "gi_debug.h" 
#include <libxml/xmlreader.h>

/* Data structures to store the debug info in memory. */

typedef enum grouptype_enum {
    grp_None = 0,
    grp_Constant = 1,
    grp_Attribute = 2,
    grp_Property = 3,
    grp_Routine = 4,
    grp_Global = 5,
    grp_Object = 6,
    grp_Array = 7,
} grouptype;

/* Used for constants, globals, locals, objects -- the meaning of the value
   field varies. */
typedef struct infoconstant_struct {
    const xmlChar *identifier;
    int32_t value;
} infoconstant;

typedef struct inforoutine_struct {
    const xmlChar *identifier;
    int32_t address;
    int32_t length;

    /* Address of the next higher function. May be beyond length if there
       are gaps. */
    int32_t nextaddress; 

    /* The locals is a block of infoconstants where the value is 
       frame-offset. We adopt Inform's assumption that locals are
       always 4 bytes long. */
    int32_t numlocals;
    infoconstant *locals;
} inforoutine;

typedef struct infoobject_struct {
    const xmlChar *identifier;
    int32_t address;

    /* Address of the next higher function. May be beyond length if there
       are gaps. */
    int32_t nextaddress;
} infoobject;

typedef struct infoarray_struct {
    const xmlChar *identifier;
    int32_t address;
    int32_t bytelength;
    int32_t bytesize; /* per element */
    int32_t count;
    int lengthfield; /* true if the zeroth element is the length */

    /* Address of the next higher function. May be beyond length if there
       are gaps. */
    int32_t nextaddress; 
} infoarray;

typedef struct breakpoint_struct {
    inforoutine *func;
    int32_t address;
    struct breakpoint_struct *next; /* linked list of breakpoints */
} breakpoint;

typedef struct debuginfofile_struct {
    strid_t str;
    int32_t strread;
    int32_t strreadmax;
    int failed;

    grouptype curgrouptype;

    int tempcounter;
    infoconstant *tempconstant;
    inforoutine *temproutine;
    infoarray *temparray;
    infoobject *tempobject;
    int tempnumlocals;
    int templocalssize;
    infoconstant *templocals;

    const xmlChar *storyfileprefix;
    xmlHashTablePtr constants;
    xmlHashTablePtr attributes;
    xmlHashTablePtr properties;
    xmlHashTablePtr globals;
    xmlHashTablePtr objects;
    int numobjects;
    infoobject **objectlist; /* array, ordered by address */
    xmlHashTablePtr arrays;
    int numarrays;
    infoarray **arraylist; /* array, ordered by address */
    xmlHashTablePtr routines;
    int numroutines;
    inforoutine **routinelist; /* array, ordered by address */
} debuginfofile;

/* This global holds the loaded debug info, if we have any. */
static debuginfofile *debuginfo = NULL;

/* Lists of breakpoints. */
static breakpoint *funcbreakpoints = NULL; /* linked list */

/* Internal functions used while loading the debug info. */

static int xmlreadstreamfunc(void *rock, char *buffer, int len);
static int xmlreadchunkfunc(void *rock, char *buffer, int len);
static int xmlclosefunc(void *rock);
static void xmlhandlenode(xmlTextReaderPtr reader, debuginfofile *context);
static int finalize_debuginfo(debuginfofile *context);

static debuginfofile *create_debuginfofile()
{
    debuginfofile *context = (debuginfofile *)malloc(sizeof(debuginfofile));
    context->str = NULL;
    context->failed = 0;
    context->tempconstant = NULL;
    context->temparray = NULL;
    context->tempobject = NULL;
    context->temproutine = NULL;
    context->tempnumlocals = 0;
    context->templocals = NULL;
    context->templocalssize = 0;
    context->curgrouptype = grp_None;
    context->constants = xmlHashCreate(16);
    context->attributes = xmlHashCreate(16);
    context->properties = xmlHashCreate(16);
    context->globals = xmlHashCreate(16);
    context->objects = xmlHashCreate(16);
    context->numobjects = 0;
    context->objectlist = NULL;
    context->arrays = xmlHashCreate(16);
    context->numarrays = 0;
    context->arraylist = NULL;
    context->routines = xmlHashCreate(16);
    context->numroutines = 0;
    context->routinelist = NULL;
    context->storyfileprefix = NULL;

    return context;
}

static void free_debuginfofile(debuginfofile *context)
{
    if (!context)
        return;

    context->str = NULL;
    context->tempconstant = NULL;
    context->temparray = NULL;
    context->tempobject = NULL;
    context->temproutine = NULL;
 
    /* We don't bother to free the member structures, because this
       only happens once at startup and then only on error
       conditions. */

    if (context->constants) {
        xmlHashFree(context->constants, NULL);
        context->constants = NULL;
    }

    if (context->attributes) {
        xmlHashFree(context->attributes, NULL);
        context->attributes = NULL;
    }

    if (context->properties) {
        xmlHashFree(context->properties, NULL);
        context->properties = NULL;
    }

    if (context->globals) {
        xmlHashFree(context->globals, NULL);
        context->globals = NULL;
    }

    if (context->objects) {
        xmlHashFree(context->objects, NULL);
        context->objects = NULL;
    }

    if (context->objectlist) {
        free(context->objectlist);
        context->objectlist = NULL;
    }

    if (context->arrays) {
        xmlHashFree(context->arrays, NULL);
        context->arrays = NULL;
    }

    if (context->arraylist) {
        free(context->arraylist);
        context->arraylist = NULL;
    }

    if (context->routines) {
        xmlHashFree(context->routines, NULL);
        context->routines = NULL;
    }

    if (context->routinelist) {
        free(context->routinelist);
        context->routinelist = NULL;
    }

    free(context);
}

static infoconstant *create_infoconstant()
{
    infoconstant *cons = (infoconstant *)malloc(sizeof(infoconstant));
    cons->identifier = NULL;
    cons->value = 0;
    return cons;
}

static infoobject *create_infoobject()
{
    infoobject *object = (infoobject *)malloc(sizeof(infoobject));
    object->identifier = NULL;
    object->address = 0;
    object->nextaddress = 0;
    return object;
}

static infoarray *create_infoarray()
{
    infoarray *arr = (infoarray *)malloc(sizeof(infoarray));
    arr->identifier = NULL;
    arr->address = 0;
    arr->bytelength = 0;
    arr->bytesize = 1;
    arr->count = 0;
    arr->lengthfield = 0;
    arr->nextaddress = 0;
    return arr;
}

static inforoutine *create_inforoutine()
{
    inforoutine *func = (inforoutine *)malloc(sizeof(inforoutine));
    func->identifier = NULL;
    func->address = 0;
    func->length = 0;
    func->nextaddress = 0;
    func->numlocals = 0;
    func->locals = NULL;
    return func;
}

static breakpoint *create_breakpoint(glui32 addr)
{
    breakpoint *bp = (breakpoint *)malloc(sizeof(breakpoint));
    bp->func = NULL; /* useful? */
    bp->address = addr;
    bp->next = NULL;
    return bp;
}

static void add_object_to_table(void *obj, void *rock, xmlChar *name)
{
    debuginfofile *context = rock;
    infoobject *object = obj;

    if (context->tempcounter >= context->numobjects) {
        printf("### object overflow!\n"); /*###*/
        return;
    }

    context->objectlist[context->tempcounter++] = object;
}

static int sort_objects_table(const void *obj1, const void *obj2)
{
    infoobject **object1 = (infoobject **)obj1;
    infoobject **object2 = (infoobject **)obj2;

    return ((*object1)->address - (*object2)->address);
}

static int find_object_in_table(const void *keyptr, const void *obj)
{
    /* Binary-search callback. We rely on address and nextaddress so
       that there are no gaps. */

    glui32 addr = *(glui32 *)(keyptr);
    infoobject **object = (infoobject **)obj;

    if (addr < (*object)->address)
        return -1;
    if (addr >= (*object)->nextaddress)
        return 1;
    return 0;
}

static void add_array_to_table(void *obj, void *rock, xmlChar *name)
{
    debuginfofile *context = rock;
    infoarray *array = obj;

    if (context->tempcounter >= context->numarrays) {
        printf("### array overflow!\n"); /*###*/
        return;
    }

    context->arraylist[context->tempcounter++] = array;
}

static int sort_arrays_table(const void *obj1, const void *obj2)
{
    infoarray **array1 = (infoarray **)obj1;
    infoarray **array2 = (infoarray **)obj2;

    return ((*array1)->address - (*array2)->address);
}

static int find_array_in_table(const void *keyptr, const void *obj)
{
    /* Binary-search callback. We rely on address and nextaddress so
       that there are no gaps. */

    glui32 addr = *(glui32 *)(keyptr);
    infoarray **array = (infoarray **)obj;

    if (addr < (*array)->address)
        return -1;
    if (addr >= (*array)->nextaddress)
        return 1;
    return 0;
}

static void add_routine_to_table(void *obj, void *rock, xmlChar *name)
{
    debuginfofile *context = rock;
    inforoutine *routine = obj;

    if (context->tempcounter >= context->numroutines) {
        printf("### array overflow!\n"); /*###*/
        return;
    }

    context->routinelist[context->tempcounter++] = routine;
}

static int sort_routines_table(const void *obj1, const void *obj2)
{
    inforoutine **routine1 = (inforoutine **)obj1;
    inforoutine **routine2 = (inforoutine **)obj2;

    return ((*routine1)->address - (*routine2)->address);
}

static int find_routine_in_table(const void *keyptr, const void *obj)
{
    /* Binary-search callback. We rely on address and nextaddress so
       that there are no gaps. */

    glui32 addr = *(glui32 *)(keyptr);
    inforoutine **routine = (inforoutine **)obj;

    if (addr < (*routine)->address)
        return -1;
    if (addr >= (*routine)->nextaddress)
        return 1;
    return 0;
}

/* Top-level function for loading debug info from a Glk stream.
   The debug data must take up the entire file; this will read until EOF.
   If successful, fills out the debuginfo global and returns true.
   If not, reports an error and returns false.
   (The stream will not be closed.)
 */
int debugger_load_info_stream(strid_t stream)
{
    debuginfofile *context = create_debuginfofile();
    context->str = stream;
    context->strread = 0; /* not used */
    context->strreadmax = 0; /* not used */

    xmlTextReaderPtr reader;
    reader = xmlReaderForIO(xmlreadstreamfunc, xmlclosefunc, context, 
        NULL, NULL, 
        XML_PARSE_RECOVER|XML_PARSE_NOENT|XML_PARSE_NONET|XML_PARSE_NOCDATA|XML_PARSE_COMPACT);
    if (!reader) {
        printf("Error: Unable to create XML reader.\n"); /*###*/
        free_debuginfofile(context);
        return 0;
    }

    while (1) {
        int res = xmlTextReaderRead(reader);
        if (res < 0) {
            context->failed = 1;
            break; /* error */
        }
        if (res == 0) {
            break; /* EOF */
        }
        xmlhandlenode(reader, context);
    }

    xmlFreeTextReader(reader);
    context->str = NULL; /* the reader didn't close it, but we're done with it. */

    if (context->failed) {
        printf("Error: Unable to load debug info.\n"); /*###*/
        free_debuginfofile(context);
        return 0;
    }

    /* Now that all the data is loaded in, we go through and create some
       indexes that will be handy. */
    return finalize_debuginfo(context);
}

/* Top-level function for loading debug info from a segment of a Glk stream.
   This starts at position pos in the file and reads len bytes.
   If successful, fills out the debuginfo global and returns true.
   If not, reports an error and returns false.
   (The stream will not be closed.)
 */
int debugger_load_info_chunk(strid_t stream, glui32 pos, glui32 len)
{
    debuginfofile *context = create_debuginfofile();
    context->str = stream;
    context->strread = 0;
    context->strreadmax = len;

    glk_stream_set_position(stream, pos, seekmode_Start);

    xmlTextReaderPtr reader;
    reader = xmlReaderForIO(xmlreadchunkfunc, xmlclosefunc, context, 
        NULL, NULL, 
        XML_PARSE_RECOVER|XML_PARSE_NOENT|XML_PARSE_NONET|XML_PARSE_NOCDATA|XML_PARSE_COMPACT);
    if (!reader) {
        printf("Error: Unable to create XML reader.\n"); /*###*/
        free_debuginfofile(context);
        return 0;
    }

    while (1) {
        int res = xmlTextReaderRead(reader);
        if (res < 0) {
            context->failed = 1;
            break; /* error */
        }
        if (res == 0) {
            break; /* EOF */
        }
        xmlhandlenode(reader, context);
    }

    xmlFreeTextReader(reader);
    context->str = NULL; /* the reader didn't close it, but we're done with it. */

    if (context->failed) {
        printf("Error: Unable to load debug info.\n"); /*###*/
        free_debuginfofile(context);
        return 0;
    }

    /* Now that all the data is loaded in, we go through and create some
       indexes that will be handy. */
    return finalize_debuginfo(context);
}

/* xmlReader callback to read from a stream (until EOF). */
static int xmlreadstreamfunc(void *rock, char *buffer, int len)
{
    debuginfofile *context = rock;
    int res = glk_get_buffer_stream(context->str, buffer, len);
    if (res < 0)
        return -1;
    return res;
}

/* xmlReader callback to read from a stream (until a given position). */
static int xmlreadchunkfunc(void *rock, char *buffer, int len)
{
    debuginfofile *context = rock;
    if (context->strread >= context->strreadmax)
        return 0;

    if (len > context->strreadmax - context->strread)
        len = context->strreadmax - context->strread;
    int res = glk_get_buffer_stream(context->str, buffer, len);
    if (res < 0)
        return -1;
    context->strread += res;
    return res;
}

/* xmlReader callback to stop reading a stream. We don't actually close
   the stream here, just discard the reference. */
static int xmlclosefunc(void *rock)
{
    debuginfofile *context = rock;
    context->str = NULL;
    return 0;
}

/* xmlReader callback: an XML node has arrived. (Might be the beginning of
   a tag, the end of a tag, or a block of text.) 

   All the work of parsing the debug format happens here, which is why this
   function is big and ugly.
*/
static void xmlhandlenode(xmlTextReaderPtr reader, debuginfofile *context)
{
    int depth = xmlTextReaderDepth(reader);
    int nodetype = xmlTextReaderNodeType(reader);

    if (depth == 0) {
        if (nodetype == XML_ELEMENT_NODE) {
            const xmlChar *name = xmlTextReaderConstName(reader);
            if (xmlStrcmp(name, BAD_CAST "inform-story-file")) {
                printf("Error: This is not an Inform debug info file.\n"); /*###*/
                context->failed = 1;
            }
        }
        else if (nodetype == XML_ELEMENT_DECL) {
            /* End of document */
            context->curgrouptype = grp_None;
            context->tempconstant = NULL;
            context->temparray = NULL;
            context->tempobject = NULL;
            context->temproutine = NULL;
        }
    }
    else if (depth == 1) {
        if (nodetype == XML_ELEMENT_NODE) {
            const xmlChar *name = xmlTextReaderConstName(reader);
            if (!xmlStrcmp(name, BAD_CAST "constant")) {
                context->curgrouptype = grp_Constant;
                context->tempconstant = create_infoconstant();
            }
            else if (!xmlStrcmp(name, BAD_CAST "attribute")) {
                context->curgrouptype = grp_Attribute;
                context->tempconstant = create_infoconstant();
            }
            else if (!xmlStrcmp(name, BAD_CAST "property")) {
                context->curgrouptype = grp_Property;
                context->tempconstant = create_infoconstant();
            }
            else if (!xmlStrcmp(name, BAD_CAST "routine")) {
                context->curgrouptype = grp_Routine;
                context->temproutine = create_inforoutine();
                context->tempnumlocals = 0;
            }
            else if (!xmlStrcmp(name, BAD_CAST "global-variable")) {
                context->curgrouptype = grp_Global;
                context->tempconstant = create_infoconstant();
            }
            else if (!xmlStrcmp(name, BAD_CAST "object")) {
                context->curgrouptype = grp_Object;
                context->tempobject = create_infoobject();
            }
            else if (!xmlStrcmp(name, BAD_CAST "array")) {
                context->curgrouptype = grp_Array;
                context->temparray = create_infoarray();
            }
            else if (!xmlStrcmp(name, BAD_CAST "story-file-prefix")) {
                xmlNodePtr nod = xmlTextReaderExpand(reader);
                if (nod && nod->children && nod->children->type == XML_TEXT_NODE) {
                    context->storyfileprefix = xmlStrdup(nod->children->content);
                }
            }
            else {
                context->curgrouptype = grp_None;
            }
        }
        else if (nodetype == XML_ELEMENT_DECL) {
            /* End of group */
            switch (context->curgrouptype) {
            case grp_Constant:
                if (context->tempconstant) {
                    infoconstant *dat = context->tempconstant;
                    context->tempconstant = NULL;
                    xmlHashAddEntry(context->constants, dat->identifier, dat);
                }
                break;
            case grp_Attribute:
                if (context->tempconstant) {
                    infoconstant *dat = context->tempconstant;
                    context->tempconstant = NULL;
                    xmlHashAddEntry(context->attributes, dat->identifier, dat);
                }
                break;
            case grp_Property:
                if (context->tempconstant) {
                    infoconstant *dat = context->tempconstant;
                    context->tempconstant = NULL;
                    xmlHashAddEntry(context->properties, dat->identifier, dat);
                }
                break;
            case grp_Global:
                if (context->tempconstant) {
                    infoconstant *dat = context->tempconstant;
                    context->tempconstant = NULL;
                    xmlHashAddEntry(context->globals, dat->identifier, dat);
                }
                break;
            case grp_Object:
                if (context->tempobject) {
                    infoobject *dat = context->tempobject;
                    context->tempobject = NULL;
                    xmlHashAddEntry(context->objects, dat->identifier, dat);
                }
                break;
            case grp_Array:
                if (context->temparray) {
                    infoarray *dat = context->temparray;
                    context->temparray = NULL;
                    dat->count = dat->bytelength / dat->bytesize;
                    xmlHashAddEntry(context->arrays, dat->identifier, dat);
                }
                break;
            case grp_Routine:
                if (context->temproutine) {
                    inforoutine *dat = context->temproutine;
                    context->temproutine = NULL;
                    /* Copy the list of locals into the inforoutine structure.
                       This loop totally assumes that locals are found
                       in order in the debug file! */
                    if (context->tempnumlocals && context->templocals) {
                        int ix;
                        dat->numlocals = context->tempnumlocals;
                        dat->locals = (infoconstant *)malloc(context->tempnumlocals * sizeof(infoconstant));
                        for (ix=0; ix<context->tempnumlocals; ix++) {
                            dat->locals[ix].identifier = context->templocals[ix].identifier;
                            dat->locals[ix].value = context->templocals[ix].value;
                            context->templocals[ix].identifier = NULL;
                            context->templocals[ix].value = 0;
                        }
                    }
                    context->tempnumlocals = 0;
                    /* An address of 0 is impossible. Inform uses this to
                       indicate a routine that was eliminated because it was
                       never called. */
                    if (dat->address != 0) {
                        xmlHashAddEntry(context->routines, dat->identifier, dat);
                    }
                }
                break;
            default:
                break;
            }
            context->curgrouptype = grp_None;
        }
    }
    else {
        if (nodetype == XML_ELEMENT_NODE) {
            const xmlChar *name = xmlTextReaderConstName(reader);
            /* These fields are always simple text nodes. */
            if (!xmlStrcmp(name, BAD_CAST "identifier")) {
                xmlNodePtr nod = xmlTextReaderExpand(reader);
                if (nod && nod->children && nod->children->type == XML_TEXT_NODE) {
                    xmlChar *text = nod->children->content;
                    if (context->curgrouptype == grp_Constant
                        || context->curgrouptype == grp_Attribute
                        || context->curgrouptype == grp_Property
                        || context->curgrouptype == grp_Global) {
                        if (context->tempconstant) {
                            if (depth == 2)
                                context->tempconstant->identifier = xmlStrdup(text);
                        }
                    }
                    else if (context->curgrouptype == grp_Object) {
                        if (context->tempobject) {
                            if (depth == 2)
                                context->tempobject->identifier = xmlStrdup(text);
                        }
                    }
                    else if (context->curgrouptype == grp_Array) {
                        if (context->temparray) {
                            if (depth == 2)
                                context->temparray->identifier = xmlStrdup(text);
                        }
                    }
                    else if (context->curgrouptype == grp_Routine) {
                        if (context->temproutine) {
                            if (depth == 2)
                                context->temproutine->identifier = xmlStrdup(text);
                        }
                    }
                }
            }
            else if (!xmlStrcmp(name, BAD_CAST "value")) {
                xmlNodePtr nod = xmlTextReaderExpand(reader);
                if (nod && nod->children && nod->children->type == XML_TEXT_NODE) {
                    if (context->curgrouptype == grp_Constant
                        || context->curgrouptype == grp_Attribute
                        || context->curgrouptype == grp_Property) {
                        if (context->tempconstant) {
                            if (depth == 2)
                                context->tempconstant->value = strtol((char *)nod->children->content, NULL, 10);
                        }
                    }
                    else if (context->curgrouptype == grp_Object) {
                        if (context->tempobject) {
                            if (depth == 2)
                                context->tempobject->address = strtol((char *)nod->children->content, NULL, 10);
                        }
                    }
                    else if (context->curgrouptype == grp_Array) {
                        if (context->temparray) {
                            if (depth == 2)
                                context->temparray->address = strtol((char *)nod->children->content, NULL, 10);
                        }
                    }
                    else if (context->curgrouptype == grp_Routine) {
                        if (context->temproutine) {
                            if (depth == 2)
                                context->temproutine->address = strtol((char *)nod->children->content, NULL, 10);
                        }
                    }
                }
            }
            else if (!xmlStrcmp(name, BAD_CAST "address")) {
                xmlNodePtr nod = xmlTextReaderExpand(reader);
                if (nod && nod->children && nod->children->type == XML_TEXT_NODE) {
                    if (context->curgrouptype == grp_Global) {
                        if (context->tempconstant) {
                            if (depth == 2)
                                context->tempconstant->value = strtol((char *)nod->children->content, NULL, 10);
                        }
                    }
                }
            }
            else if (!xmlStrcmp(name, BAD_CAST "byte-count")) {
                xmlNodePtr nod = xmlTextReaderExpand(reader);
                if (nod && nod->children && nod->children->type == XML_TEXT_NODE) {
                    if (context->curgrouptype == grp_Routine) {
                        if (context->temproutine) {
                            if (depth == 2)
                                context->temproutine->length = strtol((char *)nod->children->content, NULL, 10);
                        }
                    }
                    else if (context->curgrouptype == grp_Array) {
                        if (context->temparray) {
                            if (depth == 2)
                                context->temparray->bytelength = strtol((char *)nod->children->content, NULL, 10);
                        }
                    }
                }
            }
            else if (!xmlStrcmp(name, BAD_CAST "bytes-per-element")) {
                xmlNodePtr nod = xmlTextReaderExpand(reader);
                if (nod && nod->children && nod->children->type == XML_TEXT_NODE) {
                    if (context->curgrouptype == grp_Array) {
                        if (context->temparray) {
                            if (depth == 2)
                                context->temparray->bytesize = strtol((char *)nod->children->content, NULL, 10);
                        }
                    }
                }
            }
            else if (!xmlStrcmp(name, BAD_CAST "zeroth-element-holds-length")) {
                xmlNodePtr nod = xmlTextReaderExpand(reader);
                if (nod && nod->children && nod->children->type == XML_TEXT_NODE) {
                    if (context->curgrouptype == grp_Array) {
                        if (context->temparray) {
                            if (depth == 2)
                                if (!xmlStrcmp(nod->children->content, BAD_CAST "true"))
                                    context->temparray->lengthfield = 1;
                        }
                    }
                }
            }
            else if (!xmlStrcmp(name, BAD_CAST "local-variable")) {
                xmlNodePtr nod = xmlTextReaderExpand(reader);
                if (nod) {
                    if (!context->templocals) {
                        context->templocalssize = 8;
                        context->templocals = (infoconstant *)malloc(context->templocalssize * sizeof(infoconstant));
                    }
                    if (context->tempnumlocals >= context->templocalssize) {
                        context->templocalssize = 2*context->tempnumlocals + 4;
                        context->templocals = (infoconstant *)realloc(context->templocals, context->templocalssize * sizeof(infoconstant));
                    }
                    infoconstant *templocal = &(context->templocals[context->tempnumlocals]);
                    context->tempnumlocals += 1;
                    for (nod = nod->children; nod; nod=nod->next) {
                        if (nod->type == XML_ELEMENT_NODE) {
                            if (!xmlStrcmp(nod->name, BAD_CAST "identifier") && nod->children && nod->children->type == XML_TEXT_NODE) {
                                templocal->identifier = xmlStrdup(nod->children->content);
                            }
                            if (!xmlStrcmp(nod->name, BAD_CAST "frame-offset") && nod->children && nod->children->type == XML_TEXT_NODE) {
                                templocal->value = strtol((char *)nod->children->content, NULL, 10);
                            }
                        }
                    }
                }
            }
        }
    }
}

/* This is called after the XML data is parsed. We wrap up what we've
   found and store it in the debuginfo global. Returns 1 on success.
*/
static int finalize_debuginfo(debuginfofile *context)
{
    int ix;

    context->numarrays = xmlHashSize(context->arrays);
    context->arraylist = (infoarray **)malloc(context->numarrays * sizeof(infoarray *));

    context->tempcounter = 0;
    xmlHashScan(context->arrays, add_array_to_table, context);
    if (context->tempcounter != context->numarrays) 
        printf("### array underflow!\n"); /*###*/
    qsort(context->arraylist, context->numarrays, sizeof(infoarray *), sort_arrays_table);

    for (ix=0; ix<context->numarrays; ix++) {
        if (ix+1 < context->numarrays) {
            context->arraylist[ix]->nextaddress = context->arraylist[ix+1]->address;
        }
        else {
            context->arraylist[ix]->nextaddress = context->arraylist[ix]->address + context->arraylist[ix]->bytelength;
        }
    }

    context->numobjects = xmlHashSize(context->objects);
    context->objectlist = (infoobject **)malloc(context->numobjects * sizeof(infoobject *));

    context->tempcounter = 0;
    xmlHashScan(context->objects, add_object_to_table, context);
    if (context->tempcounter != context->numobjects) 
        printf("### object underflow!\n"); /*###*/
    qsort(context->objectlist, context->numobjects, sizeof(infoobject *), sort_objects_table);

    for (ix=0; ix<context->numobjects; ix++) {
        if (ix+1 < context->numobjects) {
            context->objectlist[ix]->nextaddress = context->objectlist[ix+1]->address;
        }
        else {
            context->objectlist[ix]->nextaddress = context->objectlist[ix]->address + 1;
        }
    }

    context->numroutines = xmlHashSize(context->routines);
    context->routinelist = (inforoutine **)malloc(context->numroutines * sizeof(inforoutine *));

    context->tempcounter = 0;
    xmlHashScan(context->routines, add_routine_to_table, context);
    if (context->tempcounter != context->numroutines) 
        printf("### array underflow!\n"); /*###*/
    qsort(context->routinelist, context->numroutines, sizeof(inforoutine *), sort_routines_table);

    for (ix=0; ix<context->numroutines; ix++) {
        if (ix+1 < context->numroutines) {
            context->routinelist[ix]->nextaddress = context->routinelist[ix+1]->address;
        }
        else {
            context->routinelist[ix]->nextaddress = context->routinelist[ix]->address + context->routinelist[ix]->length;
        }
    }

    if (context->templocals) {
        free(context->templocals);
        context->templocals = NULL;
    }
    context->templocalssize = 0;
    context->tempnumlocals = 0;

    /* Install into global. */
    debuginfo = context;
    return 1;
}

/* Compare main memory to the story-file-prefix we found. If it doesn't
   match, display a warning. 
*/
void debugger_check_story_file()
{
    if (!debuginfo || !debuginfo->storyfileprefix)
        return;

    /* Check that this looks like an Inform 6 game file. See the
       Glulx Inform Technical Reference. */
    if (Mem4(0x24) != 0x496E666F) { /* 'Info' */
        gidebug_output("Warning: This game file does not look like it was generated by Inform.");
    }

    /* Decode the storyfileprefix, which is in base64. */

    const unsigned char *cx;
    int pos = 0;
    int count = 0;
    uint32_t word = 0;
    int fail = FALSE;

    for (cx = debuginfo->storyfileprefix; *cx && *cx != '='; cx++) {
        unsigned int sixbit = 0;
        if (*cx >= 'A' && *cx <= 'Z')
            sixbit = (*cx) - 'A';
        else if (*cx >= 'a' && *cx <= 'z')
            sixbit = ((*cx) - 'a') + 26;
        else if (*cx >= '0' && *cx <= '9')
            sixbit = ((*cx) - '0') + 52;
        else if (*cx == '+')
            sixbit = 62;
        else if (*cx == '/')
            sixbit = 63;
        else
            sixbit = 0;

        switch (count) {
        case 0:
            word = (sixbit << 18);
            break;
        case 1:
            word |= (sixbit << 12);
            break;
        case 2:
            word |= (sixbit << 6);
            break;
        case 3:
            word |= (sixbit);
            break;
        }

        count++;
        if (count == 4) {
            unsigned char byte;
            byte = (word >> 16) & 0xFF;
            if (byte != Mem1(pos))
                fail = TRUE;
            byte = (word >> 8) & 0xFF;
            if (byte != Mem1(pos+1))
                fail = TRUE;
            byte = (word) & 0xFF;
            if (byte != Mem1(pos+2))
                fail = TRUE;
            pos += 3;
            count = 0;
        }
    }

    if (fail)
        gidebug_output("Warning: debug info <story-file-prefix> does not match this game file.");
}

/* The rest of this file is the debugger itself. */

static char *linebuf = NULL;
static int linebufsize = 0;

/* Expand the output line buffer to a given length, if necessary.
   If you want to write an N-character string, call ensure_line_buf(N).
   Then use snprintf() just to be safe. */
static void ensure_line_buf(int len)
{
    len += 1; /* for the closing null */
    if (linebuf && len <= linebufsize)
        return;

    linebufsize = linebufsize*2+16;
    if (linebufsize < len)
        linebufsize = len+16;

    if (!linebuf)
        linebuf = malloc(linebufsize * sizeof(char));
    else
        linebuf = realloc(linebuf, linebufsize * sizeof(char));
}

static int track_cpu = FALSE;
static int start_trap = FALSE;
static int crash_trap = FALSE;
static int quit_trap = FALSE;
static int debugger_invoked = FALSE; /* true if cycle handler called */
unsigned long debugger_opcount = 0; /* incremented in exec.c */
static struct timeval debugger_timer;

/* Set the track-CPU flag. (In fact we always track the VM CPU usage.
   This flag determines whether we report it to the debug console.)
*/
void debugger_track_cpu(int flag)
{
    track_cpu = flag;
}

/* Set the block-on-startup flag.
*/
void debugger_set_start_trap(int flag)
{
    start_trap = flag;
}

/* Set the block-on-quit flag.
*/
void debugger_set_quit_trap(int flag)
{
    quit_trap = flag;
}

/* Set the block-on-crash flag.
*/
void debugger_set_crash_trap(int flag)
{
    crash_trap = flag;
}

/* Has the library activated debugger features? If not, we assume
   that the debug console is entirely hidden from the user.
*/
int debugger_ever_invoked()
{
    return debugger_invoked;
}

/* Returns 1 if this is a (hex or decimal) numeric constant; 0 if not;
   -1 if it looks like a constant but is malformed. 

   On 1, the number itself is returned in res.
*/
static int parse_numeric_constant(char *arg, glui32 *res)
{
    if (arg[0] == '$') {
        char *cx;
        glui32 val = 0;
        for (cx=arg+1; *cx; cx++) {
            if (*cx >= '0' && *cx <= '9') {
                val = 16 * val + (*cx - '0');
                continue;
            }
            if (*cx >= 'A' && *cx <= 'F') {
                val = 16 * val + (*cx - 'A' + 10);
                continue;
            }
            if (*cx >= 'a' && *cx <= 'f') {
                val = 16 * val + (*cx - 'a' + 10);
                continue;
            }
            snprintf(linebuf, linebufsize, "Invalid hex number");
            gidebug_output(linebuf);
            return -1;
        }
        *res = val;
        return 1;
    }

    if (arg[0] >= '0' && arg[0] <= '9') {
        char *cx;
        glui32 val = 0;
        for (cx=arg; *cx; cx++) {
            if (*cx >= '0' && *cx <= '9') {
                val = 10 * val + (*cx - '0');
                continue;
            }
            snprintf(linebuf, linebufsize, "Invalid number");
            gidebug_output(linebuf);
            return -1;
        }
        *res = val;
        return 1;
    }

    return 0;
}

static infoarray *find_array_for_address(glui32 addr)
{
    if (!debuginfo)
        return NULL;

    infoarray **res = bsearch(&addr, debuginfo->arraylist, debuginfo->numarrays, sizeof(infoarray *), find_array_in_table);
    if (!res)
        return NULL;

    infoarray *arr = *res;
    if (addr < arr->address || addr >= arr->address + arr->bytelength)
        return NULL;

    return arr;
}

static infoobject *find_object_for_address(glui32 addr)
{
    if (!debuginfo)
        return NULL;

    infoobject **res = bsearch(&addr, debuginfo->objectlist, debuginfo->numobjects, sizeof(infoobject *), find_object_in_table);
    if (!res)
        return NULL;

    infoobject *arr = *res;
    if (addr != arr->address)
        return NULL;

    return arr;
}

static inforoutine *find_routine_for_address(glui32 addr)
{
    if (!debuginfo)
        return NULL;

    inforoutine **res = bsearch(&addr, debuginfo->routinelist, debuginfo->numroutines, sizeof(inforoutine *), find_routine_in_table);
    if (!res)
        return NULL;

    inforoutine *func = *res;
    if (addr < func->address || addr >= func->address + func->length)
        return NULL;

    return func;
}

static void render_value_linebuf(glui32 val)
{
    int tmplen;

    /* Special case for single-digit numbers: display the decimal
       digit and stop. */
    if (val < 10) {
        tmplen = strlen(linebuf);
        ensure_line_buf(tmplen+4);
        snprintf(linebuf+tmplen, linebufsize-tmplen, "%d", val);
        return;
    }

    /* Always display the signed decimal and unsigned hex. */
    tmplen = strlen(linebuf);
    ensure_line_buf(tmplen+40);
    if (val <= 0x7FFFFFFF)
        snprintf(linebuf+tmplen, linebufsize-tmplen, "%d ($%X)", val, val);
    else
        snprintf(linebuf+tmplen, linebufsize-tmplen, "%d ($%X)", -(int)(0x100000000-val), val);

    /* If the address of a function, display it. (But not addresses in
       the middle of a function.) */
    inforoutine *func = find_routine_for_address(val);
    if (func) {
        if (val == func->address) {
            tmplen = strlen(linebuf);
            ensure_line_buf(tmplen+40);
            snprintf(linebuf+tmplen, linebufsize-tmplen, ", %s()", func->identifier);
        }
    }

    /* If the address of an array, display it. */
    infoarray *arr = find_array_for_address(val);
    if (arr) {
        if (val == arr->address) {
            tmplen = strlen(linebuf);
            ensure_line_buf(tmplen+40);
            snprintf(linebuf+tmplen, linebufsize-tmplen, ", %s[%d]", arr->identifier, arr->count);
        }
    }

    /* If the address of an object, display it. */
    infoobject *object = find_object_for_address(val);
    if (object) {
        if (val == object->address) {
            tmplen = strlen(linebuf);
            ensure_line_buf(tmplen+40);
            snprintf(linebuf+tmplen, linebufsize-tmplen, ", %s", object->identifier);
        }
    }
}

static void debugcmd_backtrace(int wholestack)
{
    if (stack) {
        ensure_line_buf(256);
        glui32 curpc = pc;
        glui32 curframeptr = frameptr;
        glui32 curstackptr = stackptr;
        glui32 curvalstackbase = valstackbase;
        glui32 curlocalsbase = localsbase;
        glui32 locptr;
        int locnum;
        while (1) {
            inforoutine *routine = find_routine_for_address(curpc);
            if (!routine)
                snprintf(linebuf, linebufsize, "- %s()  (pc=$%.2X)", "???", curpc);
            else
                snprintf(linebuf, linebufsize, "- %s()  (pc=$%.2X)", routine->identifier, curpc);
            gidebug_output(linebuf);

            strcpy(linebuf, "   ");
            /* Again, this loop assumes that all locals are 4 bytes. */
            for (locptr = curlocalsbase, locnum = 0; locptr < curvalstackbase; locptr += 4, locnum++) {
                int tmplen = strlen(linebuf);
                ensure_line_buf(tmplen+32);
                if (!routine || !routine->locals || locnum >= routine->numlocals)
                    snprintf(linebuf+tmplen, linebufsize-tmplen, "%sloc#%d=", (locnum?"; ":""), locnum);
                else
                    snprintf(linebuf+tmplen, linebufsize-tmplen, "%s%s=", (locnum?"; ":""), routine->locals[locnum].identifier);
                
                glui32 val = Stk4(locptr);
                render_value_linebuf(val);
            }
            if (!locnum) {
                strcat(linebuf, "(no locals)");
            }
            gidebug_output(linebuf);

            if (!wholestack)
                break;

            curstackptr = curframeptr;
            if (curstackptr < 16)
                break;
            curstackptr -= 16;
            glui32 newframeptr = Stk4(curstackptr+12);
            glui32 newpc = Stk4(curstackptr+8);
            curframeptr = newframeptr;
            curpc = newpc;
            curvalstackbase = curframeptr + Stk4(curframeptr);
            curlocalsbase = curframeptr + Stk4(curframeptr+4);
        }
    }
}

static void debugcmd_set_breakpoint(char *arg)
{
    while (*arg == ' ')
        arg++;

    if (*arg == '\0') {
        gidebug_output("What function do you want to set a breakpoint at?");
        return;
    }

    int found = FALSE;
    glui32 addr = 0;

    if (!found) {
        int res = parse_numeric_constant(arg, &addr);
        if (res < 0)
            return;
        if (res > 0) {
            found = TRUE;
            /* If possible, check whether this looks like a function
               address. But if it doesn't, just print a warning. */
            if (debuginfo) {
                inforoutine *routine = find_routine_for_address(addr);
                if (!routine || routine->address != addr) {
                    ensure_line_buf(128);
                    strcpy(linebuf, "This does not look like a function address: ");
                    render_value_linebuf(addr);
                    gidebug_output(linebuf);
                }
            }
        }
    }

    if (!found) {
        if (!debuginfo) {
            gidebug_output("No debug info; cannot look up functions by name");
            return;
        }
        inforoutine *routine = xmlHashLookup(debuginfo->routines, BAD_CAST arg);
        if (!routine) {
            ensure_line_buf(128);
            snprintf(linebuf, linebufsize, "Not a function name: %s", arg);
            gidebug_output(linebuf);
            return;
        }
        found = TRUE;
        addr = routine->address;
    }

    if (!found)
        return;

    breakpoint *bp;
    for (bp = funcbreakpoints; bp; bp=bp->next) {
        if (bp->address == addr) {
            ensure_line_buf(128);
            strcpy(linebuf, "Breakpoint is already set for function: ");
            render_value_linebuf(addr);
            gidebug_output(linebuf);
            return;
        }
    }

    bp = create_breakpoint(addr);
    bp->next = funcbreakpoints;
    funcbreakpoints = bp;

    ensure_line_buf(128);
    strcpy(linebuf, "Breakpoint set for function: ");
    render_value_linebuf(addr);
    gidebug_output(linebuf);
}

static void debugcmd_clear_breakpoint(char *arg)
{
    while (*arg == ' ')
        arg++;

    if (*arg == '\0') {
        gidebug_output("What function do you want to clear a breakpoint at?");
        return;
    }

    int found = FALSE;
    glui32 addr = 0;

    if (!found) {
        int res = parse_numeric_constant(arg, &addr);
        if (res < 0)
            return;
        if (res > 0) {
            found = TRUE;
            /* If possible, check whether this looks like a function
               address. But if it doesn't, just print a warning. */
            if (debuginfo) {
                inforoutine *routine = find_routine_for_address(addr);
                if (!routine || routine->address != addr) {
                    ensure_line_buf(128);
                    strcpy(linebuf, "This does not look like a function address: ");
                    render_value_linebuf(addr);
                    gidebug_output(linebuf);
                }
            }
        }
    }

    if (!found) {
        if (!debuginfo) {
            gidebug_output("No debug info; cannot look up functions by name");
            return;
        }
        inforoutine *routine = xmlHashLookup(debuginfo->routines, BAD_CAST arg);
        if (!routine) {
            ensure_line_buf(128);
            snprintf(linebuf, linebufsize, "Not a function name: %s", arg);
            gidebug_output(linebuf);
            return;
        }
        found = TRUE;
        addr = routine->address;
    }

    if (!found)
        return;

    breakpoint **bpp;
    for (bpp = &funcbreakpoints; *bpp; bpp=&((*bpp)->next)) {
        if ((*bpp)->address == addr) {
            breakpoint *bp = *bpp;
            *bpp = bp->next;
            free(bp);
            ensure_line_buf(128);
            strcpy(linebuf, "Cleared breakpoint for function: ");
            render_value_linebuf(addr);
            gidebug_output(linebuf);
            return;
        }
    }

    ensure_line_buf(128);
    strcpy(linebuf, "No breakpoint found for function: ");
    render_value_linebuf(addr);
    gidebug_output(linebuf);
}

static void debugcmd_print(char *arg)
{
    ensure_line_buf(128); /* for a start */

    while (*arg == ' ')
        arg++;

    if (*arg == '\0') {
        gidebug_output("What do you want to print?");        
        return;
    }

    /* For plain numbers, and $HEX numbers, we print the value directly. */

    {
        glui32 val = 0;
        int res = parse_numeric_constant(arg, &val);
        if (res < 0)
            return;
        if (res > 0) {
            strcpy(linebuf, "");
            render_value_linebuf(val);
            gidebug_output(linebuf);
            return;
        }
    }

    /* Symbol recognition should be case-insensitive */

    /* Is it a local variable name? */
    if (debuginfo) {
        glui32 curpc = pc;
        glui32 curlocalsbase = localsbase;
        /* Should have a way to trawl up and down the stack. */
        inforoutine *routine = find_routine_for_address(curpc);
        if (routine && routine->locals) {
            int ix;
            for (ix=0; ix<routine->numlocals; ix++) {
                if (!xmlStrcmp(routine->locals[ix].identifier, BAD_CAST arg)) {
                    glui32 locptr = curlocalsbase + 4*ix;
                    glui32 val = Stk4(locptr);
                    snprintf(linebuf, linebufsize, "local %s = ", routine->locals[ix].identifier);
                    render_value_linebuf(val);
                    gidebug_output(linebuf);
                    return;
                }
            }
        }
    }

    /* Is it a constant name? */
    if (debuginfo) {
        infoconstant *cons = xmlHashLookup(debuginfo->constants, BAD_CAST arg);
        if (cons) {
            snprintf(linebuf, linebufsize, "%d ($%X): constant", cons->value, cons->value);
            gidebug_output(linebuf);
            return;
        }
    }

    /* Is it an attribute name? */
    if (debuginfo) {
        infoconstant *cons = xmlHashLookup(debuginfo->attributes, BAD_CAST arg);
        if (cons) {
            snprintf(linebuf, linebufsize, "%d ($%X): attribute", cons->value, cons->value);
            gidebug_output(linebuf);
            return;
        }
    }

    /* Is it a property name? */
    if (debuginfo) {
        infoconstant *cons = xmlHashLookup(debuginfo->properties, BAD_CAST arg);
        if (cons) {
            snprintf(linebuf, linebufsize, "%d ($%X): property", cons->value, cons->value);
            gidebug_output(linebuf);
            return;
        }
    }

    /* Is it an object name? */
    if (debuginfo) {
        infoobject *cons = xmlHashLookup(debuginfo->objects, BAD_CAST arg);
        if (cons) {
            snprintf(linebuf, linebufsize, "%d ($%X): object", cons->address, cons->address);
            gidebug_output(linebuf);
            return;
        }
    }

    /* Is it an array name? */
    if (debuginfo) {
        infoarray *arr = xmlHashLookup(debuginfo->arrays, BAD_CAST arg);
        if (arr) {
            snprintf(linebuf, linebufsize, "%d ($%X): array[%d] of %d-byte values", arr->address, arr->address, arr->count, arr->bytesize);
            gidebug_output(linebuf);
            return;
        }
    }

    /* Is it a global name? */
    if (debuginfo) {
        infoconstant *cons = xmlHashLookup(debuginfo->globals, BAD_CAST arg);
        if (cons) {
            glui32 val = Mem4(cons->value);
            snprintf(linebuf, linebufsize, "global %s = ", cons->identifier);
            render_value_linebuf(val);
            gidebug_output(linebuf);
            return;
        }
    }

    /* Is it a routine name? */
    if (debuginfo) {
        inforoutine *routine = xmlHashLookup(debuginfo->routines, BAD_CAST arg);
        if (routine) {
            snprintf(linebuf, linebufsize, "%d ($%X): routine", routine->address, routine->address);
            gidebug_output(linebuf);
            return;
        }
    }

    gidebug_output("Symbol not found");
}

static void debugcmd_help(char *arg)
{
    gidebug_output("Glulxe built-in debugger. Commands:");
    gidebug_output("- print <val>: Print a symbol or number.");
    gidebug_output("- bt: Display the current stack backtrace, with local variables.");
    gidebug_output("- break <func>: Set a breakpoint. (Must be a function name or the address of a function. Breakpoints currently only work at the start of a function.)");
    gidebug_output("- clear <func>: Clear a breakpoint.");
    gidebug_output("- cont: Continue execution. (From a breakpoint or other trap.)");
    gidebug_output("- help/?: This list.");
}

/* Debug console callback: This is invoked whenever the user enters a debug
   command.
   Returns 0 for most commands, 1 if the command ends a debugger pause.
*/
int debugger_cmd_handler(char *cmd)
{
    /* Trim spaces from start */
    while (*cmd == ' ')
        cmd++;
    /* Trim spaces from end */
    int len = strlen(cmd);
    while (len > 0 && cmd[len-1] == ' ') {
        cmd[len-1] = '\0';
        len--;
    }

    if (*cmd == '\0')
        return 0; /* empty command */

    char *cx;
    for (cx=cmd; *cx && *cx != ' '; cx++) { }
    len = (cx - cmd);

    if (len == 2 && !strncmp(cmd, "bt", len)) {
        debugcmd_backtrace(1);
        return 0;
    }

    if (len == 5 && !strncmp(cmd, "print", len)) {
        debugcmd_print(cx);
        return 0;
    }

    if (len == 5 && !strncmp(cmd, "break", len)) {
        debugcmd_set_breakpoint(cx);
        return 0;
    }

    if (len == 5 && !strncmp(cmd, "clear", len)) {
        debugcmd_clear_breakpoint(cx);
        return 0;
    }

    if (len == 4 && !strncmp(cmd, "cont", len)) {
        gidebug_output("Continuing...");
        return 1;
    }

    if (len == 4 && !strncmp(cmd, "help", len)) {
        debugcmd_help(cx);
        return 0;
    }

    if (len == 1 && !strncmp(cmd, "?", len)) {
        debugcmd_help(cx);
        return 0;
    }

    ensure_line_buf(strlen(cmd) + 64);
    snprintf(linebuf, linebufsize, "Unknown debug command: %s", cmd);
    gidebug_output(linebuf);
    return 0;
}

/* Debug console callback: This is invoked when the game starts, when it
   ends, and when each input cycle begins and ends.

   We take this opportunity to report CPU usage, if the track_cpu flag
   is set.
*/
void debugger_cycle_handler(int cycle)
{
    struct timeval now;
    double diff;

    debugger_invoked = TRUE;

    if (track_cpu) {
        switch (cycle) {

        case gidebug_cycle_Start:
            debugger_opcount = 0;
            gettimeofday(&debugger_timer, NULL);
            break;

        case gidebug_cycle_InputWait:
        case gidebug_cycle_DebugPause:
            gettimeofday(&now, NULL);
            diff = (now.tv_sec - debugger_timer.tv_sec) * 1000.0 + (now.tv_usec - debugger_timer.tv_usec) / 1000.0;
            ensure_line_buf(64);
            snprintf(linebuf, linebufsize, "VM: %ld cycles in %.3f ms", debugger_opcount, diff);
            gidebug_output(linebuf);
            break;

        case gidebug_cycle_InputAccept:
        case gidebug_cycle_DebugUnpause:
            debugger_opcount = 0;
            gettimeofday(&debugger_timer, NULL);
            break;

        }
    }
}

/* Do any start-time setup. This is called after VM memory is loaded,
   but before execution begins. 
*/
void debugger_setup_start_state(void)
{
    if (start_trap) {
        /* Block and debug right now. This doesn't install a trap, it
           just calls gidebug_pause() directly. */
        gidebug_output("Break at start:");
        debugcmd_backtrace(0);
        gidebug_pause();
    }
}

/* Check whether we've set a breakpoint at this function address.
   If so, block and debug.
*/
void debugger_check_func_breakpoint(glui32 addr)
{
    int pause = FALSE;
    breakpoint *bp;

    for (bp = funcbreakpoints; bp; bp=bp->next) {
        if (bp->address == addr) {
            pause = TRUE;
            break;
        }
    }

    if (pause) {
        gidebug_output("Breakpoint:");
        debugcmd_backtrace(0);
        gidebug_pause();
    }
}

/* Invoke the library's debug-block mechanism.
*/
void debugger_block_and_debug(char *msg)
{
    gidebug_output(msg);
    gidebug_pause();
}

/* If the quit-trap preference is set, enter debug mode.
*/
void debugger_handle_quit()
{
    if (quit_trap) {
        gidebug_output("Quit trap, pausing...");
        debugcmd_backtrace(0);
        gidebug_pause();
    }
}

/* Report a fatal error to the debug console, along with the current
   stack trace. Depending on preferences, we may then enter full-on debug mode.
*/
void debugger_handle_crash(char *msg)
{
    char *prefix = "Glulxe fatal error: ";
    ensure_line_buf(strlen(prefix) + strlen(msg));
    strcpy(linebuf, prefix);
    strcat(linebuf, msg);
    gidebug_output(linebuf);

    debugcmd_backtrace(1);

    if (crash_trap) {
        gidebug_pause();
    }
}


#endif /* VM_DEBUGGER */
