#ifdef RCSID
static char RCSid[] =
"$Header: d:/cvsroot/tads/TADS2/VOC.C,v 1.4 1999/07/11 00:46:31 MJRoberts Exp $";
#endif

/* 
 *   Copyright (c) 1991, 2002 Michael J. Roberts.  All Rights Reserved.
 *   
 *   Please see the accompanying license file, LICENSE.TXT, for information
 *   on using and copying this software.  
 */
/*
Name
  voc.c - vocabulary functions
Function
  Player command parser vocabulary word functions
Notes
  None
Modified
  04/11/99 CNebel        - Fix signing errors.
  11/07/91 MJRoberts     - creation
*/

#include <ctype.h>
#include <string.h>

#include "os.h"
#include "std.h"
#include "mch.h"
#include "mcm.h"
#include "voc.h"
#include "prp.h"
#include "obj.h"
#include "err.h"


/*
 *   Main vocabulary context.  This can be saved globally if desired, so
 *   that routines that don't have any other access to it (such as
 *   Unix-style signal handlers) can reach it.
 */
voccxdef *main_voc_ctx = 0;


#ifdef VOCW_IN_CACHE
vocwdef *vocwget(voccxdef *ctx, uint idx)
{
    uint pg;
    
    if (idx == VOCCXW_NONE)
        return 0;

    /* get the page we need */
    pg = idx/VOCWPGSIZ;

    /* if it's not locked, lock it */
    if (pg != ctx->voccxwplck)
    {
        /* unlock the old page */
        if (ctx->voccxwplck != MCMONINV)
            mcmunlck(ctx->voccxmem, ctx->voccxwp[ctx->voccxwplck]);

        /* lock the new page */
        ctx->voccxwpgptr = (vocwdef *)mcmlck(ctx->voccxmem,
                                             ctx->voccxwp[pg]);
        ctx->voccxwplck = pg;
    }

    /* return the entry on that page */
    return ctx->voccxwpgptr + (idx % VOCWPGSIZ);
}
#endif /*VOCW_IN_CACHE */

/* hash value is based on first 6 characters only to allow match-in-6 */
uint vochsh(uchar *t, int len)
{
    uint ret = 0;
    
    if (len > 6) len = 6;
    for ( ; len ; --len, ++t)
        ret = (ret + (uint)(vocisupper(*t) ? tolower(*t) : *t))
               & (VOCHASHSIZ - 1);
    return(ret);
}

/* copy vocabulary word, and convert to lower case */
static void voccpy(uchar *dst, uchar *src, int len)
{
    for ( ; len ; --len, ++dst, ++src)
        *dst = vocisupper(*src) ? tolower(*src) : *src;
}

/* allocate and set up a new vocwdef record, linking into a vocdef's list */
static void vocwset(voccxdef *ctx, vocdef *v, prpnum p, objnum objn,
                    int classflg)
{
    vocwdef *vw;
    uint     inx;
    vocwdef *vw2;

    /*
     *   look through the vocdef list to see if there's an existing entry
     *   with the DELETED marker -- if so, simply undelete it 
     */
    for (inx = v->vocwlst, vw = vocwget(ctx, inx) ; vw ;
         inx = vw->vocwnxt, vw = vocwget(ctx, inx))
    {
        /* if this entry was deleted, and otherwise matches, undelete it */
        if ((vw->vocwflg & VOCFDEL)
            && vw->vocwobj == objn && vw->vocwtyp == p)
        {
            /*
             *   Remove the deleted flag.  We will otherwise leave the
             *   flags unchanged, since the VOCFDEL flag applies only to
             *   statically allocated objects, and hence the original
             *   flags should take precedence over any run-time flags. 
             */
            vw->vocwflg &= ~VOCFDEL;

            /* we're done */
            return;
        }
    }

    /* make sure the word+object+type record isn't already defined */
    for (inx = v->vocwlst, vw = vocwget(ctx, inx) ; vw ;
         inx = vw->vocwnxt, vw = vocwget(ctx, inx))
    {
        if (vw->vocwobj == objn && vw->vocwtyp == p
            && (vw->vocwflg & VOCFCLASS) == (classflg & VOCFCLASS))
        {
            /* it matches - don't add a redundant record */
            return;
        }
    }
    
    /* look in the free list for an available vocwdef */
    if (ctx->voccxwfre != VOCCXW_NONE)
    {
        inx = ctx->voccxwfre;
        vw = vocwget(ctx, inx);                     /* get the free vocwdef */
        ctx->voccxwfre = vw->vocwnxt;              /* unlink from free list */
    }
    else
    {
        /* allocate another page of vocwdef's if necssary */
        if ((ctx->voccxwalocnt % VOCWPGSIZ) == 0)
        {
            int pg = ctx->voccxwalocnt / VOCWPGSIZ;
            
            /* make sure we haven't exceeded the available page count */
            if (pg >= VOCWPGMAX) errsig(ctx->voccxerr, ERR_VOCMNPG);
            
            /* allocate on the new page */
#ifdef VOCW_IN_CACHE
            mcmalo(ctx->voccxmem, (ushort)(VOCWPGSIZ * sizeof(vocwdef)),
                                           &ctx->voccxwp[pg]);
            mcmunlck(ctx->voccxmem, ctx->voccxwp[pg]);
#else
            ctx->voccxwp[pg] =
                (vocwdef *)mchalo(ctx->voccxerr,
                                  (VOCWPGSIZ * sizeof(vocwdef)),
                                  "vocwset");
#endif
        }

        /* get the next entry, and increment count of used entries */
        inx = ctx->voccxwalocnt++;
        vw = vocwget(ctx, inx);
    }

    /* link the new vocwdef into the vocdef's relation list */
    vw->vocwnxt = v->vocwlst;
    v->vocwlst = inx;

    /* set up the new vocwdef */
    vw->vocwtyp = (uchar)p;
    vw->vocwobj = objn;
    vw->vocwflg = classflg;

    /* 
     *   Scan the list and make sure we're not adding a redundant verb.
     *   Don't bother with the warning if this is a class. 
     */
    if (p == PRP_VERB && (ctx->voccxflg & VOCCXFVWARN)
        && (vw->vocwflg & VOCFCLASS) == 0)
    {
        for (vw2 = vocwget(ctx, v->vocwlst) ; vw2 ;
             vw2 = vocwget(ctx, vw2->vocwnxt))
        {
            /* 
             *   if this is a different object, and it's not a class, and
             *   it's defined as a verb, warn about it 
             */
            if (vw2 != vw
                && (vw2->vocwflg & VOCFCLASS) == 0
                && vw2->vocwtyp == PRP_VERB)
            {
                if (v->vocln2 != 0)
                    errlog2(ctx->voccxerr, ERR_VOCREVB,
                            ERRTSTR,
                            errstr(ctx->voccxerr,
                                   (char *)v->voctxt, v->voclen),
                            ERRTSTR,
                            errstr(ctx->voccxerr,
                                   (char *)v->voctxt + v->voclen, v->vocln2));
                else
                    errlog1(ctx->voccxerr, ERR_VOCREVB,
                            ERRTSTR,
                            errstr(ctx->voccxerr,
                                   (char *)v->voctxt, v->voclen));
                break;
            }
        }
    }
}

/* set up a vocdef record, and link into hash table */
static void vocset(voccxdef *ctx, vocdef *v, prpnum p, objnum objn,
                   int classflg, uchar *wrdtxt, int len,
                   uchar *wrd2, int len2)
{
    uint hshval = vochsh(wrdtxt, len);
    
    v->vocnxt = ctx->voccxhsh[hshval];
    ctx->voccxhsh[hshval] = v;
    
    v->voclen = len;
    v->vocln2 = len2;
    voccpy(v->voctxt, wrdtxt, len);
    if (wrd2) voccpy(v->voctxt + len, wrd2, len2);

    /* allocate and initialize a vocwdef for the object */
    vocwset(ctx, v, p, objn, classflg);
}

/* internal addword - already parsed into two words and have lengths */
void vocadd2(voccxdef *ctx, prpnum p, objnum objn, int classflg,
             uchar *wrdtxt, int len, uchar *wrd2, int len2)
{
    vocdef  *v;
    vocdef  *prv;
    uint     need;
    uint     hshval;

    /* if the word is null, ignore it entirely */
    if (len == 0 && len2 == 0)
        return;

    /* look for a vocdef entry with the same word text */
    hshval = vochsh(wrdtxt, len);
    for (v = ctx->voccxhsh[hshval] ; v ; v = v->vocnxt)
    {
        /* if it matches on both words, use this entry */
        if (v->voclen == len && !memcmp(wrdtxt, v->voctxt, (size_t)len)
            && ((!wrd2 && v->vocln2 == 0)
                || (v->vocln2 == len2 &&
                    !memcmp(wrd2, v->voctxt + len, (size_t)len2))))
        {
            vocwset(ctx, v, p, objn, classflg);
            return;
        }
    }

    /* look for a free vocdef entry of the same size */
    for (prv = (vocdef *)0, v = ctx->voccxfre ; v ; prv = v, v = v->vocnxt)
        if (v->voclen == len + len2) break;
    
    if (v)
    {
        /* we found something - unlink from free list */
        if (prv) prv->vocnxt = v->vocnxt;
        else ctx->voccxfre = v->vocnxt;
        
        /* reuse the entry */
        v->vocwlst = VOCCXW_NONE;
        vocset(ctx, v, p, objn, classflg, wrdtxt, len, wrd2, len2);
        return;
    }
    
    /* didn't find an existing vocdef; allocate a new one */
    need = sizeof(vocdef) + len + len2 - 1;
    if (ctx->voccxrem < need)
    {
        /* not enough space in current page; allocate a new one */
        ctx->voccxpool = mchalo(ctx->voccxerr, VOCPGSIZ, "vocadd2");
        ctx->voccxrem = VOCPGSIZ;
    }
    
    /* use top of current pool, and update pool pointer and size */
    v = (vocdef *)ctx->voccxpool;
    need = osrndsz(need);
    ctx->voccxpool += need;
    if (ctx->voccxrem > need) ctx->voccxrem -= need;
    else ctx->voccxrem = 0;
    
    /* set up new vocdef */
    v->vocwlst = VOCCXW_NONE;
    vocset(ctx, v, p, objn, classflg, wrdtxt, len, wrd2, len2);
}

static void voc_parse_words(char **wrdtxt, int *len, char **wrd2, int *len2)
{
    /* get length and pointer to actual text */
    *len = osrp2(*wrdtxt) - 2;
    *wrdtxt += 2;
    
    /* see if there's a second word - look for a space */
    for (*wrd2 = *wrdtxt, *len2 = *len ; *len2 && !vocisspace(**wrd2) ;
         ++*wrd2, --*len2) ;
    if (*len2)
    {
        *len -= *len2;
        while (*len2 && vocisspace(**wrd2)) ++*wrd2, --*len2;
    }
    else
    {
        /* no space ==> no second word */
        *wrd2 = (char *)0;
    }
}

void vocadd(voccxdef *ctx, prpnum p, objnum objn, int classflg, char *wrdtxt)
{
    int     len;
    char   *wrd2;
    int     len2;

    voc_parse_words(&wrdtxt, &len, &wrd2, &len2);
    vocadd2(ctx, p, objn, classflg, (uchar *)wrdtxt, len, (uchar *)wrd2, len2);
}

/* make sure we have a page table entry for an object, allocating one if not */
void vocialo(voccxdef *ctx, objnum obj)
{
    if (!ctx->voccxinh[obj >> 8])
    {
        ctx->voccxinh[obj >> 8] =
            (vocidef **)mchalo(ctx->voccxerr,
                               (256 * sizeof(vocidef *)), "vocialo");
        memset(ctx->voccxinh[obj >> 8], 0, (size_t)(256 * sizeof(vocidef *)));
    }
}

/* add an inheritance/location record */
void vociadd(voccxdef *ctx, objnum obj, objnum loc,
             int numsc, objnum *sc, int flags)
{
    vocidef *v;
    vocidef *min;
    vocidef *prv;
    vocidef *minprv;

    /* make sure we have a page table entry for this object */
    vocialo(ctx, obj);

    /* look in free list for an entry that's big enough */
    for (prv = (vocidef *)0, min = (vocidef *)0, v = ctx->voccxifr ; v ;
         prv = v, v = v->vocinxt)
    {
        if (v->vocinsc == numsc)
        {
            min = v;
            minprv = prv;
            break;
        }
        else if (v->vocinsc > numsc)
        {
            if (!min || v->vocinsc < min->vocinsc)
            {
                min = v;
                minprv = prv;
            }
        }
    }
    
    if (!min)
    {
        uint need;
        
        /* nothing in free list; allocate a new entry */
        need = osrndsz(sizeof(vocidef) + (numsc - 1)*sizeof(objnum));
        if (ctx->voccxilst + need >= VOCISIZ)
        {
            /* nothing left on current page; allocate a new page */
            ctx->voccxip[++(ctx->voccxiplst)] =
                mchalo(ctx->voccxerr, VOCISIZ, "vociadd");
            ctx->voccxilst = 0;
        }

        /* allocate space out of current page */
        v = (vocidef *)(ctx->voccxip[ctx->voccxiplst] + ctx->voccxilst);
        ctx->voccxilst += need;
    }
    else
    {
        /* unlink from chain and use */
        v = min;
        if (minprv)
            minprv->vocinxt = v->vocinxt;
        else
            ctx->voccxifr = v->vocinxt;
    }

    /* set up the entry */
    if (vocinh(ctx, obj) != (vocidef *)0) errsig(ctx->voccxerr, ERR_VOCINUS);
    v->vociloc = loc;
    v->vociilc = MCMONINV;
    v->vociflg = (flags & ~VOCIFXLAT);
    v->vocinsc = numsc;
    if (numsc)
    {
        if (flags & VOCIFXLAT)
        {
            int i;
            
            for (i = 0 ; i < numsc ; ++i)
                v->vocisc[i] = osrp2(&sc[i]);
        }
        else
            memcpy(v->vocisc, sc, (size_t)(numsc * sizeof(objnum)));
    }
    vocinh(ctx, obj) = v;                           /* set page table entry */
}

/* revert all objects to original state, using inheritance records */
void vocrevert(voccxdef *vctx)
{
    vocidef ***vpg;
    vocidef  **v;
    int        i;
    int        j;
    objnum     obj;

    /*
     *   Go through the inheritance records.  Delete each dynamically
     *   allocated object, and revert each static object to its original
     *   load state. 
     */
    for (vpg = vctx->voccxinh, i = 0 ; i < VOCINHMAX ; ++vpg, ++i)
    {
        if (!*vpg) continue;
        for (v = *vpg, obj = (i << 8), j = 0 ; j < 256 ; ++v, ++obj, ++j)
        {
            if (*v)
            {
                /* if the object was dynamically allocated, delete it */
                if ((*v)->vociflg & VOCIFNEW)
                {
                    /* delete vocabulary and inheritance data for the object */
                    vocidel(vctx, obj);
                    vocdel(vctx, obj);

                    /* delete the object */
                    mcmfre(vctx->voccxmem, (mcmon)obj);
                }
                else
                {
                    /* revert the object */
                    mcmrevert(vctx->voccxmem, (mcmon)obj);
                }
            }
        }
    }

    /*
     *   Revert the vocabulary list: delete all newly added words, and
     *   undelete all original words marked as deleted.  
     */
    vocdel1(vctx, MCMONINV, (char *)0, 0, TRUE, TRUE, FALSE);
}

/* initialize voc context */
void vocini(voccxdef *vocctx, errcxdef *errctx, mcmcxdef *memctx,
            runcxdef *runctx, objucxdef *undoctx,
            int fuses, int daemons, int notifiers)
{
    CLRSTRUCT(*vocctx);
    vocctx->voccxerr = errctx;
    vocctx->voccxiplst = (uint)-1;
    vocctx->voccxilst = VOCISIZ;
    vocctx->voccxmem = memctx;
    vocctx->voccxrun = runctx;
    vocctx->voccxundo = undoctx;

    vocctx->voccxme  =
    vocctx->voccxme_init = 
    vocctx->voccxvtk =
    vocctx->voccxstr =
    vocctx->voccxnum =
    vocctx->voccxit  =
    vocctx->voccxhim =
    vocctx->voccxprd =
    vocctx->voccxpre =
    vocctx->voccxpre2 =
    vocctx->voccxppc =
    vocctx->voccxlsv =
    vocctx->voccxpreinit =
    vocctx->voccxper =
    vocctx->voccxprom =
    vocctx->voccxpostprom =
    vocctx->voccxpdis =
    vocctx->voccxper2 =
    vocctx->voccxperp =
    vocctx->voccxpdef =
    vocctx->voccxpdef2 =
    vocctx->voccxpask =
    vocctx->voccxpask2 =
    vocctx->voccxpask3 =
    vocctx->voccxinitrestore =
    vocctx->voccxpuv =
    vocctx->voccxpnp =
    vocctx->voccxpostact =
    vocctx->voccxendcmd =
    vocctx->voccxher = MCMONINV;
    vocctx->voccxthc = 0;
#ifdef VOCW_IN_CACHE
    vocctx->voccxwplck = MCMONINV;
#endif

    vocctx->voccxactor = MCMONINV;
    vocctx->voccxverb = MCMONINV;
    vocctx->voccxprep = MCMONINV;
    vocctx->voccxdobj = 0;
    vocctx->voccxiobj = 0;

    vocctx->voccxunknown = 0;
    vocctx->voccxlastunk = 0;

    vocctx->voc_stk_ptr = 0;
    vocctx->voc_stk_cur = 0;
    vocctx->voc_stk_end = 0;

    /* allocate fuses, daemons, notifiers */
    vocinialo(vocctx, &vocctx->voccxfus, (vocctx->voccxfuc = fuses));
    vocinialo(vocctx, &vocctx->voccxdmn, (vocctx->voccxdmc = daemons));
    vocinialo(vocctx, &vocctx->voccxalm, (vocctx->voccxalc = notifiers));

    /* no entries in vocwdef free list yet */
    vocctx->voccxwfre = VOCCXW_NONE;
}

/* uninitialize the voc context */
void vocterm(voccxdef *ctx)
{
    /* delete the fuses, daemons, and notifiers */
    voctermfree(ctx->voccxfus);
    voctermfree(ctx->voccxdmn);
    voctermfree(ctx->voccxalm);

    /* delete the private stack */
    if (ctx->voc_stk_ptr != 0)
        mchfre(ctx->voc_stk_ptr);
}

/* clean up the vocab context */
void voctermfree(vocddef *what)
{
    if (what != 0)
        mchfre(what);
}

/*
 *   Iterate through all words for a particular object, calling a
 *   function with each vocwdef found.  If objn == MCMONINV, we'll call
 *   the callback for every word.  
 */
void voc_iterate(voccxdef *ctx, objnum objn,
                 void (*fn)(void *, vocdef *, vocwdef *), void *fnctx)
{
    int       i;
    vocdef   *v;
    vocdef  **vp;
    vocwdef  *vw;
    uint      idx;

    /* go through each hash value looking for matching words */
    for (i = VOCHASHSIZ, vp = ctx->voccxhsh ; i ; ++vp, --i)
    {
        /* go through all words in this hash chain */
        for (v = *vp ; v ; v = v->vocnxt)
        {
            /* go through each object relation for this word */
            for (idx = v->vocwlst, vw = vocwget(ctx, idx) ; vw ;
                 idx = vw->vocwnxt, vw = vocwget(ctx, idx))
            {
                /*
                 *   if this word is for this object, call the callback
                 */
                if (objn == MCMONINV || vw->vocwobj == objn)
                    (*fn)(fnctx, v, vw);
            }
        }
    }
}

/* callback context for voc_count */
struct voc_count_ctx
{
    int     cnt;
    int     siz;
    prpnum  prp;
};

/* callback for voc_count */
static void voc_count_cb(void *ctx0, vocdef *voc, vocwdef *vocw)
{
    struct voc_count_ctx *ctx = (struct voc_count_ctx *)ctx0;
    
    VARUSED(vocw);

    /*
     *   If it matches the property (or we want all properties), count
     *   it.  Don't count deleted objects. 
     */
    if ((ctx->prp == 0 || ctx->prp == vocw->vocwtyp)
        && !(vocw->vocwflg & VOCFDEL))
    {
        /* count the word */
        ctx->cnt++;
        
        /* count the size */
        ctx->siz += voc->voclen + voc->vocln2;
    }
}

/*
 *   Get the number and size of words defined for an object.  The size
 *   returns the total byte count from all the words involved.  Do not
 *   include deleted words in the count.  
 */
void voc_count(voccxdef *ctx, objnum objn, prpnum prp, int *cnt, int *siz)
{
    struct voc_count_ctx fnctx;

    /* set up the context with zero initial counts */
    fnctx.cnt = 0;
    fnctx.siz = 0;
    fnctx.prp = prp;

    /* iterate over all words for the object with our callback */
    voc_iterate(ctx, objn, voc_count_cb, &fnctx);

    /* return the data */
    if (cnt) *cnt = fnctx.cnt;
    if (siz) *siz = fnctx.siz;
}


/*
 *   Delete a particular word associated with an object, or all words if
 *   the word pointer is null.  If the word isn't marked as added at
 *   runtime (i.e., the VOCFNEW flag is not set for the word), the word is
 *   simply marked as deleted, rather than being actually deleted.
 *   However, if the 'really_delete' flag is set, the word is actually
 *   deleted.  If the 'revert' flag is true, this routine deletes _every_
 *   dynamically created word, and undeletes all dynamically deleted words
 *   that were in the original vocabulary.  
 */
void vocdel1(voccxdef *ctx, objnum objn, char *wrd1, prpnum prp,
             int really_delete, int revert, int keep_undo)
{
    int       i;
    vocdef   *v;
    vocdef   *prv;
    vocdef   *nxt;
    vocdef  **vp;
    vocwdef  *vw;
    vocwdef  *prvw;
    vocwdef  *nxtw;
    uint      nxtidx;
    uint      idx;
    int       deleted_vocdef;
    char     *wrd2;
    int       len1, len2;
    int       do_del;
    char     *orgwrd;

    /* parse the word if provided */
    orgwrd = wrd1;
    if (wrd1)
        voc_parse_words(&wrd1, &len1, &wrd2, &len2);
    
    /* go through each hash value looking for matching words */
    for (i = VOCHASHSIZ, vp = ctx->voccxhsh ; i ; ++vp, --i)
    {
        /* go through all words in this hash chain */
        for (prv = (vocdef *)0, v = *vp ; v ; v = nxt)
        {
            /* remember next word in hash chain */
            nxt = v->vocnxt;

            /* if this word doesn't match, skip it */
            if (wrd1)
            {
                /* compare the first word */
                if (v->voclen != len1
                    || memicmp((char *)v->voctxt, wrd1, (size_t)len1))
                {
                    prv = v;
                    continue;
                }

                /* if there's a second word, compare it as well */
                if (wrd2 && (v->vocln2 != len2
                             || memicmp((char *)v->voctxt + len1,
                                        wrd2, (size_t)len2)))
                {
                    prv = v;
                    continue;
                }
            }

            /* presume we're not going to delete this vocdef */
            deleted_vocdef = FALSE;
            
            /* go through all object relations for this word */
            for (prvw = 0, idx = v->vocwlst, vw = vocwget(ctx, idx) ; vw ;
                 vw = nxtw, idx = nxtidx)
            {
                /* remember next word in relation list */
                nxtidx = vw->vocwnxt;
                nxtw = vocwget(ctx, nxtidx);

                /*
                 *   figure out whether to delete this word, based on the
                 *   caller's specified operating mode 
                 */
                if (revert)
                {
                    /* if reverting, delete all new words */
                    do_del = (vw->vocwflg & VOCFNEW);

                    /* also, remove the DELETED flag if present */
                    vw->vocwflg &= ~VOCFDEL;
                }
                else
                {
                    /*
                     *   delete the word if the object number matches,
                     *   AND either we're not searching for a specific
                     *   vocabulary word (in which case wrd1 will be null)
                     *   or the word matches the vocabulary word we're
                     *   seeking (in which case the part of speech must
                     *   match) 
                     */
                    do_del = (vw->vocwobj == objn
                              && (wrd1 == 0 || vw->vocwtyp == prp));

                    /*
                     *   if we're not in really_delete mode, and the word
                     *   matches and it wasn't dynamically added at
                     *   run-time, simply mark it as deleted -- this will
                     *   allow it to be undeleted if the game is reverted
                     *   to RESTART conditions 
                     */
                    if (do_del && !really_delete && !(vw->vocwflg & VOCFNEW))
                    {
                        /* geneate undo for the operation */
                        if (keep_undo && orgwrd)
                            vocdusave_delwrd(ctx, objn, prp,
                                             vw->vocwflg, orgwrd);
                        
                        /* just mark the word for deletion, but keep vw */
                        vw->vocwflg |= VOCFDEL;
                        do_del = FALSE;
                    }
                }

                /* now delete the structure if we decided we should */
                if (do_del)
                {
                    /* geneate undo for the operation */
                    if (keep_undo && orgwrd)
                        vocdusave_delwrd(ctx, objn, prp, vw->vocwflg, orgwrd);
                                         
                    /* unlink this vocwdef from the vocdef's list */
                    if (prvw)
                        prvw->vocwnxt = vw->vocwnxt;
                    else
                        v->vocwlst = vw->vocwnxt;

                    /* link the vocwdef into the vocwdef free list */
                    vw->vocwnxt = ctx->voccxwfre;
                    ctx->voccxwfre = idx;

                    /*
                     *   if there's nothing left in the vocdef's list,
                     *   delete the entire vocdef as well 
                     */
                    if (v->vocwlst == VOCCXW_NONE)
                    {
                        if (prv) prv->vocnxt = v->vocnxt;
                        else *vp = v->vocnxt;
                    
                        /* link into free chain */
                        v->vocnxt = ctx->voccxfre;
                        ctx->voccxfre = v;

                        /* note that it's been deleted */
                        deleted_vocdef = TRUE;
                    }
                }
                else
                {
                    /* we're not deleting the word, so move prvw forward */
                    prvw = vw;
                }
            }

            /* if we didn't delete this vocdef, move prv forward onto it */
            if (!deleted_vocdef)
                prv = v;
        }
    }
}

/* delete all vocabulary for an object */
void vocdel(voccxdef *ctx, objnum objn)
{
    vocdel1(ctx, objn, (char *)0, (prpnum)0, TRUE, FALSE, FALSE);
}

/* delete object inheritance records for a particular object */
void vocidel(voccxdef *ctx, objnum obj)
{
    vocidef *v;
    
    /* get entry out of page table, and clear page table slot */
    v = vocinh(ctx, obj);
    vocinh(ctx, obj) = (vocidef *)0;
    
    /* link into free list */
    if (v)
    {
        v->vocinxt = ctx->voccxifr;
        ctx->voccxifr = v;
    }
}

/*
 *   Find template matching a verb+object+prep combination; return TRUE
 *   if a suitable template is found, FALSE otherwise.
 */
int voctplfnd(voccxdef *ctx, objnum verb_in, objnum prep,
              uchar *tplout, int *newstyle)
{
    uchar  *tplptr;
    uchar  *thistpl;
    int     found;
    int     tplcnt;
    uint    tplofs;
    objnum  verb;

    /* look for a new-style template first */
    tplofs = objgetap(ctx->voccxmem, verb_in, PRP_TPL2, &verb, FALSE);
    if (tplofs)
    {
        /* flag the presence of the new-style template */
        *newstyle = TRUE;
    }
    else
    {
        /* no new-style template - look for old version */
        tplofs = objgetap(ctx->voccxmem, verb_in, PRP_TPL, &verb, FALSE);
        *newstyle = FALSE;
    }

    /* inherit templates until we run out of them */
    for (;;)
    {
        /* if we found something already, use it */
        if (tplofs)
        {
            size_t siz;

            /* figure the size of this template style */
            siz = (*newstyle ? VOCTPL2SIZ : VOCTPLSIZ);
            
            /* lock the verb object, and get the property value pointer */
            tplptr  = mcmlck(ctx->voccxmem, verb);
            thistpl = prpvalp(tplptr + tplofs);
            
            /* first byte is number of templates in array */
            tplcnt = *thistpl++;
            
            /* look for a template that matches the preposition object */
            for (found = FALSE ; tplcnt ; thistpl += siz, --tplcnt)
            {
                if (voctplpr(thistpl) == prep)
                {
                    found = TRUE;
                    break;
                }
            }
            
            /* unlock the object and return the value if we found one */
            mcmunlck(ctx->voccxmem, verb);
            if (found)
            {
                memcpy(tplout, thistpl, siz);
                return(TRUE);
            }
        }

        /* try inheriting a template (new-style, then old-style) */
        tplofs = objgetap(ctx->voccxmem, verb_in, PRP_TPL2, &verb, TRUE);
        if (tplofs)
            *newstyle = TRUE;
        else
        {
            tplofs = objgetap(ctx->voccxmem, verb_in, PRP_TPL, &verb, TRUE);
            *newstyle = FALSE;
        }

        /* return not-found if we couldn't inherit it */
        if (!tplofs)
            return FALSE;

        /* use the newly found verb */
        verb_in = verb;
    }
}

/*
 *   Set the "Me" object 
 */
void voc_set_me(voccxdef *ctx, objnum new_me)
{
    /* save undo for the change */
    vocdusave_me(ctx, ctx->voccxme);

    /* set the new "Me" object */
    ctx->voccxme = new_me;
}

