Subversion Repositories eduke32

Rev

Rev 4336 | Blame | Compare with Previous | Last modification | View Log | RSS feed

//-------------------------------------------------------------------------
/*
Copyright (C) 2010 EDuke32 developers and contributors

This file is part of EDuke32.

EDuke32 is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 2
as published by the Free Software Foundation.

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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
*/

//-------------------------------------------------------------------------

#include "compat.h"
#include "build.h"
#include "editor.h"
#include "pragmas.h"

#include "baselayer.h"
#include "renderlayer.h"

#include "osd.h"
#include "cache1d.h"

#include "osdfuncs.h"
#include "names.h"

#include "common.h"
#include "common_game.h"
#include "mapster32.h"
#include "keys.h"

#include "keyboard.h"
#include "scriptfile.h"
#include "crc32.h"

#include "sounds_mapster32.h"
#include "fx_man.h"

#include "macros.h"
#include "lz4.h"

#include "m32script.h"
#include "m32def.h"

#ifdef LUNATIC
# include "lunatic_m32.h"
#endif

static const char *
    #include "rev.h"

#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <shellapi.h>
#endif

#include <signal.h>

// Workaround for namespace pollution in <sys/stat.h> introduced in MinGW 4.8.
#ifdef stat
# undef stat
#endif

static int32_t floor_over_floor;
static int32_t g_fillCurSector = 0;

static char g_modDir[BMAX_PATH];
static char levelname[BMAX_PATH];

// static char *startwin_labeltext = "Starting Mapster32...";
static char setupfilename[BMAX_PATH] = "mapster32.cfg";

int32_t fixmaponsave_sprites = 1;
static int32_t fixmaponsave_walls = 0;
static int32_t lastsave = -180*60;
static int32_t NoAutoLoad = 0;
static int32_t spnoclip=1;

static char *default_tiles_cfg = "tiles.cfg";
static int32_t pathsearchmode_oninit;

#ifdef LUNATIC
static L_State g_EmState;
#endif

#pragma pack(push,1)
sound_t g_sounds[MAXSOUNDS];
#pragma pack(pop)

static int16_t g_definedsndnum[MAXSOUNDS];  // maps parse order index to g_sounds index
static int16_t g_sndnum[MAXSOUNDS];  // maps current order index to g_sounds index
int32_t g_numsounds = 0;
static int32_t lastupdate, mousecol, mouseadd = 1, bstatus;

#if !defined(_WIN32)
static int32_t usecwd = 0;
#endif

char *scripthist[SCRIPTHISTSIZ];
int32_t scripthistend = 0;

int32_t g_lazy_tileselector = 0;
int32_t showambiencesounds=2;

int32_t autocorruptcheck = 0;
static int32_t corruptchecktimer;
static int32_t curcorruptthing=-1, corrupt_tryfix_alt=0;
int32_t corruptcheck_noalreadyrefd=0;

int32_t corruptlevel=0, numcorruptthings=0, corruptthings[MAXCORRUPTTHINGS];

static uint32_t templenrepquot=1;

//////////////////// Key stuff ////////////////////

#define eitherALT   (keystatus[KEYSC_LALT] || keystatus[KEYSC_RALT])
#define eitherCTRL  (keystatus[KEYSC_LCTRL] || keystatus[KEYSC_RCTRL])
#define eitherSHIFT (keystatus[KEYSC_LSHIFT] || keystatus[KEYSC_RSHIFT])

#define PRESSED_KEYSC(Key) (keystatus[KEYSC_##Key] && !(keystatus[KEYSC_##Key]=0))


//////////////////// Aiming ////////////////////
static const char *Typestr[] = { "Wall", "Ceiling", "Floor", "Sprite", "Wall" };
static const char *typestr[] = { "wall", "ceiling", "floor", "sprite", "wall" };
static const char *Typestr_wss[] = { "Wall", "Sector", "Sector", "Sprite", "Wall" };
/*static const char *typestr_wss[] = { "wall", "sector", "sector", "sprite", "wall" };*/

/** The following macros multiplex between identically named fields of sector/wall/sprite,
 * based on a macro parameter or the currently aimed at object (AIMED_ versions).
 * They can be used on either side of an assignment. */


// select wall, only makes a difference with walls that have 'swap bottom of walls' bit set
#define SELECT_WALL() (AIMING_AT_WALL ? searchbottomwall : searchwall)

#define SECFLD(i, Field)  (sector[i].Field)
#define WALFLD(i, Field)  (wall[i].Field)
#define SPRFLD(i, Field)  (sprite[i].Field)

// valid fields: z, stat, picnum, heinum, shade, pal, xpanning, ypanning
#define CEILINGFLOOR(iSec, Field) (*(AIMING_AT_CEILING ? &(sector[iSec].ceiling##Field) : &(sector[iSec].floor##Field)))
#define AIMED_CEILINGFLOOR(Field) CEILINGFLOOR(searchsector, Field)

#define AIMED_SEL_WALL(Field) WALFLD(SELECT_WALL(), Field)

// selects from wall proper or its mask
#define OVR_WALL(iWal, Field) (*(AIMING_AT_WALL ? &WALFLD(iWal, Field) : &(wall[iWal].over##Field)))
#define AIMED_SELOVR_WALL(Field) OVR_WALL(SELECT_WALL(), Field)

// the base macro to construct field multiplexing macros: wall and sector cases undetermined
#define MUXBASE(Field, SectorCase, WallCase) (*(AIMING_AT_CEILING_OR_FLOOR ? (SectorCase) : \
                                               (AIMING_AT_WALL_OR_MASK ? (WallCase) : \
                                                &SPRFLD(searchwall, Field))))


#define SFBASE_CF(Field, WallCase) MUXBASE(Field, &AIMED_CEILINGFLOOR(Field), WallCase)

#define SFBASE_(Field, WallCase) MUXBASE(Field, &SECFLD(searchsector,Field), WallCase)

#define AIMED(Field) SFBASE_(Field, &WALFLD(searchwall, Field))
#define AIMED_SEL(Field) SFBASE_(Field, &AIMED_SEL_WALL(Field))
//#define AIMED_CF(Field) SFBASE_CF(Field, &WALFLD(searchwall,Field))
#define AIMED_CF_SEL(Field) SFBASE_CF(Field, &AIMED_SEL_WALL(Field))

// OVR makes sense only with picnum
//#define AIMED_OVR_PICNUM  SFBASE_CF(picnum, &OVR_WALL(searchwall, picnum))
#define AIMED_SELOVR_PICNUM SFBASE_CF(picnum, &AIMED_SELOVR_WALL(picnum))


static const char *ONOFF_[] = {"OFF","ON"};
#define ONOFF(b) (ONOFF_[!!(b)])

// always CRLF for us
#ifdef _WIN32
# define OURNEWL "\n"
#else
# define OURNEWL "\r\n"
#endif

static int32_t tsign, mouseaction=0, mouseax=0, mouseay=0;
static int32_t repeatcountx, repeatcounty;
static int32_t infobox=3; // bit0: current window, bit1: mouse pointer, the variable should be renamed

static char wallshades[MAXWALLS];
static char sectorshades[MAXSECTORS][2];
static char spriteshades[MAXSPRITES];
static char wallpals[MAXWALLS];
static char sectorpals[MAXSECTORS][2];
static char spritepals[MAXSPRITES];
static uint8_t wallflag[MAXWALLS>>3];

#ifdef YAX_ENABLE
static const char *yupdownwall[2] = {"upwall","downwall"};
static const char *YUPDOWNWALL[2] = {"UPWALL","DOWNWALL"};

static uint8_t havebunch[YAX_MAXBUNCHES];
static int32_t *tempzar[YAX_MAXBUNCHES];

static void silentmessage(const char *fmt, ...);
static int32_t yax_invalidop()
{
    silentmessage("Operation forbidden on extended sector.");
    return 0;
}

static int32_t yax_invalidslope()
{
    silentmessage("Firstwalls must coincide for changing slope.");
    return 0;
}

// 1: ok
static int32_t yax_checkslope(int16_t sectnum, int32_t othersectnum)
{
    int16_t w1 = sector[sectnum].wallptr, w2 = wall[w1].point2;
    int16_t nw1 = sector[othersectnum].wallptr, nw2 = wall[nw1].point2;

    if (nw1 < 0)
        return 0;  // error

    nw2 = wall[nw1].point2;
    if (wall[w1].x != wall[nw1].x || wall[w1].y != wall[nw1].y ||
            wall[w2].x != wall[nw2].x || wall[w2].y != wall[nw2].y)
        return 0;

    return 1;
}

# define YAXSLOPECHK(s,os)  (yax_checkslope(s,os) || yax_invalidslope())
# define YAXCHK(p) ((p) || yax_invalidop())
#endif

// tile marking in tile selector for custom creation of tile groups
static uint8_t tilemarked[(MAXTILES+7)>>3];

#ifdef POLYMER
static int16_t spritelightid[MAXSPRITES];
_prlight *spritelightptr[MAXSPRITES];

static int32_t check_prlight_colors(int32_t i)
{
    return (sprite[i].xvel != spritelightptr[i]->color[0]) ||
        (sprite[i].yvel != spritelightptr[i]->color[1]) ||
        (sprite[i].zvel != spritelightptr[i]->color[2]);
}

static void copy_prlight_colors(_prlight *mylightptr, int32_t i)
{
    mylightptr->color[0] = sprite[i].xvel;
    mylightptr->color[1] = sprite[i].yvel;
    mylightptr->color[2] = sprite[i].zvel;
}

static void addprlight_common1(_prlight *mylightptr, int32_t i)
{
    mylightptr->sector = SECT;
    Bmemcpy(mylightptr, &sprite[i], sizeof(vec3_t));
    mylightptr->range = SHT;
    copy_prlight_colors(mylightptr, i);
    mylightptr->angle = SA;
    mylightptr->horiz = SH;
    mylightptr->minshade = sprite[i].xoffset;
    mylightptr->maxshade = sprite[i].yoffset;

    // overridden for spot lights
    mylightptr->radius = mylightptr->faderadius = mylightptr->tilenum = 0;

    if (CS & 2)
    {
        if (CS & 512)
            mylightptr->priority = PR_LIGHT_PRIO_LOW;
        else
            mylightptr->priority = PR_LIGHT_PRIO_HIGH;
    }
    else
        mylightptr->priority = PR_LIGHT_PRIO_MAX;

    mylightptr->publicflags.negative = !!(CS & 128);

    spritelightid[i] = polymer_addlight(mylightptr);
    if (spritelightid[i] >= 0)
        spritelightptr[i] = &prlights[spritelightid[i]];
}

static void DeletePolymerLights(void)
{
    int32_t i;
    for (i=0; i<MAXSPRITES; i++)
        if (spritelightptr[i] != NULL)
        {
            polymer_deletelight(spritelightid[i]);
            spritelightid[i] = -1;
            spritelightptr[i] = NULL;
        }
}

void G_Polymer_UnInit(void)
{
    DeletePolymerLights();
}
#endif

extern int32_t mskip;

//extern int32_t fillsector(int16_t sectnum, char fillcolor);

static void drawgradient(void)
{
    int32_t i, col = whitecol-21;
    begindrawing();
    for (i=ydim-STATUS2DSIZ+16; i<ydim && col>0; i++,col--)
        CLEARLINES2D(i, 1, (col<<24)|(col<<16)|(col<<8)|col);
    CLEARLINES2D(i, ydim-i, 0);
    enddrawing();
}

static void message_common1(const char *tmpstr)
{
    Bstrncpyz(getmessage, tmpstr, sizeof(getmessage));

    getmessageleng = Bstrlen(getmessage);
    getmessagetimeoff = totalclock + 120*2 + getmessageleng*(120/30);
//    lastmessagetime = totalclock;
}

void message(const char *fmt, ...)
{
    char tmpstr[256];
    va_list va;

    va_start(va, fmt);
    Bvsnprintf(tmpstr, 256, fmt, va);
    va_end(va);

    message_common1(tmpstr);

    if (!mouseaction)
        OSD_Printf("%s\n", tmpstr);
}

static void silentmessage(const char *fmt, ...)
{
    char tmpstr[256];
    va_list va;

    va_start(va, fmt);
    Bvsnprintf(tmpstr, 256, fmt, va);
    va_end(va);

    message_common1(tmpstr);
}


static int32_t osdcmd_quit(const osdfuncparm_t *parm);

////////// UNDO/REDO SYSTEM //////////
#if M32_UNDO
typedef struct mapundo_
{
    int32_t revision;
    int32_t num[3];  // numsectors, numwalls, numsprites

    // These exist temporarily as sector/wall/sprite data, but are compressed
    // most of the time.  +4 bytes refcount at the beginning.
    char *sws[3];  // sector, wall, sprite

    uint32_t crc[3];

    struct mapundo_ *next;  // 'redo' loads this
    struct mapundo_ *prev;  // 'undo' loads this
} mapundo_t;

mapundo_t *mapstate = NULL;

int32_t map_revision = 1;

#define QADDNSZ 400


static int32_t try_match_with_prev(int32_t idx, int32_t numsthgs, uint32_t crc)
{
    if (mapstate->prev && mapstate->prev->num[idx]==numsthgs && mapstate->prev->crc[idx]==crc)
    {
        // found match!
        mapstate->sws[idx] = mapstate->prev->sws[idx];
        (*(int32_t *)mapstate->sws[idx])++;  // increase refcount!

        return 1;
    }

    return 0;
}

static void create_compressed_block(int32_t idx, const void *srcdata, uint32_t size, uint32_t crc)
{
    uint32_t j;

    // allocate
    mapstate->sws[idx] = (char *)Bmalloc(4 + size + QADDNSZ);
    if (!mapstate->sws[idx]) { initprintf("OUT OF MEM in undo/redo\n"); osdcmd_quit(NULL); }

    // compress & realloc
    j = LZ4_compress((const char*)srcdata, mapstate->sws[idx]+4, size);
    mapstate->sws[idx] = (char *)Brealloc(mapstate->sws[idx], 4 + j);
    if (!mapstate->sws[idx]) { initprintf("COULD not realloc in undo/redo\n"); osdcmd_quit(NULL); }

    // write refcount
    *(int32_t *)mapstate->sws[idx] = 1;

    mapstate->crc[idx] = crc;
}

static void free_self_and_successors(mapundo_t *mapst)
{
    mapundo_t *cur = mapst;

    mapst->prev = NULL;  // break the back link

    while (cur->next)
        cur = cur->next;

    while (1)
    {
        int32_t i;
        mapundo_t *const prev = cur->prev;

        for (i=0; i<3; i++)
        {
            int32_t *const refcnt = (int32_t *)cur->sws[i];

            if (refcnt)
            {
                (*refcnt)--;
                if (*refcnt == 0)
                    Bfree(refcnt);  // free the block!
            }
        }

        Bfree(cur);

        if (!prev)
            break;

        cur = prev;
    }
}

// NOTE: only _consecutive_ matching (size+crc) sector/wall/sprite blocks are
// shared!
void create_map_snapshot(void)
{
    if (mapstate == NULL)
    {
        // create initial mapstate

        map_revision = 1;

        mapstate = (mapundo_t *)Bcalloc(1, sizeof(mapundo_t));
        mapstate->revision = map_revision;
        mapstate->prev = mapstate->next = NULL;
    }
    else
    {
        if (mapstate->next)
            free_self_and_successors(mapstate->next);
        // now, have no successors

        // calloc because not everything may be set in the following:
        mapstate->next = (mapundo_t *)Bcalloc(1, sizeof(mapundo_t));
        mapstate->next->prev = mapstate;

        mapstate = mapstate->next;

        mapstate->revision = ++map_revision;
    }


    fixspritesectors();

    mapstate->num[0] = numsectors;
    mapstate->num[1] = numwalls;
    mapstate->num[2] = Numsprites;


    if (numsectors)
    {
        int32_t j;
        uint32_t tempcrc = crc32once((uint8_t *)sector, numsectors*sizeof(sectortype));

        if (!try_match_with_prev(0, numsectors, tempcrc))
            create_compressed_block(0, sector, numsectors*sizeof(sectortype), tempcrc);

        if (numwalls)
        {
            tempcrc = crc32once((uint8_t *)wall, numwalls*sizeof(walltype));

            if (!try_match_with_prev(1, numwalls, tempcrc))
                create_compressed_block(1, wall, numwalls*sizeof(walltype), tempcrc);
        }

        if (Numsprites)
        {
            tempcrc = crc32once((uint8_t *)sprite, MAXSPRITES*sizeof(spritetype));

            if (!try_match_with_prev(2, Numsprites, tempcrc))
            {
                int32_t i = 0;
                spritetype *const tspri = (spritetype *)Bmalloc(Numsprites*sizeof(spritetype) + 4);
                spritetype *spri = tspri;

                if (!tspri) { initprintf("OUT OF MEM in undo/redo (2)\n"); osdcmd_quit(NULL); }

                for (j=0; j<MAXSPRITES && i < Numsprites; j++)
                    if (sprite[j].statnum != MAXSTATUS)
                    {
                        Bmemcpy(spri++, &sprite[j], sizeof(spritetype));
                        i++;
                    }

                create_compressed_block(2, tspri, Numsprites*sizeof(spritetype), tempcrc);
                Bfree(tspri);
            }
        }
    }

    CheckMapCorruption(5, 0);
}

void map_undoredo_free(void)
{
    if (mapstate)
    {
        free_self_and_successors(mapstate);
        mapstate = NULL;
    }

    map_revision = 1;
}

int32_t map_undoredo(int32_t dir)
{
    int32_t i;

    if (mapstate == NULL) return 1;

    if (dir)
    {
        if (mapstate->next == NULL || !mapstate->next->num[0]) return 1;

        //        while (map_revision+1 != mapstate->revision && mapstate->next)
        mapstate = mapstate->next;
    }
    else
    {
        if (mapstate->prev == NULL || !mapstate->prev->num[0]) return 1;

        //        while (map_revision-1 != mapstate->revision && mapstate->prev)
        mapstate = mapstate->prev;
    }

    numsectors = mapstate->num[0];
    numwalls = mapstate->num[1];
    map_revision = mapstate->revision;

    Bmemset(show2dsector, 0, sizeof(show2dsector));

    reset_highlightsector();
    reset_highlight();

    initspritelists();

    if (mapstate->num[0])
    {
        // restore sector[]
        LZ4_decompress_fast(mapstate->sws[0]+4, (char*)sector, numsectors*sizeof(sectortype));

        if (mapstate->num[1])  // restore wall[]
            LZ4_decompress_fast(mapstate->sws[1]+4, (char*)wall, numwalls*sizeof(walltype));

        if (mapstate->num[2])  // restore sprite[]
            LZ4_decompress_fast(mapstate->sws[2]+4, (char*)sprite, (mapstate->num[2])*sizeof(spritetype));
    }

    // insert sprites
    for (i=0; i<mapstate->num[2]; i++)
    {
        if ((sprite[i].cstat & 48) == 48) sprite[i].cstat &= ~48;
        Bassert((unsigned)sprite[i].sectnum < (unsigned)numsectors
                   && (unsigned)sprite[i].statnum < MAXSTATUS);
        insertsprite(sprite[i].sectnum, sprite[i].statnum);
    }

    Bassert(Numsprites == mapstate->num[2]);

#ifdef POLYMER
    if (in3dmode() && getrendermode() == REND_POLYMER)
        polymer_loadboard();
#endif
#ifdef YAX_ENABLE
    yax_update(0);
    yax_updategrays(pos.z);
#endif
    CheckMapCorruption(4, 0);

    return 0;
}
#endif


#define M32_NUM_SPRITE_MODES ((signed)(sizeof(SpriteMode)/sizeof(SpriteMode[0])))
static const char *SpriteMode[]=
{
    "NONE",
    "SECTORS",
    "WALLS",
    "SPRITES",
    "ALL",
    "ITEMS ONLY",
    "CURRENT SPRITE ONLY",
    "ONLY SECTOREFFECTORS AND SECTORS",
    "NO SECTOREFFECTORS OR SECTORS"
};

#define MAXSKILL 5
static const char *SKILLMODE[MAXSKILL]=
{
    "Actor skill display: PIECE OF CAKE",
    "Actor skill display: LET'S ROCK",
    "Actor skill display: COME GET SOME",
    "Actor skill display: DAMN I'M GOOD",
    "Actor skill display: ALL SKILL LEVELS"
};

#define MAXNOSPRITES 4
static const char *SPRDSPMODE[MAXNOSPRITES]=
{
    "Sprite display: DISPLAY ALL SPRITES",
    "Sprite display: NO EFFECTORS",
    "Sprite display: NO ACTORS",
    "Sprite display: NO EFFECTORS OR ACTORS"
};

#define MAXHELP3D ((signed)(sizeof(Help3d)/sizeof(Help3d[0])))
static const char *Help3d[]=
{
    "Mapster32 3D mode help",
    " ",
    " F2 = TOGGLE CLIPBOARD",
    " F3 = TOGGLE MOUSELOOK",
    " F4 = TOGGLE AMBIENT SOUNDS",
    " F6 = AUTOMATIC SECTOREFFECTOR HELP",
    " F7 = AUTOMATIC SECTOR TAG HELP",
    "",
    " ' A = TOGGLE AUTOSAVE",
    " ' D = CYCLE SPRITE SKILL DISPLAY",
    " ' R = TOGGLE FRAMERATE DISPLAY",
    " ' W = TOGGLE SPRITE DISPLAY",
    " ' X = MAP SHADE PREVIEW",
    " ' I = TOGGLE INVISIBLE SPRITES",
    "",
    " ' T = CHANGE LOTAG",
    " ' H = CHANGE HITAG",
    " ' S = CHANGE SHADE",
    " ' M = CHANGE EXTRA",
    " ' V = CHANGE VISIBILITY",
    " ' L = CHANGE OBJECT COORDINATES",
    " ' C = CHANGE GLOBAL SHADE",
    "",
    " ' ENTER = PASTE GRAPHIC ONLY",
    " ' P & ; P = PASTE PALETTE TO ALL SELECTED SECTORS",
    " ; V = SET VISIBILITY ON ALL SELECTED SECTORS",
    " ' DEL = CSTAT=0",
    " CTRL-S = SAVE BOARD",
    " HOME = PGUP/PGDN MODIFIER (256 UNITS)",
    " END = PGUP/PGDN MODIFIER (512 UNITS)",
};

int32_t kopen4loadfrommod(const char *filename, char searchfirst)
{
    static char fn[BMAX_PATH];
    int32_t r=-1;

    if (g_modDir[0])
    {
        Bsnprintf(fn,sizeof(fn),"%s/%s",g_modDir,filename);
        r = kopen4load(fn,searchfirst);
    }

    if (r < 0)
        r = kopen4load(filename,searchfirst);

    return r;
}

const char *ExtGetVer(void)
{
    return s_buildRev;
}

void ExtSetupMapFilename(const char *mapname)
{
    Bstrcpy(levelname, mapname);

    Bsprintf(tempbuf, "Mapster32 - %s", mapname);
    wm_setapptitle(tempbuf);
}

void ExtLoadMap(const char *mapname)
{
    getmessageleng = 0;
    getmessagetimeoff = 0;

    ExtSetupMapFilename(mapname);

    // Old-fashioned multi-psky handling setup.
    G_SetupGlobalPsky();

    parallaxtype = 0;

    //////////
#if M32_UNDO
    map_undoredo_free();
#endif

    VM_OnEvent(EVENT_LOADMAP, -1);
}

void ExtSaveMap(const char *mapname)
{
    UNREFERENCED_PARAMETER(mapname);
    saveboard("backup.map", &pos, ang, cursectnum);

    VM_OnEvent(EVENT_SAVEMAP, -1);
}


////////// tag labeling system //////////

typedef struct
{
    hashtable_t hashtab;
    char *label[32768];
    int32_t numlabels;
} taglab_t;

static taglab_t g_taglab;

static void tstrtoupper(char *s)
{
    int32_t i;
    for (i=0; s[i]; i++)
        s[i] = Btoupper(s[i]);
}

void taglab_init()
{
    int32_t i;

    g_taglab.numlabels = 0;
    g_taglab.hashtab.size = 16384;
    hash_init(&g_taglab.hashtab);

    for (i=0; i<32768; i++)
    {
        if (g_taglab.label[i])
            Bfree(g_taglab.label[i]);
        g_taglab.label[i] = NULL;
    }
}

int32_t taglab_load(const char *filename, int32_t flags)
{
    int32_t fil, len, i;
    char buf[BMAX_PATH], *dot, *filebuf;

    taglab_init();

    len = Bstrlen(filename);
    if (len >= BMAX_PATH-1)
        return -1;
    Bmemcpy(buf, filename, len+1);

    //
    dot = Bstrrchr(buf, '.');
    if (!dot)
        dot = &buf[len];

    if (dot-buf+8 >= BMAX_PATH)
        return -1;
    Bmemcpy(dot, ".maptags", 9);
    //

    if ((fil = kopen4load(buf,flags)) == -1)
        return -1;

    len = kfilelength(fil);

    filebuf = (char *)Bmalloc(len+1);
    if (!filebuf)
    {
        kclose(fil);
        return -1;
    }

    kread(fil, filebuf, len);
    filebuf[len] = 0;
    kclose(fil);

    // ----

    {
        int32_t tag;
        char *cp=filebuf, *bp, *ep;

        while (1)
        {
#define XTAGLAB_STRINGIFY(X) TAGLAB_STRINGIFY(X)
#define TAGLAB_STRINGIFY(X) #X
            i = sscanf(cp, "%d %" XTAGLAB_STRINGIFY(TAGLAB_MAX) "s", &tag, buf);
#undef XTAGLAB_STRINGIFY
#undef TAGLAB_STRINGIFY
            if (i != 2 || !buf[0] || tag<0 || tag>=32768)
                goto nextline;

            buf[TAGLAB_MAX-1] = 0;

            i = Bstrlen(buf);
            bp = buf; while (*bp && isspace(*bp)) bp++;
            ep = &buf[i-1]; while (ep>buf && isspace(*ep)) ep--;
            ep++;

            if (!(ep > bp))
                goto nextline;
            *ep = 0;

            taglab_add(bp, tag);
//initprintf("add tag %s:%d\n", bp, tag);
nextline:
            while (*cp && *cp!='\n')
                cp++;
            while (*cp=='\r' || *cp=='\n')
                cp++;
            if (*cp == 0)
                break;
        }
    }

    // ----
    Bfree(filebuf);

    return 0;
}

int32_t taglab_save(const char *mapname)
{
    int32_t fil, len, i;
    char buf[BMAX_PATH], *dot;
    const char *label;

    if (g_taglab.numlabels==0)
        return 1;

    Bstrncpyz(buf, mapname, BMAX_PATH);

    len = Bstrlen(buf);
    //
    dot = Bstrrchr(buf, '.');
    if (!dot)
        dot = &buf[len];

    if (dot-buf+8 >= BMAX_PATH)
        return -1;
    Bmemcpy(dot, ".maptags", 9);
    //

    if ((fil = Bopen(buf,BO_BINARY|BO_TRUNC|BO_CREAT|BO_WRONLY,BS_IREAD|BS_IWRITE)) == -1)
    {
        initprintf("Couldn't open \"%s\" for writing: %s\n", buf, strerror(errno));
        return -1;
    }

    for (i=0; i<32768; i++)
    {
        label = taglab_getlabel(i);
        if (!label)
            continue;

        len = Bsprintf(buf, "%d %s" OURNEWL, i, label);
        if (Bwrite(fil, buf, len)!=len)
            break;
    }

    Bclose(fil);

    return (i!=32768);
}

int32_t taglab_add(const char *label, int16_t tag)
{
    const char *otaglabel;
    char buf[TAGLAB_MAX];
    int32_t olabeltag, diddel=0;

    if (tag < 0)
        return -1;

    Bstrncpyz(buf, label, sizeof(buf));
    // upcase the tag for storage and comparison
    tstrtoupper(buf);

    otaglabel = g_taglab.label[tag];
    if (otaglabel)
    {
        if (!Bstrcasecmp(otaglabel, buf))
            return 0;

//        hash_delete(&g_taglab.hashtab, g_taglab.label[tag]);

        // a label having the same tag number as 'tag' is deleted
        Bfree(g_taglab.label[tag]);
        g_taglab.label[tag] = NULL;
        diddel |= 1;
    }
    else
    {
        olabeltag = hash_findcase(&g_taglab.hashtab, buf);
        if (olabeltag==tag)
            return 0;

        if (olabeltag>=0)
        {
            // the label gets assigned to a new tag number ('tag deleted')
            Bfree(g_taglab.label[olabeltag]);
            g_taglab.label[olabeltag] = NULL;
            diddel |= 2;
        }
    }

    if (!diddel)
        g_taglab.numlabels++;
    g_taglab.label[tag] = Bstrdup(buf);
//initprintf("added %s %d to hash\n", g_taglab.label[tag], tag);
    hash_add(&g_taglab.hashtab, g_taglab.label[tag], tag, 1);

    return diddel;
}

const char *taglab_getlabel(int16_t tag)
{
    if (tag < 0)  // || tag>=32768 implicitly
        return NULL;

    return g_taglab.label[tag];
}

int32_t taglab_gettag(const char *label)
{
    char buf[TAGLAB_MAX];

    Bstrncpyz(buf, label, TAGLAB_MAX);

    // need to upcase since hash_findcase doesn't work as expected:
    // getting the code is still (necessarily) case-sensitive...
    tstrtoupper(buf);

    return hash_findcase(&g_taglab.hashtab, buf);
}

#define TLCHAR "+"
#define TLCHR(Cond) ((Cond)?TLCHAR:"")
static uint64_t taglab_nolink_SEs = (1ull<<10)|(1ull<<27)|(1ull<<28)|(1ull<<29)|
    (1ull<<31)|(1ull<<32)|(1ull<<49)|(1ull<<50);

// Whether the individual tags have linking semantics. Based on
//  http://infosuite.duke4.net/index.php?page=references_special_textures
// The return value is an OR of the following:
//  1: lotag has linking semantics
//  2: hitag
//  4: extra
//  8: xvel
//  16: yvel
//  32: zvel
//  64: owner
// This function is only supposed to say something about the potential of a tag:
// it will also 'say yes' if a particular tag is zero.
int32_t taglab_linktags(int32_t spritep, int32_t num)
{
    int32_t picnum;
    int32_t l, link = 0;

    if (spritep)
        picnum = sprite[num].picnum;
    else
        picnum = wall[num].picnum;

    if (spritep)
    {
        switch (picnum)
        {
        case SECTOREFFECTOR:
            // SEs potentially link by their hitag
            l = sprite[num].lotag;
            if (l>=0 && l<=63 && (taglab_nolink_SEs&(1ull<<l)))
                break;
            link = 2;
            break;

            // various lotag-linkers
        case ACTIVATOR: case TOUCHPLATE: case ACTIVATORLOCKED: case MASTERSWITCH:
        case RESPAWN:  // ---
        case ACCESSSWITCH: case ACCESSSWITCH2:
        case MULTISWITCH:  // *
        case DIPSWITCH: case TECHSWITCH: case ALIENSWITCH: case TARGET: case DUCK:
        case REACTOR:
        case CAMERA1:
            link = 1;
            break;

            // various hitag-linkers
        case VIEWSCREEN2: case VIEWSCREEN:
        case CRACK1: case CRACK2: case CRACK3: case CRACK4: case FIREEXT:
        case FEM1: case FEM2: case FEM3: case FEM4: case FEM5: case FEM6:
        case FEM7: case FEM8: case FEM9: case FEM10: case PODFEM1: case NAKED1: //case STATUE: //?
        case SEENINE: case OOZFILTER:
        case CRANEPOLE: case CRANE:
        case NATURALLIGHTNING:
            link = 2;
            break;
        }
    }
    else  // walls
    {
#ifdef YAX_ENABLE
        if (yax_getnextwall(num, YAX_CEILING) < 0)
#endif
        switch (picnum)
        {
        case TECHLIGHT2: case TECHLIGHT4: case WALLLIGHT4:
        case WALLLIGHT3: case WALLLIGHT1: case WALLLIGHT2:
        case BIGFORCE: case W_FORCEFIELD:
            link = 1;
            break;
        }
    }

    if (!link)
#ifdef YAX_ENABLE
    if (spritep || yax_getnextwall(num, YAX_CEILING) < 0)
#endif
    {
        // try a few that work both as sprites and as walls
        switch (picnum)
        {
        case ACCESSSWITCH: case SLOTDOOR: case LIGHTSWITCH: case SPACEDOORSWITCH:
        case SPACELIGHTSWITCH: case FRANKENSTINESWITCH: case MULTISWITCH:
        case DIPSWITCH: case DIPSWITCH2: case TECHSWITCH: case DIPSWITCH3:
        case ACCESSSWITCH2: case LIGHTSWITCH2: case POWERSWITCH1: case LOCKSWITCH1:
        case POWERSWITCH2: case HANDSWITCH: case PULLSWITCH: case ALIENSWITCH:  // ---
        case DOORTILE5: case DOORTILE6: case DOORTILE1: case DOORTILE2: case DOORTILE3:
        case DOORTILE4: case DOORTILE7: case DOORTILE8: case DOORTILE9: case DOORTILE10:
        case DOORTILE22: case DOORTILE18: case DOORTILE19: case DOORTILE20:
        case DOORTILE14: case DOORTILE16: case DOORTILE15: case DOORTILE21:
        case DOORTILE17: case DOORTILE11: case DOORTILE12: case DOORTILE23:  // ---
            link = 1;
            break;
        }
    }

    g_iReturnVar = link;
    VM_OnEvent(EVENT_LINKTAGS, spritep?num:-1);
    link = g_iReturnVar;

    return link;
}

int32_t taglab_getnextfreetag(void)
{
    int32_t i, nextfreetag=1;

    for (i=0; i<MAXSPRITES; i++)
    {
        int32_t tag;

        if (sprite[i].statnum == MAXSTATUS)
            continue;

        if (sprite[i].picnum==MULTISWITCH)
        {
            // MULTISWITCH needs special care
            int32_t endtag = sprite[i].lotag+3;
            if (nextfreetag <= endtag)
                nextfreetag = endtag+1;
            continue;
        }

        tag = select_sprite_tag(i);

        if (tag != INT32_MIN && nextfreetag <= tag)
            nextfreetag = tag+1;
    }

    for (i=0; i<numwalls; i++)
    {
        int32_t lt = taglab_linktags(0, i);

        if ((lt&1) && nextfreetag <= wall[i].lotag)
            nextfreetag = wall[i].lotag+1;
        if ((lt&2) && nextfreetag <= wall[i].hitag)
            nextfreetag = wall[i].hitag+1;        
    }

    if (nextfreetag < 32768)
        return nextfreetag;

    return 0;
}


static void taglab_handle1(int32_t linktagp, int32_t tagnum, char *buf)
{
    const char *label = NULL;
    if (linktagp && showtags==2)
        label = taglab_getlabel(tagnum);

    if (label)
        Bsprintf(buf, "%d<%s>", tagnum, label);
    else
        Bsprintf(buf, "%d%s", tagnum, TLCHR(linktagp));
}
////////// end tag labeling system //////////


static int32_t getTileGroup(const char *groupName)
{
    int32_t temp;
    for (temp = 0; temp < MAX_TILE_GROUPS; temp++)
    {
        if (s_TileGroups[temp].szText == NULL)
            return -1;

        if (!Bstrcmp(s_TileGroups[temp].szText, groupName))
            return temp;
    }
    return -1;
}

static int32_t tileInGroup(int32_t group, int32_t tilenum)
{
    // @todo Make a bitmap instead of doing this slow search..
    int32_t temp;
    if (group < 0 || group >= MAX_TILE_GROUPS || s_TileGroups[group].szText == NULL)
    {
        // group isn't valid.
        return 0;
    }
    for (temp=0; temp<s_TileGroups[group].nIds; temp++)
    {
        if (tilenum == s_TileGroups[group].pIds[temp])
            return 1;
    }
    return 0;
}

const char *ExtGetSectorType(int32_t lotag)
{
    switch (lotag)
    {
    case 1: return "WATER (SE 7)";
    case 2: return "UNDERWATER (SE 7)";
    case 9: return "STAR TREK DOORS";
    case 15: return "ELEVATOR TRANSPORT (SE 17)";
    case 16: return "ELEVATOR PLATFORM DOWN";
    case 17: return "ELEVATOR PLATFORM UP";
    case 18: return "ELEVATOR DOWN";
    case 19: return "ELEVATOR UP";
    case 20: return "CEILING DOOR";
    case 21: return "FLOOR DOOR";
    case 22: return "SPLIT DOOR";
    case 23: return "SWING DOOR (SE 11)";
    case 25: return "SLIDE DOOR (SE 15)";
    case 26: return "SPLIT STAR TREK DOOR";
    case 27: return "BRIDGE (SE 20)";
    case 28: return "DROP FLOOR (SE 21)";
    case 29: return "TEETH DOOR (SE 22)";
    case 30: return "ROTATE RISE BRIDGE";
    case 31: return "2 WAY TRAIN (SE=30)";
    case 32767: return "SECRET ROOM";
    case -1: return "END OF LEVEL";
    default:
        if (lotag > 10000 && lotag < 32767)
            return "1 TIME SOUND";
//        else Bsprintf(tempbuf,"%hu",lotag);
    }

    return "";
}

const char *ExtGetSectorCaption(int16_t sectnum)
{
    static char tempbuf[64];

    Bmemset(tempbuf, 0, sizeof(tempbuf));

    if (!in3dmode() && ((onnames!=1 && onnames!=4 && onnames!=7) || onnames==8))
        return tempbuf;

    if (in3dmode() || (sector[sectnum].lotag|sector[sectnum].hitag))
    {
        Bstrcpy(lo, ExtGetSectorType(sector[sectnum].lotag));
        if (!in3dmode())
            Bsprintf_nowarn(tempbuf,"%hu,%hu %s", TrackerCast(sector[sectnum].hitag), TrackerCast(sector[sectnum].lotag), lo);
        else
            Bsprintf_nowarn(tempbuf,"%hu %s", TrackerCast(sector[sectnum].lotag), lo);
    }
    return(tempbuf);
}

const char *ExtGetWallCaption(int16_t wallnum)
{
    static char tempbuf[64];

    Bmemset(tempbuf,0,sizeof(tempbuf));

    if (wall[wallnum].cstat & (1<<14))
    {
        Bsprintf(tempbuf,"%d", wallength(wallnum));
        wall[wallnum].cstat &= ~(1<<14);
        return(tempbuf);
    }

    if (!(onnames==2 || onnames==4))
    {
        tempbuf[0] = 0;
        return(tempbuf);
    }

    // HERE

    if ((wall[wallnum].lotag|wall[wallnum].hitag) == 0)
        tempbuf[0] = 0;
    else
    {
        int32_t lt = taglab_linktags(0, wallnum);
        char histr[TAGLAB_MAX+16], lostr[TAGLAB_MAX+16];

        lt &= ~(int)(wall[wallnum].lotag<=0);
        lt &= ~(int)((wall[wallnum].hitag<=0)<<1);

        taglab_handle1(lt&2, wall[wallnum].hitag, histr);

#ifdef YAX_ENABLE__COMPAT
        if (yax_getnextwall(wallnum, YAX_CEILING) >= 0)  // ceiling nextwall: lotag
        {
            if (wall[wallnum].hitag == 0)
                tempbuf[0] = 0;
            else
                Bsprintf(tempbuf, "%s,*", histr);
        }
        else
#endif
        {
            taglab_handle1(lt&1, wall[wallnum].lotag, lostr);
            Bsprintf(tempbuf, "%s,%s", histr, lostr);
        }
    }

    return(tempbuf);
} //end

const char *SectorEffectorTagText(int32_t lotag)
{
    static char tempbuf[64];

    static const char *tags[] =
    {
        "ROTATED SECTOR",                // 0
        "PIVOT SPRITE FOR SE 0",
        "EARTHQUAKE",
        "RANDOM LIGHTS AFTER SHOT OUT",
        "RANDOM LIGHTS",
        "(UNKNOWN)",                     // 5
        "SUBWAY",
        "TRANSPORT",
        "UP OPEN DOOR LIGHTS",
        "DOWN OPEN DOOR LIGHTS",
        "DOOR AUTO CLOSE (H=DELAY)",     // 10
        "ROTATE SECTOR DOOR",
        "LIGHT SWITCH",
        "EXPLOSIVE",
        "SUBWAY CAR",
        "SLIDE DOOR (ST 25)",            // 15
        "ROTATE REACTOR SECTOR",
        "ELEVATOR TRANSPORT (ST 15)",
        "INCREMENTAL SECTOR RISE/FALL",
        "CEILING FALL ON EXPLOSION",
        "BRIDGE (ST 27)",                // 20
        "DROP FLOOR (ST 28)",
        "TEETH DOOR (ST 29)",
        "1-WAY SE7 DESTINATION (H=SE 7)",
        "CONVEYER BELT",
        "ENGINE",                        // 25
        "(UNKNOWN)",
        "CAMERA FOR PLAYBACK",
        "LIGHTNING (H=TILE#4890)",
        "FLOAT",
        "2 WAY TRAIN (ST=31)",           // 30
        "FLOOR RISE/FALL",
        "CEILING RISE/FALL",
        "SPAWN JIB W/QUAKE",
    };

    Bmemset(tempbuf,0,sizeof(tempbuf));

    if (lotag>=0 && lotag<(int32_t)(sizeof(tags)/sizeof(tags[0])))
        Bsprintf(tempbuf, "%d: %s", lotag, tags[lotag]);
    else
        switch (lotag)
        {
        case 36:
            Bsprintf(tempbuf,"%d: SHOOTER",lotag);
            break;
        case 49:
            Bsprintf(tempbuf,"%d: POINT LIGHT",lotag);
            break;
        case 50:
            Bsprintf(tempbuf,"%d: SPOTLIGHT",lotag);
            break;
        default:
            Bsprintf(tempbuf,"%d: (UNKNOWN)",lotag);
            break;
        }

    return (tempbuf);
}

const char *MusicAndSFXTagText(int32_t lotag)
{
    static char tempbuf[16];

    Bmemset(tempbuf, 0, sizeof(tempbuf));

    if (g_numsounds <= 0)
        return tempbuf;

    if (lotag>0 && lotag<999 && g_sounds[lotag].definedname)
        return g_sounds[lotag].definedname;

    if (lotag>=1000 && lotag<2000)
        Bsprintf(tempbuf, "REVERB");
    return tempbuf;
}

const char *SectorEffectorText(int32_t spritenum)
{
    static char tempbuf[64];

    Bmemset(tempbuf, 0, sizeof(tempbuf));
    Bmemset(lo, 0, sizeof(lo));

    Bstrcpy(lo, SectorEffectorTagText(sprite[spritenum].lotag));
    if (!lo[5]) // tags are 5 chars or less
        SpriteName(spritenum, tempbuf);
    else
        Bsprintf(tempbuf, "SE %s",lo);

    return (tempbuf);
}

const char *ExtGetSpriteCaption(int16_t spritenum)
{
    static char tempbuf[1024];
    int32_t retfast = 0, lt;

    Bmemset(tempbuf,0,sizeof(tempbuf));

    if (!(onnames>=3 && onnames<=8) || (onnames==7 && sprite[spritenum].picnum!=SECTOREFFECTOR))
        retfast = 1;
    if (onnames==5 && !tileInGroup(tilegroupItems, sprite[spritenum].picnum))
        retfast = 1;
    if (onnames==6 && sprite[spritenum].picnum != sprite[cursprite].picnum)
        retfast = 1;

    if (retfast)
        return tempbuf;

    lt = taglab_linktags(1, spritenum);
    lt &= ~(int)(sprite[spritenum].lotag<=0);
    lt &= ~(int)((sprite[spritenum].hitag<=0)<<1);

    if ((sprite[spritenum].lotag|sprite[spritenum].hitag) == 0)
    {
        SpriteName(spritenum,lo);
        if (lo[0]!=0)
        {
            if (sprite[spritenum].pal==1)
                Bsprintf(tempbuf,"%s (MULTIPLAYER)",lo);
            else
                Bsprintf(tempbuf,"%s",lo);
        }

        return tempbuf;
    }

    {
        char histr[TAGLAB_MAX+16], lostr[TAGLAB_MAX+16];

        taglab_handle1(lt&2, sprite[spritenum].hitag, histr);

        if (sprite[spritenum].picnum==SECTOREFFECTOR)
        {
            if (onnames!=8)
            {
                Bsprintf(lo,"%s",SectorEffectorText(spritenum));
                Bsprintf(tempbuf,"%s, %s",lo, histr);
            }
        }
        else
        {
            taglab_handle1(lt&1, sprite[spritenum].lotag, lostr);

            SpriteName(spritenum,lo);
            if (sprite[spritenum].extra != -1)
                Bsprintf_nowarn(tempbuf,"%s,%s,%d %s", histr, lostr, TrackerCast(sprite[spritenum].extra), lo);
            else
                Bsprintf(tempbuf,"%s,%s %s", histr, lostr, lo);
        }
    }

    return tempbuf;

} //end

//printext16 parameters:
//printext16(int32_t xpos, int32_t ypos, int16_t col, int16_t backcol,
//           char name[82], char fontsize)
//  xpos 0-639   (top left)
//  ypos 0-479   (top left)
//  col 0-15
//  backcol 0-15, -1 is transparent background
//  name
//  fontsize 0=8*8, 1=3*5

//drawline16 parameters:
// drawline16(int32_t x1, int32_t y1, int32_t x2, int32_t y2, char col)
//  x1, x2  0-639
//  y1, y2  0-143  (status bar is 144 high, origin is top-left of STATUS BAR)
//  col     0-15

static void PrintStatus(const char *string, int32_t num, int32_t x, int32_t y, int32_t color)
{
    Bsprintf(tempbuf, "%s %d", string, num);
    printext16(x*8, ydim-STATUS2DSIZ+y*8, editorcolors[color], -1, tempbuf, 0);
}

void ExtShowSectorData(int16_t sectnum)   //F5
{
    int32_t x,x2,y;
    int32_t i,yi;
    int32_t secrets=0;
    int32_t totalactors1=0,totalactors2=0,totalactors3=0,totalactors4=0;
    int32_t totalrespawn=0;

    UNREFERENCED_PARAMETER(sectnum);
    if (in3dmode())
        return;

    for (i=0; i<numsectors; i++)
        secrets += (sector[i].lotag==32767);

    for (i=headspritestat[0]; i != -1; i=nextspritestat[i])
    {
        // Count all non-player actors.
        if (tileInGroup(tilegroupActors, sprite[i].picnum))
        {
            if (sprite[i].lotag<=1) totalactors1++;
            if (sprite[i].lotag<=2) totalactors2++;
            if (sprite[i].lotag<=3) totalactors3++;
            if (sprite[i].lotag<=4) totalactors4++;
        }

        if (sprite[i].picnum == RESPAWN)
            totalrespawn++;
    }

    Bmemset(numsprite, 0, sizeof(numsprite));
    Bmemset(multisprite, 0, sizeof(numsprite));

    for (i=0; i<MAXSPRITES; i++)
    {
        if (sprite[i].statnum==0 && sprite[i].picnum>=0 && sprite[i].picnum<MAXTILES)
        {
            if (sprite[i].pal!=0)
                multisprite[sprite[i].picnum]++;
            else
                numsprite[sprite[i].picnum]++;
        }
    }

    clearmidstatbar16();             //Clear middle of status bar

    ydim -= 8;
    drawgradient();
    ydim += 8;

    printmessage16("Level %s next tag %d", levelname, taglab_getnextfreetag());

#define PRSTAT(Str, Tiledef) \
    PrintStatus(Str, numsprite[Tiledef], x, y+yi, numsprite[Tiledef]?11:7); \
    PrintStatus("",multisprite[Tiledef], x2,y+yi, multisprite[Tiledef]?9:7); \
    yi++;


    ydim -= 8; // vvvvvv reset at end!!

    begindrawing();  //{{{

    x=1; x2=14;
    y=4; yi=2;
    printext16(x*8, ydim-STATUS2DSIZ+y*8, editorcolors[11], -1, "Item Count", 0);

    PRSTAT("10%health=", COLA);
    PRSTAT("30%health=", SIXPAK);
    PRSTAT("Med-Kit  =", FIRSTAID);
    PRSTAT("Atom     =", ATOMICHEALTH);
    PRSTAT("Shields  =", SHIELD);

    x=17; x2=30;
    y=4; yi=2;
    printext16(x*8, ydim-STATUS2DSIZ+y*8, editorcolors[11], -1, "Inventory", 0);

    PRSTAT("Steroids =", STEROIDS);
    PRSTAT("Airtank  =", AIRTANK);
    PRSTAT("Jetpack  =", JETPACK);
    PRSTAT("Goggles  =", HEATSENSOR);
    PRSTAT("Boots    =", BOOTS);
    PRSTAT("HoloDuke =", HOLODUKE);
    PRSTAT("Multi D  =", APLAYER);

    x=33; x2=46;
    y=4; yi=2;
    printext16(x*8, ydim-STATUS2DSIZ+y*8, editorcolors[11], -1, "Weapon Count", 0);

    PRSTAT("Pistol   =", FIRSTGUNSPRITE);
    PRSTAT("Shotgun  =", SHOTGUNSPRITE);
    PRSTAT("Chaingun =", CHAINGUNSPRITE);
    PRSTAT("RPG      =", RPGSPRITE);
    PRSTAT("Pipe Bomb=", HEAVYHBOMB);
    PRSTAT("Shrinker =", SHRINKERSPRITE);
    PRSTAT("Devastatr=", DEVISTATORSPRITE);
    PRSTAT("Trip mine=", TRIPBOMBSPRITE);
    PRSTAT("Freezeray=", FREEZESPRITE);

    x=49; x2=62;
    y=4; yi=2;
    printext16(x*8,ydim-STATUS2DSIZ+y*8,editorcolors[11],-1,"Ammo Count",0);

    PRSTAT("Pistol   =", AMMO);
    PRSTAT("Shot     =", SHOTGUNAMMO);
    PRSTAT("Chain    =", BATTERYAMMO);
    PRSTAT("RPG Box  =", RPGAMMO);
    PRSTAT("Pipe Bomb=", HBOMBAMMO);
    PRSTAT("Shrinker =", CRYSTALAMMO);
    PRSTAT("Devastatr=", DEVISTATORAMMO);
    PRSTAT("Expander =", GROWAMMO);
    PRSTAT("Freezeray=", FREEZEAMMO);

    printext16(65*8, ydim-STATUS2DSIZ+4*8, editorcolors[11], -1, "MISC", 0);
    printext16(65*8, ydim-STATUS2DSIZ+8*8, editorcolors[11], -1, "ACTORS", 0);

#undef PRSTAT

    PrintStatus("Secrets =", secrets, 65, 6, 11);
    PrintStatus("Skill 1 =", totalactors1, 65, 10, 11);
    PrintStatus("Skill 2 =", totalactors2, 65, 11, 11);
    PrintStatus("Skill 3 =", totalactors3, 65, 12, 11);
    PrintStatus("Skill 4 =", totalactors4, 65, 13, 11);
    PrintStatus("Respawn =", totalrespawn, 65, 14, 11);

    enddrawing();  //}}}

    ydim += 8; // ^^^^^^ see above!
}

void ExtShowWallData(int16_t wallnum)       //F6
{
    int32_t i, runi, total=0, x, y, yi;

    UNREFERENCED_PARAMETER(wallnum);

    if (in3dmode())
        return;

    clearmidstatbar16();
    drawgradient();

    printmessage16("Level %s next tag %d", levelname, taglab_getnextfreetag());


#define CASES_LIZTROOP \
    LIZTROOP: case LIZTROOPRUNNING : case LIZTROOPSTAYPUT: case LIZTROOPSHOOT: \
              case LIZTROOPJETPACK: case LIZTROOPONTOILET: case LIZTROOPDUCKING

#define CASES_BOSS1  BOSS1: case BOSS1STAYPUT: case BOSS1SHOOT: case BOSS1LOB: case BOSSTOP

    Bmemset(numsprite, 0, sizeof(numsprite));
    Bmemset(multisprite, 0, sizeof(multisprite));

    for (i=0; i<MAXSPRITES; i++)
    {
        if (sprite[i].statnum==0 && sprite[i].pal)
            switch (sprite[i].picnum)
            {
            case CASES_LIZTROOP:
                numsprite[LIZTROOP]++;
                break;
            case CASES_BOSS1:
                multisprite[BOSS1]++;
                break;
            case BOSS2:
                multisprite[BOSS2]++;
                break;
            case BOSS3:
                multisprite[BOSS3]++;
            default:
                break;
            }
    }

    // runi==0: Count Normal Actors
    // runi==1: Count Respawn Actors
    for (runi=0; runi<2; runi++)
    {
        if (runi==1)
        {
            Bmemset(numsprite, 0, sizeof(numsprite));
            Bmemset(multisprite, 0, sizeof(multisprite));
        }

        for (i=0; i<MAXSPRITES; i++)
        {
            int32_t pic;

            if (sprite[i].statnum!=0)
                continue;

            if (runi==0 && sprite[i].pal!=0)
                continue;

            if (runi==1 && sprite[i].picnum!=RESPAWN)
                continue;

            pic = (runi==0) ? (int)sprite[i].picnum : (int)sprite[i].hitag;
            if (pic<0 || pic>=MAXTILES)
                continue;

            switch (pic)
            {
            case CASES_LIZTROOP:
                numsprite[LIZTROOP]++;
                break;
            case PIGCOP: case PIGCOPSTAYPUT: case PIGCOPDIVE:
                numsprite[PIGCOP]++;
                break;
            case LIZMAN: case LIZMANSTAYPUT: case LIZMANSPITTING: case LIZMANFEEDING: case LIZMANJUMP:
                numsprite[LIZMAN]++;
                break;
            case CASES_BOSS1:
                if (runi==0 || sprite[i].pal==0)
                    numsprite[BOSS1]++;
                else
                    multisprite[BOSS1]++;
                break;
            case COMMANDER:
            case COMMANDERSTAYPUT:
                numsprite[COMMANDER]++;
                break;
            case OCTABRAIN:
            case OCTABRAINSTAYPUT:
                numsprite[OCTABRAIN]++;
                break;
            case RECON: case DRONE: case ROTATEGUN: case EGG: case ORGANTIC: case GREENSLIME:
            case BOSS2: case BOSS3: case TANK: case NEWBEAST: case NEWBEASTSTAYPUT: case BOSS4:
                numsprite[pic]++;
                break;
            default:
                break;
            }
        }

#undef CASES_LIZTROOP
#undef CASES_BOSS1

        total=0;
        for (i=0; i<MAXTILES; i++)
            total += numsprite[i];
        for (i=0; i<MAXTILES; i++)
            total += multisprite[i];

        begindrawing();  //{{{

        x=2+runi*34;
        y=4;
        PrintStatus(runi==0?"Normal actors:":"Respawn actors:", total, x, y, 11);

#define PRSTAT(Str, Tiledef)  PrintStatus(Str, numsprite[Tiledef], x, y+(yi++), numsprite[Tiledef]?11:7);
        yi=1;

        PRSTAT(" Liztroop  =", LIZTROOP);
        PRSTAT(" Lizman    =", LIZMAN);
        PRSTAT(" Commander =", COMMANDER);
        PRSTAT(" Octabrain =", OCTABRAIN);
        PRSTAT(" PigCop    =", PIGCOP);
        PRSTAT(" Recon Car =", RECON);
        PRSTAT(" Drone     =", DRONE);
        PRSTAT(" Turret    =", ROTATEGUN);
        PRSTAT(" Egg       =", EGG);

        x+=17;
        yi=1;
        PRSTAT("Slimer    =", GREENSLIME);
        PRSTAT("Boss1     =", BOSS1);
        PrintStatus("MiniBoss1 =", multisprite[BOSS1], x, y+(yi++), multisprite[BOSS1]?11:7);
        PRSTAT("Boss2     =", BOSS2);
        PRSTAT("Boss3     =", BOSS3);
        PRSTAT("Riot Tank =", TANK);
        PRSTAT("Newbeast  =", NEWBEAST);
        PRSTAT("Boss4     =", BOSS4);
#undef PRSTAT

        enddrawing();  //}}}
    }
}

// formerly Show2dText and Show3dText
static void ShowFileText(const char *name)
{
    int32_t fp,t;
    uint8_t x=0,y=4,xmax=0,xx=0,col=0;

    if (!in3dmode())
    {
        clearmidstatbar16();
        drawgradient();
    }

    if ((fp=kopen4load(name,0)) == -1)
    {
        Bsprintf(tempbuf, "ERROR: file \"%s\" not found.", name);
        if (in3dmode())
            printext256(1*4,4*8,whitecol,-1,tempbuf,0);
        else
            printext16(1*4,ydim-STATUS2DSIZ+4*8,editorcolors[11],-1,tempbuf,0);
        return;
    }

    t=65;
    begindrawing();
    while (t!=EOF && col<5)
    {
        t = 0;
        if (kread(fp,&t,1)<=0)
            t = EOF;
        while (t!=EOF && t!='\n' && x<250)
        {
            tempbuf[x]=t;
            t = 0;
            if (kread(fp,&t,1)<=0) t = EOF;
            x++;
            if (x>xmax) xmax=x;
        }
        tempbuf[x]=0;

        if (in3dmode())
            printext256(xx*4,(y*6)+2,whitecol,-1,tempbuf,1);
        else
            printext16(xx*4,ydim-STATUS2DSIZ+(y*6)+2,editorcolors[11],-1,tempbuf,1);

        x=0;
        y++;
        if (y>18)
        {
            col++;
            y=6;
            xx+=xmax;
            xmax=0;
        }
    }
    enddrawing();

    kclose(fp);

}

// PK_ vvvv
typedef struct helppage_
{
    int32_t numlines;
    char line[][80];  // C99 flexible array member
} helppage_t;

static helppage_t **helppage=NULL;
static int32_t numhelppages=0;

static int32_t emptyline(const char *start)
{
    int32_t i;
    for (i=0; i<80; i++)
    {
        if (start[i]=='\n' || !start[i]) break;
        if (start[i]!=' ' && start[i]!='\t' && start[i]!='\r')
            return 0;
    }
    return 1;
}

static int32_t newpage(const char *start)
{
    int32_t i;
    for (i=80-1; i>=0; i--)
    {
        if (start[i] == '^' && start[i+1] == 'P')
            return 1;
    }
    return 0;
}

#define IHELP_INITPAGES 32
#define IHELP_INITLINES 16

static void ReadHelpFile(const char *name)
{
    BFILE *fp;
    int32_t i, j, k, numallocpages;
    int32_t pos, charsread=0;
    helppage_t *hp;
    char skip=0;

    initprintf("Loading \"%s\"\n",name);

    if ((fp=fopenfrompath(name,"rb")) == NULL)
    {
        initprintf("Error initializing integrated help: file \"%s\" not found.\n", name);
        return;
    }

    helppage=(helppage_t **)Bmalloc(IHELP_INITPAGES * sizeof(helppage_t *));
    numallocpages=IHELP_INITPAGES;
    if (!helppage) goto HELPFILE_ERROR;

    i=0;
    while (!Bfeof(fp) && !ferror(fp))
    {
        while (!Bfeof(fp))    // skip empty lines
        {
            pos = ftell(fp);
            if (Bfgets(tempbuf, 80, fp) == NULL) break;
            charsread = ftell(fp)-pos;
            if (!newpage(tempbuf))
            {
                break;
            }
        }

        if (Bfeof(fp) || charsread<=0) break;

        hp=(helppage_t *)Bcalloc(1,sizeof(helppage_t) + IHELP_INITLINES*80);
        if (!hp) goto HELPFILE_ERROR;
        hp->numlines = IHELP_INITLINES;

        if (charsread == 79 && tempbuf[78]!='\n') skip=1;
        j=0;

        do
        {
            if (j >= hp->numlines)
            {
                hp=(helppage_t *)Brealloc(hp, sizeof(helppage_t) + 2*hp->numlines*80);
                if (!hp) goto HELPFILE_ERROR;
                hp->numlines *= 2;
            }

            // limit the line length to 78 chars and probably get rid of the CR
            if (charsread>0)
            {
                tempbuf[charsread-1]=0;
                if (charsread>=2 && tempbuf[charsread-2]==0x0d)
                    tempbuf[charsread-2]=0;
            }

            Bmemcpy(hp->line[j], tempbuf, 80);

            for (k=charsread; k<80; k++) hp->line[j][k]=0;

            if (skip)
            {
                while (fgetc(fp)!='\n' && !Bfeof(fp)) /*skip rest of line*/;
                skip=0;
            }

            pos = ftell(fp);
            if (Bfgets(tempbuf, 80, fp) == NULL) break;
            charsread = ftell(fp)-pos;
            if (charsread == 79 && tempbuf[78]!='\n') skip=1;

            j++;

        }
        while (!newpage(tempbuf) && !Bfeof(fp) && charsread>0);

        hp=(helppage_t *)Brealloc(hp, sizeof(helppage_t) + j*80);
        if (!hp) goto HELPFILE_ERROR;
        hp->numlines=j;

        if (i >= numallocpages)
        {
            helppage = (helppage_t **)Brealloc(helppage, 2*numallocpages*sizeof(helppage_t *));
            numallocpages *= 2;
            if (!helppage) goto HELPFILE_ERROR;
        }
        helppage[i] = hp;
        i++;
    }

    helppage =(helppage_t **) Brealloc(helppage, i*sizeof(helppage_t *));
    if (!helppage) goto HELPFILE_ERROR;
    numhelppages = i;

    Bfclose(fp);
    return;

HELPFILE_ERROR:
    Bfclose(fp);
    initprintf("ReadHelpFile(): ERROR allocating memory.\n");
    return;
}

// why can't MSVC allocate an array of variable size?!
#define IHELP_NUMDISPLINES 110 // ((overridepm16y>>4)+(overridepm16y>>5)+(overridepm16y>>7)-2)
#define IHELP_PATLEN 45
extern int32_t overridepm16y;  // influences clearmidstatbar16()

static void IntegratedHelp(void)
{
    if (!helppage) return;

    overridepm16y = ydim;//3*STATUS2DSIZ;

    {
        int32_t i, j;
        static int32_t curhp=0, curline=0;
        int32_t highlighthp=-1, highlightline=-1, lasthighlighttime=0;
        char disptext[IHELP_NUMDISPLINES][80];
        char oldpattern[IHELP_PATLEN+1];

        Bmemset(oldpattern, 0, sizeof(char));
        //    clearmidstatbar16();

        begindrawing();
        CLEARLINES2D(0, ydim, 0);
        enddrawing();

        while (keystatus[KEYSC_ESC]==0 && keystatus[KEYSC_Q]==0 && keystatus[KEYSC_F1]==0)
        {
            idle_waitevent();
            if (handleevents())
                quitevent = 0;

            //        printmessage16("Help mode, press <Esc> to exit");

            if (keystatus[KEYSC_S])
            {
                fade_editor_screen(-1);
            }
            else
            {
                begindrawing();
                CLEARLINES2D(0, ydim, 0);
                enddrawing();
            }

            // based on 'save as' dialog in overheadeditor()
            if (keystatus[KEYSC_S])    // text search
            {
                char ch, bad=0, pattern[IHELP_PATLEN+1];

                for (i=0; i<IHELP_PATLEN+1; i++) pattern[i]=0;

                i=0;
                bflushchars();
                while (bad == 0)
                {
                    _printmessage16("Search: %s_", pattern);
                    showframe(1);

                    idle_waitevent();

                    if (handleevents())
                        quitevent = 0;

                    ch = bgetchar();

                    if (keystatus[1]) bad = 1;
                    else if (ch == 13) bad = 2;
                    else if (ch > 0)
                    {
                        if (i > 0 && (ch == 8 || ch == 127))
                        {
                            i--;
                            pattern[i] = 0;
                        }
                        else if (i < IHELP_PATLEN && ch >= 32 && ch < 128)
                        {
                            pattern[i++] = ch;
                            pattern[i] = 0;
                        }
                    }
                }

                if (bad==1)
                {
                    keystatus[KEYSC_ESC] = keystatus[KEYSC_Q] = keystatus[KEYSC_F1] = 0;
                }

                if (bad==2)
                {
                    keystatus[KEYSC_ENTER] = 0;

                    for (i=curhp; i<numhelppages; i++)
                    {
                        for (j = (i==curhp)?(curline+1):0; j<helppage[i]->numlines; j++)
                        {
                            // entering an empty pattern will search with the last used pattern
                            if (Bstrstr(helppage[i]->line[j], pattern[0]?pattern:oldpattern))
                            {
                                curhp = i;

                                if ((curline=j) <= helppage[i]->numlines - 32 /*-IHELP_NUMDISPLINES*/) /**/;
                                else if ((curline=helppage[i]->numlines- 32 /*-IHELP_NUMDISPLINES*/) >= 0) /**/;
                                else curline=0;

                                highlighthp = i;
                                highlightline = j;
                                lasthighlighttime = totalclock;
                                goto ENDFOR1;
                            }
                        }
                    }
ENDFOR1:
                    if (pattern[0])
                        Bmemcpy(oldpattern, pattern, IHELP_PATLEN+1);
                }
            }
            else if (PRESSED_KEYSC(T))    // goto table of contents
            {
                curhp=0;
                curline=0;
            }
            else if (PRESSED_KEYSC(G))    // goto arbitrary page
            {
                curhp=getnumber16("Goto page: ", 0, numhelppages-1, 0);
                curline=0;
            }
            else if (PRESSED_KEYSC(UP))    // scroll up
            {
                if (curline>0) curline--;
            }
            else if (PRESSED_KEYSC(DOWN))    // scroll down
            {
                if (curline + 32/*+IHELP_NUMDISPLINES*/ < helppage[curhp]->numlines) curline++;
            }
            else if (PRESSED_KEYSC(PGUP))    // scroll one page up
            {
                i=IHELP_NUMDISPLINES;
                while (i>0 && curline>0) i--, curline--;
            }
            else if (PRESSED_KEYSC(PGDN))    // scroll one page down
            {
                i=IHELP_NUMDISPLINES;
                while (i>0 && curline + 32/*+IHELP_NUMDISPLINES*/ < helppage[curhp]->numlines) i--, curline++;
            }
            else if (PRESSED_KEYSC(SPACE))   // goto next paragraph
            {
                for (i=curline, j=0; i < helppage[curhp]->numlines; i++)
                {
                    if (emptyline(helppage[curhp]->line[i])) { j=1; continue; }
                    if (j==1 && !emptyline(helppage[curhp]->line[i])) { j=2; break; }
                }
                if (j==2)
                {
                    if (i + 32 /*+IHELP_NUMDISPLINES*/ < helppage[curhp]->numlines)
                        curline=i;
                    else if (helppage[curhp]->numlines - 32/*-IHELP_NUMDISPLINES*/ > curline)
                        curline = helppage[curhp]->numlines - 32/*-IHELP_NUMDISPLINES*/;
                }
            }
            else if (PRESSED_KEYSC(BS))   // goto prev paragraph
            {
                for (i=curline-1, j=0; i>=0; i--)
                {
                    if (!emptyline(helppage[curhp]->line[i])) { j=1; continue; }
                    if (j==1 && emptyline(helppage[curhp]->line[i])) { j=2; break; }
                }
                if (j==2 || i==-1) curline=i+1;
            }
            else if (PRESSED_KEYSC(HOME))    // goto beginning of page
            {
                curline=0;
            }
            else if (PRESSED_KEYSC(END))    // goto end of page
            {
                if ((curline=helppage[curhp]->numlines - 32/*-IHELP_NUMDISPLINES*/) >= 0) /**/;
                else curline=0;
            }
            else if (PRESSED_KEYSC(LEFT) || PRESSED_KEYSC(LBRACK))    // prev page
            {
                if (curhp>0)
                {
                    curhp--;
                    curline=0;
                }
            }
            else if (PRESSED_KEYSC(RIGHT) || PRESSED_KEYSC(RBRACK))    // next page
            {
                if (curhp<numhelppages-1)
                {
                    curhp++;
                    curline=0;
                }
            }
            else    // '1'-'0' on the upper row
            {
                for (i=2; i<=11; i++)
                    if (keystatus[i]) break;
                if (i--<12 && i<numhelppages)
                {
                    curhp=i;
                    curline=0;
                }
            }

//            drawgradient();

            begindrawing();
            printext16(9, ydim2d-overridepm16y+9, editorcolors[4], -1, "Help Mode", 0);
            printext16(8, ydim2d-overridepm16y+8, editorcolors[12], -1, "Help Mode", 0);
            printext16(8 + 9*8 + 2*8, ydim2d-overridepm16y+8, editorcolors[15], -1, "(S:search)", 0);
            enddrawing();

            if (curhp < helppage[0]->numlines)
                _printmessage16("%s", helppage[0]->line[curhp]);
            else
                _printmessage16("%d. (Untitled page)", curhp);

            for (i=0; j=(curhp==0)?(i+curline+1):(i+curline),
                    i<IHELP_NUMDISPLINES && j<helppage[curhp]->numlines; i++)
            {
                if (ydim-overridepm16y+28+i*9+32 >= ydim)
                    break;
                Bmemcpy(disptext[i], helppage[curhp]->line[j], 80);
                printext16(8, ydim-overridepm16y+28+i*9, editorcolors[10],
                           (j==highlightline && curhp==highlighthp
                            && totalclock-lasthighlighttime<120*5) ?
                           editorcolors[1] : -1,
                           disptext[i], 0);
            }

            showframe(1);
        }

        clearkeys();
    }

    overridepm16y = -1;
}

#define SOUND_NUMDISPLINES IHELP_NUMDISPLINES

static int32_t compare_sounds_s(int16_t k1, int16_t k2)
{
    return (int32_t)k1 - (int32_t)k2;
}
static int32_t compare_sounds_d(int16_t k1, int16_t k2)
{
    sound_t *s1 = &g_sounds[k1], *s2 = &g_sounds[k2];
    char *n1 = s1->definedname, *n2 = s2->definedname;

    if (!n1 && !n2) return 0;
    if (!n1) return -1;
    if (!n2) return 1;
    return Bstrcasecmp(n1, n2);
}
static int32_t compare_sounds_f(int16_t k1, int16_t k2)
{
    sound_t *s1 = &g_sounds[k1], *s2 = &g_sounds[k2];
    char *n1 = s1->filename, *n2 = s2->filename;

    if (!n1 && !n2) return 0;
    if (!n1) return -1;
    if (!n2) return 1;
    return Bstrcasecmp(n1, n2);
}
static int32_t compare_sounds_1(int16_t k1, int16_t k2)
{
    return (g_sounds[k2].m&1) - (g_sounds[k1].m&1);
}
static int32_t compare_sounds_2(int16_t k1, int16_t k2)
{
    return (g_sounds[k2].m&2) - (g_sounds[k1].m&2);
}
static int32_t compare_sounds_3(int16_t k1, int16_t k2)
{
    return (g_sounds[k2].m&4) - (g_sounds[k1].m&4);
}
static int32_t compare_sounds_4(int16_t k1, int16_t k2)
{
    return (g_sounds[k2].m&8) - (g_sounds[k1].m&8);
}
static int32_t compare_sounds_5(int16_t k1, int16_t k2)
{
    return (g_sounds[k2].m&16) - (g_sounds[k1].m&16);
}


static int32_t sort_sounds(int32_t how)
{
    int32_t (*compare_sounds)(int16_t, int16_t) = NULL;

    int32_t ms, ofs, l, lb, r, rb, d, n, k1, k2;
    int16_t *src, *dst, *source, *dest, *tmp;

    n = g_numsounds;
    src = source = g_sndnum;
    dest = (int16_t *)Bmalloc(sizeof(int16_t) * n);
    dst = dest;
    if (!dest) return -1;

    switch (how)
    {
    case 'g':  // restore original order
        Bmemcpy(g_sndnum, g_definedsndnum, sizeof(int16_t)*n);
        return 0;
    case 's':
        compare_sounds = compare_sounds_s;
        break;
    case 'd':
        compare_sounds = compare_sounds_d;
        break;
    case 'f':
        compare_sounds = compare_sounds_f;
        break;
    case '1':
        compare_sounds = compare_sounds_1;
        break;
    case '2':
        compare_sounds = compare_sounds_2;
        break;
    case '3':
        compare_sounds = compare_sounds_3;
        break;
    case '4':
        compare_sounds = compare_sounds_4;
        break;
    case '5':
        compare_sounds = compare_sounds_5;
        break;
    default:
        return -2;
    }

    for (ms=1; ms<n; ms*=2)
    {
        for (ofs=0; ofs<n; ofs+=2*ms)
        {
            l = ofs;
            r = ofs+ms;
            d = ofs;
            lb = min((l+ms), n);
            rb = min((r+ms), n);
            while (l < lb || r < rb)
            {
                if (l >= lb)
                {
                    dst[d++] = src[r++];
                    continue;
                }
                if (r >= rb)
                {
                    dst[d++] = src[l++];
                    continue;
                }
                k1 = src[l];
                k2 = src[r];
                if (compare_sounds(k1, k2) <= 0)
                {
                    dst[d++] = src[l++];
                    continue;
                }
                dst[d++] = src[r++];
            }
        }
        tmp = src;
        src = dst;
        dst = tmp;
    }
    if (src != source)
        Bmemcpy(source, src, sizeof(int16_t) * n);

    Bfree(dest);
    return 0;
}

static void SoundDisplay(void)
{
    if (g_numsounds <= 0) return;

    overridepm16y = ydim;//3*STATUS2DSIZ;

    {
        // cursnd is the first displayed line, cursnd+curofs is where the cursor is
        static int32_t cursnd=0, curofs=0;
        char disptext[80];

        int32_t i, j;
        const int32_t halfpage = (ydim-64)/(2*9);

        while (keystatus[KEYSC_ESC]==0 && keystatus[KEYSC_Q]==0 && keystatus[KEYSC_F2]==0
                && keystatus[buildkeys[BK_MODE2D_3D]]==0)  // quickjump to 3d mode
        {
            begindrawing();
            CLEARLINES2D(0, ydim16, 0);
            enddrawing();

            idle_waitevent();
            if (handleevents())
                quitevent = 0;

//            drawgradient();

            begindrawing();
            printext16(9, ydim2d-overridepm16y+9, editorcolors[4], -1, "Sound Index", 0);
            printext16(8, ydim2d-overridepm16y+8, editorcolors[12], -1, "Sound Index", 0);
            printext16(8 + 11*8 + 2*8, ydim2d-overridepm16y+8, editorcolors[15], -1, "(SPACE:play, S:sort)", 0);
            enddrawing();

            if (PRESSED_KEYSC(G))    // goto specified sound#
            {
                _printmessage16("                                                    ");
                j = getnumber16("Goto sound#: ", 0, g_numsounds-1, 0);
                for (i=0; i<g_numsounds; i++)
                    if (g_sndnum[i]==j)
                        break;
                if (i != g_numsounds)
                {
                    if (i<SOUND_NUMDISPLINES)
                        cursnd = 0, curofs = i;
                    else if (i>=g_numsounds-halfpage)
                        cursnd = g_numsounds-halfpage, curofs = i-cursnd;
                    else
                        curofs = halfpage/2, cursnd = i-curofs;
                }
            }
            else if (PRESSED_KEYSC(UP))    // scroll up
            {
                if (curofs>0) curofs--;
                else if (cursnd>0) cursnd--;
            }
            else if (PRESSED_KEYSC(DOWN))    // scroll down
            {
                if (curofs < halfpage-1 && cursnd+curofs<g_numsounds-1)
                    curofs++;
                else if (cursnd + halfpage < g_numsounds)
                    cursnd++;
            }
            else if (PRESSED_KEYSC(PGUP))    // scroll one page up
            {
                i = halfpage/2;

                while (i>0 && curofs>0)
                    i--, curofs--;
                while (i>0 && cursnd>0)
                    i--, cursnd--;
            }
            else if (PRESSED_KEYSC(PGDN))    // scroll one page down
            {
                i = halfpage/2;

                while (i>0 && curofs < halfpage-1 && cursnd+curofs<g_numsounds-1)
                    i--, curofs++;
                while (i>0 && cursnd + halfpage < g_numsounds)
                    i--, cursnd++;
            }
            else if (PRESSED_KEYSC(SPACE) || PRESSED_KEYSC(ENTER))   // play/stop sound
            {
                int32_t j = cursnd+curofs;
                int32_t k = g_sndnum[j];

                if (S_CheckSoundPlaying(0, k) > 0)
                    S_StopSound(k);
                else
                    S_PlaySound(k);
            }
            else if (PRESSED_KEYSC(HOME))    // goto first sound#
            {
                cursnd = curofs = 0;
            }
            else if (PRESSED_KEYSC(END))    // goto last sound#
            {
                if ((cursnd = g_numsounds - halfpage) >= 0)
                    curofs = halfpage-1;
                else
                {
                    cursnd = 0;
                    curofs = g_numsounds-1;
                }
            }

            _printmessage16("                          FILE NAME         PITCH RANGE  PRI FLAGS VOLUME");
            for (i=0; j=cursnd+i, i<SOUND_NUMDISPLINES && j<g_numsounds; i++)
            {
                int32_t l, m, k=g_sndnum[j];
                sound_t *snd=&g_sounds[k];
                char *cp;

                if (ydim-overridepm16y+28+i*9+32 >= ydim) break;

                Bsprintf(disptext,
                         "%4d .................... ................ %6d:%-6d %3d %c%c%c%c%c %6d",
                         //   5678901234567890X23456789012345678901234567
                         k, snd->ps, snd->pe, snd->pr,
                         snd->m&1 ? 'R':'-', snd->m&2 ? 'M':'-', snd->m&4 ? 'D':'-',
                         snd->m&8 ? 'P':'-', snd->m&16 ? 'G':'-', snd->vo);
                for (l = Bsnprintf(disptext+5, 20, "%s", snd->definedname); l<20; l++)
                    disptext[5+l] = ' ';
                if (snd->filename)
                {
                    l = Bstrlen(snd->filename);
                    if (l<=16)
                        cp = snd->filename;
                    else
                        cp = snd->filename + l-15;
                    for (m = Bsnprintf(disptext+26, 16, "%s", cp); m<16; m++)
                        disptext[26+m] = ' ';
                    if (l>16)
                        disptext[26] = disptext[27] = disptext[28] = '.';
                }

                printext16(8, ydim-overridepm16y+28+i*9,
                           keystatus[KEYSC_S]?editorcolors[8] : (S_CheckSoundPlaying(-1, k) ? editorcolors[2] : editorcolors[10]),
                           j==cursnd+curofs ? editorcolors[1] : -1,
                           disptext, 0);
            }

            if (keystatus[KEYSC_S])    // sorting
            {

                char ch, bad=0;

                _printmessage16("Sort by: (S)oundnum (D)ef (F)ile ori(g) or flags (12345)");
                showframe(1);

                i=0;
                bflushchars();
                while (bad == 0)
                {
                    idle_waitevent();
                    if (handleevents())
                        quitevent = 0;

                    ch = bgetchar();

                    if (keystatus[1]) bad = 1;

                    else if (ch == 's' || ch == 'd' || ch == 'f' || ch == 'g' ||
                             ch == '1' || ch == '2' || ch == '3' || ch == '4' || ch == '5')
                    {
                        bad = 2;
                        sort_sounds(ch);
                    }
                }

                clearkeys();
            }
            else
                showframe(1);
        }

        overridepm16y = -1;

        FX_StopAllSounds();
        S_ClearSoundLocks();

        clearkeys();
    }
}
// PK_ ^^^^

int32_t AmbienceToggle = 1;
int32_t ParentalLock = 0;

uint8_t g_ambiencePlaying[MAXSPRITES>>3];

#define testbit(bitarray, i) (bitarray[(i)>>3] & (1<<((i)&7)))
#define setbit(bitarray, i) bitarray[(i)>>3] |= (1<<((i)&7))
#define clearbit(bitarray, i) bitarray[(i)>>3] &= ~(1<<((i)&7))

// adapted from actors.c
static void M32_MoveFX(void)
{
    int32_t i, j;
    int32_t x, ht;
    spritetype *s;

    for (i=headspritestat[0]; i>=0; i=nextspritestat[i])
    {
        s = &sprite[i];

        if (s->picnum != MUSICANDSFX)
        {
            if (testbit(g_ambiencePlaying, i))
            {
                clearbit(g_ambiencePlaying, i);
                S_StopEnvSound(s->lotag, i);
            }
        }
        else if (s->sectnum>=0)
        {
            ht = s->hitag;

            if (s->lotag < 999 && (unsigned)sector[s->sectnum].lotag < 9 &&
                    AmbienceToggle && sector[s->sectnum].floorz != sector[s->sectnum].ceilingz)
            {
                if ((g_sounds[s->lotag].m & SF_MSFX))
                {
                    x = dist((spritetype *)&pos,s);
                    if (x < ht && !testbit(g_ambiencePlaying, i) && FX_VoiceAvailable(g_sounds[s->lotag].pr-1))
                    {
                        char om = g_sounds[s->lotag].m;
                        if (g_numEnvSoundsPlaying == NumVoices)
                        {
                            for (j = headspritestat[0]; j >= 0; j = nextspritestat[j])
                            {
                                if (s->picnum == MUSICANDSFX && j != i && sprite[j].lotag < 999 &&
                                        testbit(g_ambiencePlaying, j) && dist(&sprite[j],(spritetype *)&pos) > x)
                                {
                                    S_StopEnvSound(sprite[j].lotag,j);
                                    break;
                                }

                            }
                            if (j == -1) continue;
                        }
                        g_sounds[s->lotag].m |= SF_LOOP;
                        A_PlaySound(s->lotag,i);
                        g_sounds[s->lotag].m = om;
                        setbit(g_ambiencePlaying, i);
                    }
                    if (x >= ht && testbit(g_ambiencePlaying, i))
                    {
                        clearbit(g_ambiencePlaying, i);
                        S_StopEnvSound(s->lotag,i);
                    }
                }
            }
        }
    }
}


///__ShowHelpText__

void ExtShowSpriteData(int16_t spritenum)   //F6
{
    UNREFERENCED_PARAMETER(spritenum);
    if (!in3dmode())
        ShowFileText("sehelp.hlp");
}

// Floor Over Floor (duke3d)

// If standing in sector with SE42 or SE44
// then draw viewing to SE41 and raise all =hi SE43 cielings.

// If standing in sector with SE43 or SE45
// then draw viewing to SE40 and lower all =hi SE42 floors.

static int32_t fofsizex = -1;
static int32_t fofsizey = -1;
#if 0
static void ResetFOFSize(void)
{
    if (fofsizex != -1) tilesizx[FOF] = fofsizex;
    if (fofsizey != -1) tilesizy[FOF] = fofsizey;
}
#endif
static void ExtSE40Draw(int32_t spnum,int32_t x,int32_t y,int32_t z,int16_t a,int16_t h)
{
    static int32_t tempsectorz[MAXSECTORS];
    static int32_t tempsectorpicnum[MAXSECTORS];

    int32_t j=0,k=0;
    int32_t floor1=0,floor2=0,ok=0,fofmode=0,draw_both=0;
    int32_t offx,offy,offz;

    if (sprite[spnum].ang!=512) return;

    // Things are a little different now, as we allow for masked transparent
    // floors and ceilings. So the FOF textures is no longer required
    //  if (!(gotpic[FOF>>3]&(1<<(FOF&7))))
    //          return;
    //  gotpic[FOF>>3] &= ~(1<<(FOF&7));

    if (tilesizx[562])
    {
        fofsizex = tilesizx[562];
        tilesizx[562] = 0;
    }
    if (tilesizy[562])
    {
        fofsizey = tilesizy[562];
        tilesizy[562] = 0;
    }

    floor1=spnum;

    if (sprite[spnum].lotag==42) fofmode=40;
    if (sprite[spnum].lotag==43) fofmode=41;
    if (sprite[spnum].lotag==44) fofmode=40;
    if (sprite[spnum].lotag==45) fofmode=41;

    // fofmode=sprite[spnum].lotag-2;

    // sectnum=sprite[j].sectnum;
    // sectnum=cursectnum;
    ok++;

    /*  recursive?
    for(j=0;j<MAXSPRITES;j++)
    {
    if(
    sprite[j].sectnum==sectnum &&
    sprite[j].picnum==1 &&
    sprite[j].lotag==110
    ) { DrawFloorOverFloor(j); break;}
    }
    */


    // if(ok==0) { Message("no fof",RED); return; }

    for (j=0; j<MAXSPRITES; j++)
    {
        if (sprite[j].picnum==1 && sprite[j].lotag==fofmode && sprite[j].hitag==sprite[floor1].hitag)
        {
            floor1=j;
            fofmode=sprite[j].lotag;
            ok++;
            break;
        }
    }
    // if(ok==1) { Message("no floor1",RED); return; }

    if (fofmode==40) k=41;
    else k=40;

    for (j=0; j<MAXSPRITES; j++)
    {
        if (sprite[j].picnum==1 && sprite[j].lotag==k && sprite[j].hitag==sprite[floor1].hitag)
        {
            floor2=j;
            ok++;
            break;
        }
    }

//    i=floor1;
    offx=sprite[floor2].x-sprite[floor1].x;
    offy=sprite[floor2].y-sprite[floor1].y;
    offz=0;

    if (sprite[floor2].ang >= 1024)
        offz = sprite[floor2].z;
    else if (fofmode==41)
        offz = SPRITESEC(floor2).floorz;
    else
        offz = SPRITESEC(floor2).ceilingz;

    if (sprite[floor1].ang >= 1024)
        offz -= sprite[floor1].z;
    else if (fofmode==40)
        offz -= SPRITESEC(floor1).floorz;
    else
        offz -= SPRITESEC(floor1).ceilingz;

    // if(ok==2) { Message("no floor2",RED); return; }

    for (j=0; j<MAXSPRITES; j++) // raise ceiling or floor
    {
        if (sprite[j].picnum==1 && sprite[j].lotag==k+2 && sprite[j].hitag==sprite[floor1].hitag)
        {
            if (k==40)
            {
                tempsectorz[sprite[j].sectnum] = SPRITESEC(j).floorz;
                SPRITESEC(j).floorz += (((z-SPRITESEC(j).floorz)/32768)+1)*32768;
                tempsectorpicnum[sprite[j].sectnum] = SPRITESEC(j).floorpicnum;
                SPRITESEC(j).floorpicnum = 562;
            }
            else
            {
                tempsectorz[sprite[j].sectnum] = SPRITESEC(j).ceilingz;
                SPRITESEC(j).ceilingz += (((z-SPRITESEC(j).ceilingz)/32768)-1)*32768;
                tempsectorpicnum[sprite[j].sectnum] = SPRITESEC(j).ceilingpicnum;
                SPRITESEC(j).ceilingpicnum = 562;
            }
            draw_both = 1;
        }
    }

    drawrooms(x+offx,y+offy,z+offz,a,h,sprite[floor2].sectnum);
    ExtAnalyzeSprites(0,0,0,0);
    drawmasks();
    M32_ResetFakeRORTiles();

    if (draw_both)
    {
        for (j=0; j<MAXSPRITES; j++) // restore ceiling or floor for the draw both sectors
        {
            if (sprite[j].picnum==SECTOREFFECTOR &&
                    sprite[j].lotag==k+2 && sprite[j].hitag==sprite[floor1].hitag)
            {
                if (k==40)
                {
                    SPRITESEC(j).floorz = tempsectorz[sprite[j].sectnum];
                    SPRITESEC(j).floorpicnum = tempsectorpicnum[sprite[j].sectnum];
                }
                else
                {
                    SPRITESEC(j).ceilingz = tempsectorz[sprite[j].sectnum];
                    SPRITESEC(j).ceilingpicnum = tempsectorpicnum[sprite[j].sectnum];
                }
            }// end if
        }// end for

        // Now re-draw
        drawrooms(x+offx,y+offy,z+offz,a,h,sprite[floor2].sectnum);
        ExtAnalyzeSprites(0,0,0,0);
        drawmasks();
        M32_ResetFakeRORTiles();
    }

} // end SE40

static void SE40Code(int32_t x,int32_t y,int32_t z,int32_t a,int32_t h)
{
    int32_t i;

    i = 0;
    while (i<MAXSPRITES)
    {
        int32_t t = sprite[i].lotag;
        switch (t)
        {
            //            case 40:
            //            case 41:
            //                ExtSE40Draw(i,x,y,z,a,h);
            //                break;
        case 42:
        case 43:
        case 44:
        case 45:
            if (cursectnum == sprite[i].sectnum)
                ExtSE40Draw(i,x,y,z,a,h);
            break;
        }
        i++;
    }
}

void ExtEditSectorData(int16_t sectnum)    //F7
{
    if (in3dmode())
        return;

    if (eitherALT)  //ALT
    {
        keystatus[KEYSC_F7] = 0;
        wallsprite=0;
        curwall = 0;
        curwallnum = 0;
        cursearchspritenum = 0;
        cursectornum=0;
        cursector_lotag = sector[sectnum].lotag;
        cursector_lotag = getnumber16("Enter search sector lotag : ", cursector_lotag, BTAG_MAX,0);
        _printmessage16("Search sector lotag %d",cursector_lotag);
    }
    else EditSectorData(sectnum);
}// end ExtEditSectorData

void ExtEditWallData(int16_t wallnum)       //F8
{
    if (in3dmode())
        return;

    if (eitherALT)  //ALT
    {
        wallsprite=1;
        curwall = wallnum;
        curwallnum = 0;
        cursearchspritenum = 0;
        cursectornum = 0;
        search_lotag = wall[curwall].lotag;
        search_hitag = wall[curwall].hitag;
        search_lotag = getnumber16("Enter wall search lotag : ", search_lotag, BTAG_MAX,0);
        search_hitag = getnumber16("Enter wall search hitag : ", search_hitag, BTAG_MAX,0);
        //    Bsprintf(tempbuf,"Current wall %d lo=%d hi=%d",
        //             curwall,wall[curwall].lotag,wall[curwall].hitag);
        _printmessage16("Search wall lo=%d hi=%d",search_lotag,search_hitag);
    }
    else EditWallData(wallnum);
}

static void GenericSpriteSearch(void);

void ExtEditSpriteData(int16_t spritenum)   //F8
{
    if (in3dmode())
        return;

    if (eitherALT)  //ALT
        GenericSpriteSearch();
    else EditSpriteData(spritenum);
}

static inline void SpriteName(int16_t spritenum, char *lo2)
{
    Bstrcpy(lo2, names[sprite[spritenum].picnum]);
}// end SpriteName

// Returns: did error?
static int32_t ReadPaletteTable(void)
{
    int32_t fp;

    // Make base shade table at shade 0 into the identity map.
    // (In the shade table of Duke3D's PALETTE.DAT, palookup[0][239]==143.)
    // This makes it possible to sensibly use Lunatic's engine.saveLookupDat().
    palookup[0][239] = 239;

    if ((fp=kopen4load("lookup.dat",0)) == -1)
    {
        if ((fp=kopen4load("lookup.dat",1)) == -1)
        {
            initprintf("LOOKUP.DAT not found\n");
            return 1;
        }
    }

    g_firstFogPal = loadlookups(fp, basepaltable);
    kclose(fp);

    if (g_firstFogPal < 0)
    {
        initprintf("ERROR loading PALOOKUP.DAT: failed reading enough data\n");
        return 1;
    }

    return 0;
}


static void m32_showmouse(void)
{
    int32_t i, col;

    if (totalclock > lastupdate)
    {
        mousecol += mouseadd;
        if (mousecol >= 30 || mousecol <= 0)
        {
            mouseadd = -mouseadd;
            mousecol += mouseadd;
        }
        lastupdate = totalclock + 3;
    }

    switch (whitecol)
    {
    case 1:  // Shadow Warrior
        col = whitecol+mousecol;
        break;
    case 31: // Duke Nukem 3D
        col = whitecol-mousecol;
        break;
    default:
        col = whitecol;
        break;
    }

    push_nofog();

    if (col != whitecol)
    {
        for (i=((xdim > 640)?3:2); i<=((xdim > 640)?7:3); i++)
        {
            plotpixel(searchx+i,searchy,col);
            plotpixel(searchx-i,searchy,col);
            plotpixel(searchx,searchy-i,col);
            plotpixel(searchx,searchy+i,col);
        }

        for (i=1; i<=((xdim > 640)?2:1); i++)
        {
            plotpixel(searchx+i,searchy,whitecol);
            plotpixel(searchx-i,searchy,whitecol);
            plotpixel(searchx,searchy-i,whitecol);
            plotpixel(searchx,searchy+i,whitecol);
        }

        i = (xdim > 640)?8:4;

        plotpixel(searchx+i,searchy,0);
        plotpixel(searchx-i,searchy,0);
        plotpixel(searchx,searchy-i,0);
        plotpixel(searchx,searchy+i,0);
    }

    if (xdim > 640)
    {
        for (i=1; i<=4; i++)
        {
            plotpixel(searchx+i,searchy,whitecol);
            plotpixel(searchx-i,searchy,whitecol);
            plotpixel(searchx,searchy-i,whitecol);
            plotpixel(searchx,searchy+i,whitecol);
        }
    }

    pop_nofog();
}

int32_t AskIfSure(const char *text)
{
    int32_t retval=1;

    if (in3dmode())
    {
        begindrawing(); //{{{
        printext256(0,0,whitecol,0,text?text:"Are you sure you want to proceed?",0);
        enddrawing();   //}}}
    }
    else
    {
        _printmessage16("%s", text?text:"Are you sure you want to proceed?");
    }

    showframe(1);

    while ((keystatus[KEYSC_ESC]|keystatus[KEYSC_ENTER]|keystatus[KEYSC_SPACE]|keystatus[KEYSC_N]) == 0)
    {
        idle_waitevent();

        if (handleevents())
        {
            if (quitevent)
            {
                retval = 1;
                break;
            }
        }

        if (PRESSED_KEYSC(Y) || PRESSED_KEYSC(ENTER))
        {
            retval = 0;
            break;
        }
    }

    if (PRESSED_KEYSC(ESC))
        retval = 1;

    return retval;
}

static int32_t IsValidTile(int32_t idTile)
{
    return (idTile>=0 && idTile<MAXTILES) && (tilesizx[idTile] && tilesizy[idTile]);
}

static int32_t SelectAllTiles(int32_t iCurrentTile)
{
    int32_t i;

    if (iCurrentTile < localartlookupnum)
        iCurrentTile = localartlookup[iCurrentTile];
    else
        iCurrentTile = 0;

    localartlookupnum = MAXTILES;

    for (i = 0; i < MAXTILES; i++)
    {
        localartlookup[i] = i;
        localartfreq[i] = 0;
    }

    return iCurrentTile;
}

static int32_t OnGotoTile(int32_t iTile);
static int32_t OnSelectTile(int32_t iTile);
static int32_t OnSaveTileGroup();
static int32_t loadtilegroups(const char *fn);
static int32_t s_Zoom = INITIAL_ZOOM;
static int32_t s_TileZoom = 1;
static char tilesel_errmsg[128], tilesel_showerr=0;

static int32_t DrawTiles(int32_t iTopLeft, int32_t iSelected, int32_t nXTiles, int32_t nYTiles,
                         int32_t TileDim, int32_t offset, int32_t showmsg);

#define TMPERRMSG_SHOW(alsoOSD) do { \
    printext256(0, 0, whitecol, 0, tilesel_errmsg, 0); \
    if (alsoOSD) OSD_Printf("%s\n", tilesel_errmsg); \
} while (0)


#define TMPERRMSG_PRINT(Msg, ...) do {  \
    Bsprintf(tilesel_errmsg, Msg, ## __VA_ARGS__); \
    TMPERRMSG_SHOW(1); \
    showframe(1); \
    tilesel_showerr = 1; \
} while (0)


#define TMPERRMSG_RETURN(Msg, ...) do   \
{ \
    TMPERRMSG_PRINT(Msg, ## __VA_ARGS__);  \
    return 1; \
} while (0)



static inline void pushDisableFog(void)
{
#ifdef USE_OPENGL
    bglPushAttrib(GL_ENABLE_BIT);
    bglDisable(GL_FOG);
#endif
}

static inline void popDisableFog(void)
{
#ifdef USE_OPENGL
    bglPopAttrib();
#endif
}

static int32_t m32gettile(int32_t idInitialTile)
{
    int32_t gap, temp, zoomsz;
    int32_t nXTiles, nYTiles, nDisplayedTiles;
    int32_t i;
    int32_t iTile, iTopLeftTile, iLastTile;
    int32_t idSelectedTile;
    int32_t scrollmode;
    int32_t mousedx, mousedy, mtile, omousex=searchx, omousey=searchy, moffset=0;

    int32_t noTilesMarked=1;
    int32_t mark_lastk = -1;

    pushDisableFog();

    // Enable following line for testing. I couldn't work out how to change vidmode on the fly
    // s_Zoom = NUM_ZOOMS - 1;

    idInitialTile = clamp(idInitialTile, 0, MAXTILES-1);

    // Ensure zoom not to big (which can happen if display size
    //   changes whilst Mapster is running)
    do
    {
        nXTiles = xdim / ZoomToThumbSize[s_Zoom];
        nYTiles = ydim / ZoomToThumbSize[s_Zoom];
        // Refuse to draw less than half of a row.
        if (ZoomToThumbSize[s_Zoom]/2 < 12) nYTiles--;
        nDisplayedTiles  = nXTiles * nYTiles;

        if (!nDisplayedTiles)
        {
            // Eh-up, resolution changed since we were last displaying tiles.
            s_Zoom--;
        }
    }
    while (!nDisplayedTiles);

    keystatus[KEYSC_V] = 0;

    for (i = 0; i < MAXTILES; i++)
    {
        localartfreq[i] = 0;
        localartlookup[i] = i;
    }

    iLastTile = iTile = idSelectedTile = idInitialTile;

    switch (searchstat)
    {
    case SEARCH_WALL:
        for (i = 0; i < numwalls; i++)
            localartfreq[ wall[i].picnum ]++;
        break;

    case SEARCH_CEILING:
    case SEARCH_FLOOR:
        for (i = 0; i < numsectors; i++)
        {
            localartfreq[ sector[i].ceilingpicnum ]++;
            localartfreq[ sector[i].floorpicnum ]++;
        }
        break;

    case SEARCH_SPRITE:
        for (i=0; i<MAXSPRITES; i++)
            localartfreq[ sprite[i].picnum ] += (sprite[i].statnum < MAXSTATUS);
        break;

    case SEARCH_MASKWALL:
        for (i = 0; i < numwalls; i++)
            localartfreq[ wall[i].overpicnum ]++;
        break;

    default:
        break;
    }


    //
    //  Sort tiles into frequency order
    //

    gap = MAXTILES / 2;

    do
    {
        for (i = 0; i < MAXTILES-gap; i++)
        {
            temp = i;

            while (temp >= 0 && localartfreq[temp]<localartfreq[temp+gap])
            {
                int32_t tempint;

                tempint = localartfreq[temp];
                localartfreq[temp] = localartfreq[temp+gap];
                localartfreq[temp+gap] = tempint;

                tempint = localartlookup[temp];
                localartlookup[temp] = localartlookup[temp+gap];
                localartlookup[temp+gap] = tempint;

                if (iTile == temp)
                    iTile = temp + gap;
                else if (iTile == temp + gap)
                    iTile = temp;

                temp -= gap;
            }
        }
        gap >>= 1;
    }
    while (gap > 0);

    //
    // Set up count of number of used tiles
    //

    localartlookupnum = 0;
    while (localartfreq[localartlookupnum] > 0)
        localartlookupnum++;

    //
    // Check : If no tiles used at all then switch to displaying all tiles
    //

    if (!localartfreq[0])
    {
        localartlookupnum = MAXTILES;

        for (i = 0; i < MAXTILES; i++)
        {
            localartlookup[i] = i;
            localartfreq[i] = 0; // Terrible bodge : zero tilefreq's not displayed in tile view. Still, when in Rome ... :-)
        }

        iTile = idInitialTile;
    }

    //
    //
    //

    iTopLeftTile = iTile - (iTile % nXTiles);
    iTopLeftTile = clamp(iTopLeftTile, 0, MAXTILES-nDisplayedTiles);

    zoomsz = ZoomToThumbSize[s_Zoom];

    searchx = ((iTile-iTopLeftTile)%nXTiles)*zoomsz + zoomsz/2;
    searchy = ((iTile-iTopLeftTile)/nXTiles)*zoomsz + zoomsz/2;

    ////////////////////////////////
    // Start of key handling code //
    ////////////////////////////////

    while ((keystatus[KEYSC_ENTER]|keystatus[KEYSC_ESC]|(bstatus&1)) == 0) // <- Presumably one of these is escape key ??
    {
        int32_t ret;
        zoomsz = ZoomToThumbSize[s_Zoom];

        ret = DrawTiles(iTopLeftTile, (iTile >= localartlookupnum) ? localartlookupnum-1 : iTile,
                        nXTiles, nYTiles, zoomsz, moffset,
                        (tilesel_showerr && (iTile==iLastTile || (tilesel_showerr=0))));

        if (ret==0)
        {
            idle_waitevent_timeout(500);
            // SDL seems to miss mousewheel events when rotated slowly.

            if (handleevents())
                quitevent = 0;
        }
        getmousevalues(&mousedx,&mousedy,&bstatus);

        iLastTile = iTile;

        searchx += mousedx;
        searchy += mousedy;

        if (bstatus&2)
        {
            moffset += mousedy*2;
            searchy += mousedy;
            searchx -= mousedx;

            if ((moffset < 0 && iTopLeftTile > localartlookupnum-nDisplayedTiles-1)
                    || (moffset > 0 && iTopLeftTile==0))
            {
                moffset=0;
                searchy -= mousedy*2;
            }

            while (moffset > zoomsz)
            {
                iTopLeftTile -= nXTiles;
                moffset -= zoomsz;
            }
            while (moffset < -zoomsz)
            {
                iTopLeftTile += nXTiles;
                moffset += zoomsz;
            }
        }

        // Keep the pointer visible at all times.
        temp = min(zoomsz/2, 12);
        inpclamp(&searchx, temp, xdim-temp);
        inpclamp(&searchy, temp, ydim-temp);

        scrollmode = !(eitherCTRL^revertCTRL);
        if (bstatus&16 && scrollmode && iTopLeftTile > 0)
        {
            mouseb &= ~16;
            iTopLeftTile -= (nXTiles*scrollamount);
        }
        else if (bstatus&32 && scrollmode && iTopLeftTile < localartlookupnum-nDisplayedTiles-1)
        {
            mouseb &= ~32;
            iTopLeftTile += (nXTiles*scrollamount);
        }

        mtile = iTile = searchx/zoomsz + ((searchy-moffset)/zoomsz)*nXTiles + iTopLeftTile;
        while (iTile >= iTopLeftTile + nDisplayedTiles)
        {
            iTile -= nXTiles;
            mtile = iTile;
        }

        // These two lines are so obvious I don't need to comment them ...;-)
        synctics = totalclock-lockclock;
        lockclock += synctics;

        // Zoom in / out using numeric key pad's / and * keys
        if (((keystatus[KEYSC_gSLASH] || (!scrollmode && bstatus&16)) && s_Zoom<(signed)(NUM_ZOOMS-1))
                || ((keystatus[KEYSC_gSTAR]  || (!scrollmode && bstatus&32)) && s_Zoom>0))
        {
            if (PRESSED_KEYSC(gSLASH) || (!scrollmode && bstatus&16))
            {
                mouseb &= ~16;
                bstatus &= ~16;

                // Watch out : If editor window is small, then the next zoom level
                //  might get so large that even one tile might not fit !
                if (ZoomToThumbSize[s_Zoom+1]<=xdim && ZoomToThumbSize[s_Zoom+1]<=ydim)
                {
                    // Phew, plenty of room.
                    s_Zoom++;
                }
            }
            else
            {
                keystatus[KEYSC_gSTAR] = 0;
                mouseb &= ~32;
                bstatus &= ~32;
                s_Zoom--;
            }

            zoomsz = ZoomToThumbSize[s_Zoom];

            if (iTile >= localartlookupnum)
                iTile = localartlookupnum-1;

            // Calculate new num of tiles to display
            nXTiles = xdim / zoomsz;
            nYTiles = ydim / zoomsz;
            // Refuse to draw less than half of a row.
            if (zoomsz/2 < 12)
                nYTiles--;
            nDisplayedTiles  = nXTiles * nYTiles;

            // Determine if the top-left displayed tile needs to
            //   alter in order to display selected tile
            iTopLeftTile = iTile - (iTile % nXTiles);
            iTopLeftTile = clamp(iTopLeftTile, 0, MAXTILES - nDisplayedTiles);

            // scroll window so mouse points the same tile as it was before zooming
            iTopLeftTile -= searchx/zoomsz + ((searchy-moffset)/zoomsz)*nXTiles + iTopLeftTile-iTile;
        }

        if (PRESSED_KEYSC(LEFT))
        {
            if (eitherCTRL)  // same as HOME, for consistency with CTRL-UP/DOWN
                iTile = (iTile/nXTiles)*nXTiles;
            else
                iTile--;
        }

        if (PRESSED_KEYSC(RIGHT))
        {
            if (eitherCTRL)  // same as END, for consistency with CTRL-UP/DOWN
                iTile = ((iTile+nXTiles)/nXTiles)*nXTiles - 1;
            else
                iTile++;
        }

        if (PRESSED_KEYSC(UP))
        {
            if (eitherCTRL)
                while (iTile-nXTiles >= iTopLeftTile)
                    iTile -= nXTiles;
            else
                iTile -= nXTiles;
        }

        if (PRESSED_KEYSC(DOWN))
        {
            if (eitherCTRL)
                while (iTile+nXTiles < iTopLeftTile + nDisplayedTiles)
                    iTile += nXTiles;
            else
                iTile += nXTiles;
        }

        if (PRESSED_KEYSC(PGUP))
        {
            if (eitherCTRL)
                iTile = 0;
            else
                iTile -= nDisplayedTiles;
        }

        if (PRESSED_KEYSC(PGDN))
        {
            if (eitherCTRL)
                iTile = localartlookupnum-1;
            else
                iTile += nDisplayedTiles;
        }

        if (PRESSED_KEYSC(HOME))
        {
            if (eitherCTRL)
                iTile = iTopLeftTile;
            else
                iTile = (iTile/nXTiles)*nXTiles;
        }

        if (PRESSED_KEYSC(END))
        {
            if (eitherCTRL)
                iTile = iTopLeftTile + nDisplayedTiles - 1;
            else
                iTile = ((iTile+nXTiles)/nXTiles)*nXTiles - 1;
        }

        // 'V'  KEYPRESS
        if (PRESSED_KEYSC(V))
            iTile = SelectAllTiles(iTile);

        // 'G'  KEYPRESS - Goto frame
        if (PRESSED_KEYSC(G))
        {
            if (eitherCTRL)
            {
                if (OnSaveTileGroup() == 0)
                {
//                    iTile = SelectAllTiles(iTile);
                    Bmemset(tilemarked, 0, sizeof(tilemarked));
                    mark_lastk = -1;
                    noTilesMarked = 1;
                }
            }
            else
                iTile = OnGotoTile(iTile);
        }

        // 'U'  KEYPRESS : go straight to user defined art
        if (PRESSED_KEYSC(U))
        {
            SelectAllTiles(iTile);
            iTile = FIRST_USER_ART_TILE;
        }

        // 'A'  KEYPRESS : Go straight to start of Atomic edition's art
        if (PRESSED_KEYSC(A))
        {
            SelectAllTiles(iTile);
            iTile = FIRST_ATOMIC_TILE;
        }

        // 'E'  KEYPRESS : Go straight to start of extended art
        if (PRESSED_KEYSC(E))
        {
            SelectAllTiles(iTile);

            if (iTile == FIRST_EXTENDED_TILE)
                iTile = SECOND_EXTENDED_TILE;
            else iTile = FIRST_EXTENDED_TILE;
        }

        // 'T' KEYPRESS = Select from pre-defined tileset
        if (PRESSED_KEYSC(T))
            iTile = OnSelectTile(iTile);

        if (PRESSED_KEYSC(Z))
            s_TileZoom = !s_TileZoom;

        //
        // Ensure tilenum is within valid range
        //
        iTile = clamp(iTile, 0, min(MAXTILES-1, localartlookupnum+nDisplayedTiles-1));

        // 'S' KEYPRESS: search for named tile
        if (PRESSED_KEYSC(S))
        {
            static char laststr[25] = "";
            const char *searchstr = getstring_simple("Search for tile name: ", laststr, sizeof(names[0])-1, 1);
            static char buf[2][25];

            if (searchstr && searchstr[0])
            {
                int32_t i, i0, slen=Bstrlen(searchstr)-1;

                Bstrncpyz(laststr, searchstr, 25);
                i0 = localartlookup[iTile];

                Bmemcpy(buf[0], laststr, 25);
                Bstrupr(buf[0]);

                for (i=(i0+1)%MAXTILES; i!=i0; i=(i+1)%MAXTILES)
                {
                    Bmemcpy(buf[1], names[i], 25);
                    buf[1][24]=0;
                    Bstrupr(buf[1]);

                    if ((searchstr[0]=='^' && !Bstrncmp(buf[1], buf[0]+1, slen)) ||
                        (searchstr[0]!='^' && Bstrstr(buf[1], buf[0])))
                    {
                        SelectAllTiles(i);
                        iTile = i;
                        break;
                    }
                }
            }

            mousex = mousey = mouseb = 0;
        }

        //
        //      Adjust top-left to ensure tilenum is within displayed range of tiles
        //

        while (iTile < iTopLeftTile - (moffset<0)?nXTiles:0)
            iTopLeftTile -= nXTiles;

        while (iTile >= iTopLeftTile + nDisplayedTiles)
            iTopLeftTile += nXTiles;

        iTopLeftTile = clamp(iTopLeftTile, 0, MAXTILES - nDisplayedTiles);


        // SPACE keypress: mark/unmark selected tile
        if (PRESSED_KEYSC(SPACE))
        {
            if (iTile < localartlookupnum && IsValidTile(localartlookup[iTile]))
            {
                if (keystatus[KEYSC_LCTRL] && keystatus[KEYSC_RSHIFT])
                {
                    Bmemset(tilemarked, 0, sizeof(tilemarked));
                    mark_lastk = -1;
                    noTilesMarked = 1;
                }
                else
                {
                    int32_t k=iTile, kend, dir;

                    if (noTilesMarked)
                    {
                        noTilesMarked = 0;
                        TMPERRMSG_PRINT("Beginning marking tiles. To group, press Ctrl-G."
                                        " To reset, press LCtrl-RShift-SPACE.");
                    }

                    if (mark_lastk>=0 && eitherCTRL)
                    {
                        kend = mark_lastk;
                        dir = ksgn(mark_lastk-k);
                    }
                    else
                    {
                        kend = k;
                        dir = 0;
                    }

                    mark_lastk = k;

                    for (; dir==0 || dir*(kend-k)>=1; k+=dir)
                    {
                        tilemarked[localartlookup[k]>>3] ^= (1<<(localartlookup[k]&7));
                        if (dir==0)
                            break;
                    }
                }
            }
        }

        if ((keystatus[KEYSC_ENTER] || (bstatus&1)) == 0)   // uh ? Not escape key ?
        {
            idSelectedTile = idInitialTile;
        }
        else
        {
            if (iTile < localartlookupnum)
            {
                // Convert tile num from index to actual tile num
                idSelectedTile = localartlookup[iTile];

                // Check : if invalid tile selected, return original tile num
                if (!IsValidTile(idSelectedTile))
                    idSelectedTile = idInitialTile;
            }
            else
            {
                idSelectedTile = idInitialTile;
            }
        }
        if (mtile!=iTile) // if changed by keyboard, update mouse cursor
        {
            searchx = ((iTile-iTopLeftTile)%nXTiles) * zoomsz + zoomsz/2;
            searchy = ((iTile-iTopLeftTile)/nXTiles) * zoomsz + zoomsz/2 + moffset;
        }
    }

    searchx=omousex;
    searchy=omousey;

    keystatus[KEYSC_ESC] = 0;
    keystatus[KEYSC_ENTER] = 0;

    popDisableFog();

    return idSelectedTile;
}

// Dir = 0 (zoom out) or 1 (zoom in)
//void OnZoomInOut( int32_t *pZoom, int32_t Dir /*0*/ )
//{
//}

static int32_t OnSaveTileGroup(void)
{
    int32_t i, n=0;
    char hotkey;
    const char *cp, *name;

    if (tile_groups==MAX_TILE_GROUPS)
        TMPERRMSG_RETURN("Cannot save tile group: maximum number of groups (%d) exceeded.", MAX_TILE_GROUPS);

    for (i=0; i<MAXTILES; i++)
        n += !!(tilemarked[i>>3]&(1<<(i&7)));

    if (n==0)
        TMPERRMSG_RETURN("Cannot save tile group: no tiles marked.");
    else if (n > MAX_TILE_GROUP_ENTRIES)
        TMPERRMSG_RETURN("Cannot save tile group: too many tiles in group. Have %d, max is %d.",
                  n, MAX_TILE_GROUP_ENTRIES);

    cp = getstring_simple("Hotkey for new group: ", "", 1, 0);
    if (!cp || !*cp)
        return 1;

    hotkey = Btoupper(cp[0]);
    if (!isalpha(hotkey))
        TMPERRMSG_RETURN("Hotkey must be alphabetic.");

    for (i=0; i<tile_groups; i++)
        if (s_TileGroups[i].key1==hotkey || s_TileGroups[i].key2==Btolower(hotkey))
            TMPERRMSG_RETURN("Hotkey '%c' already in use by tile group `%s'.", hotkey, s_TileGroups[i].szText);

    name = getstring_simple("Name for new tile group: ", "", 63, 0);
    if (!name || !*name)
        return 1;

    for (i=0; name[i]; i++)
        if (!(isalnum(name[i]) || name[i]==' ' || name[i]==','))
            TMPERRMSG_RETURN("Name may only consist of alphabetic, numeric and space characters.");

    {
        int32_t lasti=-1, col=0, j, k, opathsearchmode=pathsearchmode;
        BFILE *fp;

        pathsearchmode = pathsearchmode_oninit;  // use the same pathsearchmode as on init
        fp = fopenfrompath(default_tiles_cfg, "a");
        pathsearchmode = opathsearchmode;
        if (!fp)
            TMPERRMSG_RETURN("Could not open `%s' for writing: %s.", default_tiles_cfg, strerror(errno));
        if (fseek(fp, 0, BSEEK_END))  // seems to be necessary even though we fopen with "a"
            TMPERRMSG_RETURN("Could not seek to end of file `%s'.", default_tiles_cfg);

#define TTAB "\t"
#define TBITCHK(i) ((i)<MAXTILES && (tilemarked[(i)>>3]&(1<<((i)&7))))
        Bfprintf(fp, OURNEWL);
        Bfprintf(fp, "tilegroup \"%s\"" OURNEWL"{" OURNEWL, name);
        Bfprintf(fp, TTAB "hotkey \"%c\"" OURNEWL OURNEWL, hotkey);

        if (!(s_TileGroups[tile_groups].pIds = (int32_t *)Bmalloc(n * sizeof(s_TileGroups[tile_groups].pIds[0]))))
            TMPERRMSG_RETURN("Out of memory.");

        j = 0;
        // tileranges for consecutive runs of 3 or more tiles
        for (i=0; i<MAXTILES; i++)
        {
            if (lasti>=0 && !TBITCHK(i))
            {
                if (names[lasti][0] && names[i-1][0])
                    Bfprintf(fp, TTAB "tilerange %s %s" OURNEWL, names[lasti], names[i-1]);
                else
                    Bfprintf(fp, TTAB "tilerange %d %d" OURNEWL, lasti, i-1);

                for (k=lasti; k<i; k++)
                {
                    s_TileGroups[tile_groups].pIds[j++] = k;
                    tilemarked[k>>3] &= ~(1<<(k&7));
                }

                lasti = -1;
            }
            else if (lasti==-1 && TBITCHK(i))
            {
                if (TBITCHK(i+1) && TBITCHK(i+2))
                {
                    lasti = i;
                    i += 2;
                }
            }
        }
        if (lasti>=0 && lasti<=MAXTILES-3)
        {
            for (k=lasti; k<MAXTILES; k++)
            {
                s_TileGroups[tile_groups].pIds[j++] = k;
                tilemarked[k>>3] &= ~(1<<(k&7));
            }
            Bfprintf(fp, TTAB "tilerange %d %d" OURNEWL, lasti, MAXTILES-1);
        }
        Bfprintf(fp, OURNEWL);

        k = 0;
        for (i=0; i<MAXTILES; i++)
            if (tilemarked[i>>3]&(1<<(i&7)))
            {
                k = 1;
                break;
            }

        if (k)
        {
            // throw them all in a tiles{...} group else
            Bfprintf(fp, TTAB "tiles\n" TTAB "{" OURNEWL);
            for (i=0; i<MAXTILES; i++)
            {
                if (TBITCHK(i))
                {
                    s_TileGroups[tile_groups].pIds[j++] = i;

                    if (col==0)
                        Bfprintf(fp, TTAB TTAB), col+=8;

                    if (names[i][0])
                        col+=Bfprintf(fp, "%s ", names[i]);
                    else
                        col+=Bfprintf(fp, "%d ", i);

                    if (col>80)
                    {
                        Bfprintf(fp, OURNEWL);
                        col = 0;
                    }
                }
            }
            if (col>0)
                Bfprintf(fp, OURNEWL);
            Bfprintf(fp, TTAB "}" OURNEWL);
        }
#undef TBITCHK
#undef TTAB
        Bfprintf(fp, "}" OURNEWL);

        Bfclose(fp);

        if (!(s_TileGroups[tile_groups].szText = Bstrdup(name)))
        {
            Bfree(s_TileGroups[tile_groups].pIds);
            TMPERRMSG_RETURN("Out of memory.");
        }

        s_TileGroups[tile_groups].nIds = n;
        s_TileGroups[tile_groups].key1 = Btoupper(hotkey);
        s_TileGroups[tile_groups].key2 = Btolower(hotkey);
        s_TileGroups[tile_groups].color1 = s_TileGroups[tile_groups].color2 = 0;
        tile_groups++;

        TMPERRMSG_PRINT("Wrote and installed new tile group.");
    }

    return 0;
}


static int32_t OnGotoTile(int32_t iTile)
{
    //Automatically press 'V'
    iTile = SelectAllTiles(iTile);

    return getnumber256("Goto tile: ", 0, MAXTILES-1, 0+2+16);
}


static int32_t LoadTileSet(const int32_t idCurrentTile, const int32_t *pIds, const int32_t nIds)
{
    int32_t iNewTile = 0;
    int32_t i;

    localartlookupnum = nIds;

    for (i = 0; i < localartlookupnum; i++)
    {
        localartlookup[i] = pIds[i];
        // REM : Could we still utilise localartfreq[] to mark
        //  which tiles are currently used in the map ? Set to 0xFFFF perhaps ?
        localartfreq[i] = 0;

        if (idCurrentTile == pIds[i])
            iNewTile = i;
    }

    return iNewTile;
}

static int32_t OnSelectTile(int32_t iTile)
{
    int32_t bDone = 0;
    int32_t i;
    char ch;

    if (tile_groups <= 0)
    {
        TMPERRMSG_PRINT("No tile groups loaded. Check for existence of `%s'.", default_tiles_cfg);
        return iTile;
    }

    SelectAllTiles(iTile);

    bflushchars();

    setpolymost2dview();
#ifdef USE_OPENGL
    bglEnable(GL_TEXTURE_2D);
#endif
    clearview(0);

    //
    //  Await appropriate selection keypress.
    //

    bDone = 0;

    while (keystatus[KEYSC_ESC] == 0 && !bDone)
    {
        if (handleevents())
            quitevent = 0;

        idle_waitevent();

        //
        // Display the description strings for each available tile group
        //
        for (i = 0; i < tile_groups; i++)
        {
            if (s_TileGroups[i].szText != NULL)
            {
                if ((i+2)*16 > ydimgame) break;
                Bsprintf(tempbuf,"(%c) %s",s_TileGroups[i].key1,s_TileGroups[i].szText);
                printext256(10L, (i+1)*16, whitecol, -1, tempbuf, 0);
            }
        }
        showframe(1);

        ch = bgetchar();

        for (i = 0; i < tile_groups; i++)
        {
            if (s_TileGroups[i].pIds != NULL && s_TileGroups[i].key1)
                if ((ch == s_TileGroups[i].key1) || (ch == s_TileGroups[i].key2))
                {
                    iTile = LoadTileSet(iTile, s_TileGroups[i].pIds, s_TileGroups[i].nIds);
                    bDone = 1;
                }
        }
    }

    showframe(1);

    clearkeys();

    return iTile;
}

static const char *GetTilePixels(int32_t idTile)
{
    char *pPixelData = 0;

    if (idTile >= 0 && idTile < MAXTILES)
    {
        if (!waloff[idTile])
            loadtile(idTile);

        if (IsValidTile(idTile))
            pPixelData = (char *)waloff[idTile];
    }

    return pPixelData;
}

static void classic_drawtilescreen(int32_t x, int32_t y, int32_t idTile, int32_t TileDim,
                                   const char *pRawPixels)
{
    int32_t dispxsz = tilesizx[idTile], dispysz = tilesizy[idTile];
    int32_t divinc = 1, mulinc = 1;

    int32_t xofs, yofs;
    char *pScreen;

    while ((dispxsz/divinc > TileDim) || (dispysz/divinc) > TileDim)
    {
        divinc++;
    }

    if (divinc == 1 && s_TileZoom)
    {
        while ((dispxsz*(mulinc+1)) <= TileDim && (dispysz*(mulinc+1)) <= TileDim)
        {
            mulinc++;
        }
    }

    dispxsz = (dispxsz / divinc) * mulinc;
    dispysz = (dispysz / divinc) * mulinc;

    for (yofs = 0; yofs < dispysz; yofs++)
    {
        y += yofs;
        if (y>=0 && y<ydim)
        {
            pScreen = (char *)ylookup[y]+x+frameplace;
            for (xofs = 0; xofs < dispxsz; xofs++)
            {
                pScreen[xofs] = pRawPixels[((yofs*divinc)/mulinc) + (((xofs*divinc)/mulinc)*tilesizy[idTile])];
            }
        }
        y -= yofs;
    }
}

static void tilescreen_drawbox(int32_t iTopLeft, int32_t iSelected, int32_t nXTiles,
                               int32_t TileDim, int32_t offset,
                               int32_t iTile, int32_t idTile)
{
    int32_t marked = (IsValidTile(idTile) && tilemarked[idTile>>3]&(1<<(idTile&7)));

    //
    // Draw white box around currently selected tile or marked tile
    // p1=(x1, y1), p2=(x1+TileDim-1, y1+TileDim-1)
    //
    if (iTile == iSelected || marked)
    {
        int32_t x1 = ((iTile-iTopLeft) % nXTiles)*TileDim;
        int32_t y1 = ((iTile - ((iTile-iTopLeft) % nXTiles) - iTopLeft)/nXTiles)*TileDim + offset;
        int32_t x2 = x1+TileDim-1;
        int32_t y2 = y1+TileDim-1, oydim16=ydim16;

        char markedcol = editorcolors[14];

        setpolymost2dview();

        y1=max(y1, 0);
        y2=min(y2, ydim-1);

        // plotlines2d uses drawline16, which clips against ydim16...
        ydim16 = ydim;

        {
            // box
            int32_t xx[] = {x1, x1, x2, x2, x1};
            int32_t yy[] = {y1, y2, y2, y1, y1};
            plotlines2d(xx, yy, 5, iTile==iSelected ? whitecol : markedcol);
        }

        // cross
        if (marked)
        {
            int32_t xx[] = {x1, x2};
            int32_t yy[] = {y1, y2};

            plotlines2d(xx, yy, 2, markedcol);
            swaplong(&yy[0], &yy[1]);
            plotlines2d(xx, yy, 2, markedcol);
        }

        ydim16 = oydim16;
    }
}

static void tilescreen_drawrest(int32_t iSelected, int32_t showmsg)
{
    if (iSelected>=0 && iSelected<MAXTILES)
    {
        int32_t idTile = localartlookup[iSelected];
        int32_t i;
        char szT[128];

        // Draw info bar at bottom.

        // Clear out behind the text for improved visibility.
        //drawline256(0, (ydim-12)<<12, xdim<<12, (ydim-12)<<12, whitecol);
        for (i=ydim-12; i<ydim; i++)
            drawline256(0, i<<12, xdim<<12, i<<12, (ydim-i));

        // Tile number on left.
        Bsprintf(szT, "%d" , idTile);
        printext256(1, ydim-10, whitecol, -1, szT, 0);

        // Tile name on right.
        printext256(xdim-(Bstrlen(names[idTile])<<3)-1,ydim-10,whitecol,-1,names[idTile],0);

        // Tile dimensions.
        Bsprintf(szT,"%dx%d",tilesizx[idTile],tilesizy[idTile]);
        printext256(xdim>>2,ydim-10,whitecol,-1,szT,0);

        // EditArt offset flags.
        Bsprintf(szT,"%d, %d", picanm[idTile].xofs, picanm[idTile].yofs);
        printext256((xdim>>2)+100,ydim-10,whitecol,-1,szT,0);

        // EditArt animation flags.
        if (picanm[idTile].sf&PICANM_ANIMTYPE_MASK)
        {
            static const char *anmtype[] = {"", "Osc", "Fwd", "Bck"};
            int32_t ii = (picanm[idTile].sf&PICANM_ANIMTYPE_MASK)>>PICANM_ANIMTYPE_SHIFT;

            Bsprintf(szT,"%s %d", anmtype[ii], picanm[idTile].num);
            printext256((xdim>>2)+100+14*8,ydim-10,whitecol,-1,szT,0);
        }
    }

    if (showmsg)
        TMPERRMSG_SHOW(0);

    m32_showmouse();
}

////////// main tile drawing function //////////
static int32_t DrawTiles(int32_t iTopLeft, int32_t iSelected, int32_t nXTiles, int32_t nYTiles,
                         int32_t TileDim, int32_t offset, int32_t showmsg)
{
    int32_t XTile, YTile;
    int32_t iTile, idTile;
    int32_t i, x, y;
    const char *pRawPixels;
    char szT[128];
#ifdef USE_OPENGL
    int32_t lazyselector = g_lazy_tileselector && usehightile;
    int32_t usehitile;
#else
    int32_t lazyselector = g_lazy_tileselector;
#endif
    int32_t runi=0;
    static uint8_t loadedhitile[(MAXTILES+7)>>3];

#ifdef USE_OPENGL
    setpolymost2dview();

    if (getrendermode() >= REND_POLYMOST)
    {
        bglEnable(GL_TEXTURE_2D);

        if (lazyselector)
            bglDrawBuffer(GL_FRONT_AND_BACK);
    }
#endif
    clearview(0);

    begindrawing();

restart:
    for (YTile = 0-(offset>0); YTile < nYTiles+(offset<0)+1; YTile++)
    {
        for (XTile = 0; XTile < nXTiles; XTile++)
        {
            iTile = iTopLeft + XTile + (YTile * nXTiles);

            if (iTile < 0 || iTile >= localartlookupnum)
                continue;
#ifdef USE_OPENGL
            usehitile = (runi || !lazyselector);
#endif

            idTile = localartlookup[ iTile ];
            if (loadedhitile[idTile>>3]&(1<<(idTile&7)))
            {
                if (runi==1)
                    continue;

#ifdef USE_OPENGL
                usehitile = 1;
#endif
            }

            // Get pointer to tile's raw pixel data
            pRawPixels = GetTilePixels(idTile);

            if (pRawPixels != NULL)
            {
                x = XTile * TileDim;
                y = YTile * TileDim+offset;

#ifdef USE_OPENGL
                if (polymost_drawtilescreen(x, y, idTile, TileDim, s_TileZoom,
                                            usehitile, loadedhitile))
#endif
                    classic_drawtilescreen(x, y, idTile, TileDim, pRawPixels);

                if (localartfreq[iTile] != 0 && y >= 0 && y <= ydim-20)
                {
                    Bsprintf(szT, "%d", localartfreq[iTile]);
                    printext256(x, y, whitecol, -1, szT, 1);
                }
            }

            tilescreen_drawbox(iTopLeft, iSelected, nXTiles, TileDim, offset, iTile, idTile);

            if (runi==1 && lazyselector)
            {
                int32_t k;

                if (handleevents())
                    quitevent=0;

                tilescreen_drawrest(iSelected, showmsg);

                k = (mousex || mousey || mouseb);
                if (!k)
                    for (i=0; i<(signed)(sizeof(keystatus)/sizeof(keystatus[0])); i++)
                        if (keystatus[i])
                        {
                            k = 1;
                            break;
                        }
                if (k)
                {
                    enddrawing();
                    showframe(1);
#ifdef USE_OPENGL
                    if (getrendermode() >= REND_POLYMOST && lazyselector)
                        bglDrawBuffer(GL_BACK);
#endif
                    return 1;
                }

                enddrawing();
                showframe(1);
                begindrawing();
            }
        }
    }

    tilescreen_drawrest(iSelected, showmsg);

    if (getrendermode() >= REND_POLYMOST && in3dmode() && lazyselector)
    {
        if (runi==0)
        {
            enddrawing();
            showframe(1);
            begindrawing();

            runi = 1;
            goto restart;
        }
    }

    enddrawing();
    showframe(1);

#ifdef USE_OPENGL
    if (getrendermode() >= REND_POLYMOST && lazyselector)
        bglDrawBuffer(GL_BACK);
#endif

    return 0;

}

#undef TMPERRMSG_SHOW
#undef TMPERRMSG_PRINT
#undef TMPERRMSG_RETURN

#define WIND1X   3
#define WIND1Y 150

static char *tileinfo_colorstr = "";

static void tileinfo_doprint(int32_t x, int32_t y, char *buf, const char *label, int32_t value, int32_t pos)
{
    int32_t small = (xdimgame<=640), i = ydimgame>>6;
    Bsprintf(buf,"%s:%s%4d", label, tileinfo_colorstr, value);
    printext256(x+2, y+2+i*pos, 0, -1, buf, small);
    printext256(x, y+i*pos, whitecol, -1, buf, small);
}

// flags: 1:draw asterisk for lotag
//        2:draw asterisk for extra
//        4:print bottom-swapped wall members colored
static void drawtileinfo(const char *title,int32_t x,int32_t y,int32_t picnum,int32_t shade,int32_t pal,int32_t cstat,
                         int32_t lotag,int32_t hitag,int32_t extra, uint32_t flags)
{
    char buf[64];
    int32_t small = (xdimgame<=640);
    int32_t oviewingrange=viewingrange, oyxaspect=yxaspect;

    if (tilesizx[picnum]>0 && tilesizy[picnum]>0)
    {
        double scalediv;
        int32_t scale=65536;
        int32_t x1 = x+80;

        if (small)
            x1 /= 2;

        x1 = (int32_t)(x1 * 320.0/xdimgame);
        scalediv = max(tilesizx[picnum],tilesizy[picnum])/24.0;
        scale = (int32_t)((double)scale/scalediv);

        setaspect(65536L, (int32_t)divscale16(ydim*320L,xdim*200L));
        // +1024: prevents rotatesprite from setting aspect itself
        rotatesprite_fs((x1+13)<<16,(y+11)<<16,scale,0, picnum,shade,pal, 2+1024);
        setaspect(oviewingrange, oyxaspect);
    }

    x = (int32_t)(x * xdimgame/320.0);
    y = (int32_t)(y * ydimgame/200.0);

    begindrawing();
    printext256(x+2,y+2,0,-1,title,small);
    printext256(x,y,255-13,-1,title,small);

    if (flags&4)
        tileinfo_colorstr = "^242";
    tileinfo_doprint(x, y, buf, "Pic", picnum, 1);
    tileinfo_doprint(x, y, buf, "Shd", shade, 2);
    tileinfo_doprint(x, y, buf, "Pal", pal, 3);
    tileinfo_doprint(x, y, buf, "Cst", cstat, 4);
    tileinfo_colorstr = "";
    tileinfo_doprint(x, y, buf, (flags&1)?"Lo*":"Lot", lotag, 5);
    tileinfo_doprint(x, y, buf, "Hit", hitag, 6);
    tileinfo_doprint(x, y, buf, (flags&2)?"Ex*":"Ext", extra, 7);

    enddrawing();
}
//int32_t snap=0;
//int32_t saveval1,saveval2,saveval3;

static inline void getnumber_dochar(char *ptr, int32_t num)
{
    *ptr = (char) num;
}

static inline void getnumber_doint16_t(int16_t *ptr, int32_t num)
{
    *ptr = (int16_t) num;
}

static inline void getnumber_doint32(int32_t *ptr, int32_t num)
{
    *ptr = (int32_t) num;
}

static inline void getnumber_doint64(int64_t *ptr, int32_t num)
{
    *ptr = (int64_t) num;
}

static void getnumberptr256(const char *namestart, void *num, int32_t bytes, int32_t maxnumber, char sign, void *(func)(int32_t))
{
    char buffer[80], ch;
    int32_t danum = 0, oldnum;
    uint8_t flags = (sign>>1)&3;
    sign &= 1;

    switch (bytes)
    {
    case 1:
        danum = sign ? *(int8_t *)num : *(uint8_t *)num;
        break;
    case 2:
        danum = sign ? *(int16_t *)num : *(uint16_t *)num;
        break;
    case 4:
        danum = *(int32_t *)num;
        break;
    case 8:
        danum = *(int64_t *)num;
        break;
    }

    oldnum = danum;
    bflushchars();
    while (keystatus[0x1] == 0)
    {
        if (handleevents())
            quitevent = 0;

        M32_DrawRoomsAndMasks();

        ch = bgetchar();

        if (keystatus[0x1]) break;

        clearkeys();

        mouseb = 0;
        searchx = osearchx;
        searchy = osearchy;

        inputchecked = 1;

        ExtCheckKeys();

        Bsprintf(buffer,"%s%d",namestart,danum);
        if (totalclock & 32) Bstrcat(buffer,"_ ");
        printmessage256(0, 0, buffer);
        if (func != NULL)
        {
            Bsprintf(buffer,"%s",(char *)func((int32_t)danum));
            printmessage256(0, 9, buffer);
        }
        showframe(1);

        if (getnumber_internal1(ch, &danum, maxnumber, sign) ||
            getnumber_autocomplete(namestart, ch, &danum, flags))
        {
            if (danum != oldnum)
                asksave = 1;
            oldnum = danum;
            break;
        }

        switch (bytes)
        {
        case 1:
            getnumber_dochar((char *)num, danum);
            break;
        case 2:
            getnumber_doint16_t((int16_t *)num, danum);
            break;
        case 4:
            getnumber_doint32((int32_t *)num, danum);
            break;
        case 8:
            getnumber_doint64((int64_t *)num, danum);
            break;
        }
    }

    clearkeys();

    lockclock = totalclock;  //Reset timing

    switch (bytes)
    {
    case 1:
        getnumber_dochar((char *)num, oldnum);
        break;
    case 2:
        getnumber_doint16_t((int16_t *)num, oldnum);
        break;
    case 4:
        getnumber_doint32((int32_t *)num, oldnum);
        break;
    case 8:
        getnumber_doint64((int64_t *)num, oldnum);
        break;
    }
}

#if 0
int64_t ldistsqr(spritetype *s1,spritetype *s2)
{
    return (((int64_t)(s2->x - s1->x))*((int64_t)(s2->x - s1->x)) +
            ((int64_t)(s2->y - s1->y))*((int64_t)(s2->y - s1->y)));
}
#endif

static void TextEntryMode(int16_t startspr)
{
    char ch, buffer[80], doingspace=0;
    int16_t daang = 0, t, alphidx, basetile, linebegspr, curspr, cursor;
    int32_t i, j, k, dax = 0, day = 0;
    static uint8_t hgap=0, vgap=4;
    static uint8_t spcgap[MAX_ALPHABETS], firstrun=1;
    spritetype *sp;

    int16_t *spritenums;
    int32_t stackallocsize=32, numletters=0;

    if (firstrun)
    {
        firstrun=0;
        for (i=0; i<numalphabets; i++)
            spcgap[i] = 0;
    }

    if (startspr<0 || startspr>=MAXSPRITES ||
            sprite[startspr].statnum == MAXSTATUS)
        return;

    if (numalphabets == 0)
    {
        message("Alphabet configuration not read.");
        return;
    }

    if ((sprite[startspr].cstat&16) == 0)
    {
        message("Must point at a wall-aligned text sprite.");
        return;
    }

    t = sprite[startspr].picnum;
    alphidx = -1;
    for (i=0; i<numalphabets; i++)
    {
        for (j=0; j<NUMPRINTABLES; j++)
            if (alphabets[i].pic[j] == t)
            {
                alphidx = i;
                basetile = t;
                if (spcgap[i] == 0)
                    spcgap[i] = 3*tilesizx[t]/2;
                goto ENDFOR1;
            }
    }
ENDFOR1:
    if (alphidx==-1)
    {
        message("Must point at a text sprite.");
        return;
    }

    curspr = linebegspr = startspr;

    t = sprite[startspr].picnum;
    sprite[startspr].xoffset = -picanm[t].xofs;
    sprite[startspr].yoffset = -picanm[t].yofs;

    spritenums = (int16_t *)Bmalloc(stackallocsize * sizeof(int16_t));
    if (!spritenums) goto ERROR_NOMEMORY;

    cursor = insertsprite(sprite[startspr].sectnum,0);
    if (cursor < 0) goto ERROR_TOOMANYSPRITES;

    sp = &sprite[cursor];
    Bmemcpy(sp, &sprite[startspr], sizeof(spritetype));
    sp->yoffset = 0;
    sp->picnum = SMALLFNTCURSOR;
    sp->xrepeat = clamp(sp->xrepeat/tilesizx[sp->picnum], 2, 255);
    sp->yrepeat = clamp((sp->yrepeat*tilesizy[sprite[startspr].picnum])/tilesizy[sp->picnum], 4, 255);
    sp->pal = 0;
    sp->cstat = 18;

    bflushchars();
    while (keystatus[0x1] == 0)
    {
        if (handleevents())
            quitevent = 0;


        if (PRESSED_KEYSC(UP))  // vertical gap in pixels (32 x-units)
            vgap += (vgap<255);

        if (PRESSED_KEYSC(DOWN))
            vgap -= (vgap>0);

        if (PRESSED_KEYSC(RIGHT))  // horizontal gap in half pixels
            hgap += (hgap<255);

        if (PRESSED_KEYSC(LEFT))
            hgap -= (hgap>0);

        if (PRESSED_KEYSC(INSERT))  // space gap in half pixels
            spcgap[alphidx] += (spcgap[alphidx]<255);

        if (PRESSED_KEYSC(DELETE))
            spcgap[alphidx] -= (spcgap[alphidx]>1);

        if (PRESSED_KEYSC(HOME))  // shade
            sprite[linebegspr].shade += (sprite[linebegspr].shade<127);

        if (PRESSED_KEYSC(END))
            sprite[linebegspr].shade -= (sprite[linebegspr].shade>-128);

        if (PRESSED_KEYSC(PGUP))  // pal
            sprite[linebegspr].pal += (sprite[linebegspr].pal<255);

        if (PRESSED_KEYSC(PGDN))
            sprite[linebegspr].pal -= (sprite[linebegspr].pal>0);

        M32_DrawRoomsAndMasks();

        ch = bgetchar();

        if (keystatus[0x1]) break;

        clearkeys();

        mouseb = 0;
        searchx = osearchx;
        searchy = osearchy;

        inputchecked = 1;

        ExtCheckKeys();

        printmessage256(0,0,"^251Text entry mode.^31 Navigation keys change vars.");
        Bsprintf_nowarn(buffer, "Hgap=%d, Vgap=%d, SPCgap=%d, Shd=%d, Pal=%d",
                 hgap, vgap, spcgap[alphidx], TrackerCast(sprite[linebegspr].shade), TrackerCast(sprite[linebegspr].pal));
        printmessage256(0, 9, buffer);
        showframe(1);

        // ---
        sp = &sprite[curspr];
        if (!doingspace)
        {
            dax = sp->x; day = sp->y;
            daang = sp->ang;
        }

        j = sp->xrepeat*(hgap+tilesizx[sp->picnum]+2);
        {
            vec3_t vect;
            vect.x = dax + ((j*sintable[daang])>>17);
            vect.y = day - ((j*sintable[(daang+512)&2047])>>17);
            vect.z = sp->z;
            setsprite(cursor,&vect);
        }

        if (ch>=33 && ch<=126 && alphabets[alphidx].pic[ch-33] >= 0)
        {
            int16_t sect;

            // mapping char->tilenum
            t = alphabets[alphidx].pic[ch-33];
            j = sp->xrepeat*(hgap+tilesizx[sp->picnum]+tilesizx[t]);

            dax += (j*sintable[daang])>>17;
            day -= (j*sintable[(daang+512)&2047])>>17;
            dax += (j*sintable[(sprite[curspr].ang+2560)&2047])>>17;
            day += (j*sintable[(sprite[curspr].ang+2048)&2047])>>17;

            sect = sprite[curspr].sectnum;
            updatesector(dax,day,&sect);
            if (Numsprites < MAXSPRITES && sect >= 0)
            {
                i = insertsprite(sect,0);
                Bmemcpy(&sprite[i], &sprite[linebegspr], sizeof(spritetype));
                sprite[i].sectnum = sect;

                sprite[i].x = dax, sprite[i].y = day;
                sprite[i].picnum = t;
                sprite[i].ang = daang;

                sprite[i].xoffset = -picanm[t].xofs;
                sprite[i].yoffset = -picanm[t].yofs;
                sprite[i].xoffset += alphabets[alphidx].xofs[(int32_t)ch-33];
                sprite[i].yoffset += alphabets[alphidx].yofs[(int32_t)ch-33];

                DoSpriteOrnament(i);

                for (k=0; k<MAXTILES; k++)
                    localartfreq[k] = 0;
                for (k=0; k<MAXSPRITES; k++)
                    if (sprite[k].statnum < MAXSTATUS)
                        localartfreq[sprite[k].picnum]++;

                curspr = i;
                doingspace = 0;

                asksave = 1;

                if (numletters >= stackallocsize)
                {
                    stackallocsize *= 2;
                    spritenums = (int16_t *)Brealloc(spritenums, stackallocsize*sizeof(int16_t));
                    if (!spritenums) goto ERROR_NOMEMORY;
                }
                spritenums[numletters++] = i;
            }
        }
        else if (ch == 32)
        {
            dax += ((sp->xrepeat*spcgap[alphidx]*sintable[daang])>>17);
            day -= ((sp->xrepeat*spcgap[alphidx]*sintable[(daang+512)&2047])>>17);
            doingspace = 1;
        }
        else if (ch == 8)  // backspace
        {
            if (doingspace)
                doingspace = 0;
            else if (numletters > 0)
            {
                int16_t last = spritenums[numletters-1];

                if (sprite[last].z != sprite[linebegspr].z)  // only "delete" line break
                {
                    sprite[linebegspr].z = sprite[last].z;
                    curspr = last;
                }
                else if (numletters > 1)
                {
                    int16_t sectolast = spritenums[numletters-2];

                    if (sprite[last].z == sprite[sectolast].z)
                        curspr = sectolast;
                    else  // if we delete the first letter on the line
                        curspr = linebegspr;

                    numletters--;
                    deletesprite(last);

                    asksave = 1;
                }
                else
                {
                    numletters--;
                    deletesprite(last);

                    curspr = linebegspr;
                    asksave = 1;
                }
            }
            else
            {
                sprite[linebegspr].z -= ((sprite[linebegspr].yrepeat*(vgap+tilesizy[basetile]))<<2);
                asksave = 1;
            }
        }
        else if (ch == 13)  // enter
        {
            sprite[linebegspr].z += ((sprite[linebegspr].yrepeat*(vgap+tilesizy[basetile]))<<2);
            curspr = linebegspr;
            doingspace = 0;
            asksave = 1;
        }
    }

ERROR_TOOMANYSPRITES:
    if (cursor < 0) message("Too many sprites in map!");
    else deletesprite(cursor);

ERROR_NOMEMORY:
    if (spritenums) Bfree(spritenums);
    else message("Out of memory!");

    clearkeys();

    lockclock = totalclock;  //Reset timing
}

static void mouseaction_movesprites(int32_t *sumxvect, int32_t *sumyvect, int32_t yangofs, int32_t mousexory)
{
    int32_t xvect,yvect, daxvect,dayvect, ii, spi;
    int32_t units, gridlock = (eitherCTRL && grid > 0 && grid < 9);
    spritetype *sp = &sprite[searchwall];
    int16_t tsect = sp->sectnum;
    vec3_t tvec = { sp->x, sp->y, sp->z };

    xvect = -((mousexory*(int32_t)sintable[(ang+yangofs+512)&2047])<<3);
    yvect = -((mousexory*(int32_t)sintable[(ang+yangofs)&2047])<<3);

    if (gridlock)
    {
        units = 1<<(11-grid);

        if ((tvec.x & (units-1)) || (tvec.y & (units-1)))
        {
            daxvect = ((tvec.x & ~(units-1)) - tvec.x)<<14;
            dayvect = ((tvec.y & ~(units-1)) - tvec.y)<<14;
        }
        else
        {
            units <<= 14;

            *sumxvect += xvect;
            *sumyvect += yvect;

            if (klabs(*sumxvect) >= units)
            {
                daxvect = ((*sumxvect)/units)*units;
                *sumxvect %= units;
            }
            else
                daxvect = 0;

            if (klabs(*sumyvect) >= units)
            {
                dayvect = ((*sumyvect)/units)*units;
                *sumyvect %= units;
            }
            else
                dayvect = 0;
        }
    }
    else
    {
        daxvect = xvect;
        dayvect = yvect;
    }

    if (highlightcnt<=0 || (show2dsprite[searchwall>>3] & (1<<(searchwall&7)))==0)
    {
        clipmove(&tvec, &tsect, daxvect,dayvect, sp->clipdist,64<<4,64<<4, spnoclip?1:CLIPMASK0);
        setsprite(searchwall, &tvec);
    }
    else
    {
        xvect = daxvect;
        yvect = dayvect;

        // test run
        for (ii=0; ii<highlightcnt; ii++)
            if (highlight[ii]&16384)
            {
                spi = highlight[ii]&16383;
                Bmemcpy(&tvec, &sprite[spi], sizeof(vec3_t));
                tsect = sprite[spi].sectnum;
                clipmove(&tvec, &tsect, xvect,yvect, 128,64<<4,64<<4, spnoclip?1:CLIPMASK0);

                xvect = (tvec.x - sprite[spi].x)<<14;
                yvect = (tvec.y - sprite[spi].y)<<14;
            }
        // the real thing
        for (ii=0; ii<highlightcnt; ii++)
            if (highlight[ii]&16384)
            {
                spi = highlight[ii]&16383;
                Bmemcpy(&tvec, &sprite[spi], sizeof(vec3_t));
                tsect = sprite[spi].sectnum;
                clipmove(&tvec, &tsect, xvect,yvect, 128,64<<4,64<<4, spnoclip?1:CLIPMASK0);
                setsprite(spi, &tvec);
            }
    }
}

static int32_t addtobyte(int8_t *byte, int32_t num)
{
    int32_t onum = *byte, clamped=0;
    if (onum + num != (int8_t)(onum + num))
        clamped = 1;
    if (!clamped)
        *byte = (onum + num);
    return clamped;
}

static void toggle_sprite_alignment(int32_t spritenum)
{
    static const char *aligntype[4] = { "view", "wall", "floor", "???" };

    int32_t i = sprite[spritenum].cstat;

    if ((i&48) < 32)
        i += 16;
    else
        i &= ~48;
    sprite[spritenum].cstat = i;

    message("Sprite %d now %s aligned", spritenum, aligntype[(i&48)/16]);
    asksave = 1;
}

static void toggle_cf_flipping(int32_t sectnum, int32_t floorp)
{
    static const int8_t next3[8] = { 6, 7, 4, 5, 0, 1, 3, 2 };  // 0->6->3->5->1->7->2->4->0
    static const int8_t prev3[8] = { 4, 5, 7, 6, 2, 3, 0, 1 };  // 0<-6<-3<-5<-1<-7<-2<-4<-0

    static const int16_t orient[8] = { 360, -360, -180, 180, -270, 270, 90, -90 };

    const int32_t search = floorp ? SEARCH_FLOOR : SEARCH_CEILING;

    uint16_t *const stat = &SECTORFLD(sectnum,stat, floorp);
    int32_t i = *stat;

    i = (i&0x4)+((i>>4)&3);
    i = eitherSHIFT ? prev3[i] : next3[i];
    message("Sector %d %s flip %d deg%s", sectnum, typestr[search],
            klabs(orient[i])%360, orient[i] < 0 ? " mirrored":"");
    i = (i&0x4)+((i&3)<<4);
    *stat &= ~0x34;
    *stat |= i;
    asksave = 1;
}

// vec2_t initializer from a 1st point of wall
#define WALLVEC_INITIALIZER(w) { POINT2(w).x-wall[w].x, POINT2(w).y-wall[w].y }

static int64_t lldotv2(const vec2_t *v1, const vec2_t *v2)
{
    return (int64_t)v1->x * v2->x + (int64_t)v1->y * v2->y;
}

#ifdef NEW_MAP_FORMAT
# define YAX_BIT_PROTECT 0
#else
# define YAX_BIT_PROTECT YAX_BIT
#endif

////////// KEY PRESS HANDLER IN 3D MODE //////////
static void Keys3d(void)
{
    int32_t i = 0, changedir,tsign; // ,count,nexti
    int32_t j, k, tempint = 0, hiz, loz;
    int32_t hihit, lohit;
    char smooshyalign=0, repeatpanalign=0; //, buffer[80];
    int16_t startwall, endwall, dasector; //, statnum=0;
    char tempbuf[128];

    /* start Mapster32 */

    if (g_numsounds > 0 && AmbienceToggle)
    {
        M32_MoveFX();
        S_Update();
    }

    if (usedcount && !helpon)
    {
#if 0
        if (!AIMING_AT_SPRITE)
        {
            count=0;
            for (i=0; i<numwalls; i++)
            {
                if (wall[i].picnum == temppicnum) count++;
                if (wall[i].overpicnum == temppicnum) count++;
            }
            for (i=0; i<numsectors; i++)
            {
                if (sector[i].ceilingpicnum == temppicnum) count++;
                if (sector[i].floorpicnum == temppicnum) count++;
            }
        }
        else
        {
            count=0;
            statnum=0;

            i = headspritestat[statnum];
            while (i != -1)
            {
                nexti = nextspritestat[i];
                if (sprite[i].picnum == temppicnum) count++;
                i = nexti;
            }
        }
#endif
        drawtileinfo("Clipboard",3,124,temppicnum,tempshade,temppal,tempcstat,templotag,temphitag,tempextra,0);
    }// end if usedcount

    if (searchsector > -1 && searchsector < numsectors)
    {
        char lines[8][64];
        int32_t num=0;
        int32_t x,y,flags=0;

        if (infobox&1)
        {
            int32_t height2 = sector[searchsector].floorz - sector[searchsector].ceilingz;

            switch (searchstat)
            {
            case SEARCH_WALL:
            case SEARCH_MASKWALL:
            {
                int32_t dist, height1=0, height3=0;
                const int32_t w = SELECT_WALL();
                const int32_t swappedbot = (int32_t)(w != searchwall);
                flags |= (swappedbot<<2);
#ifdef YAX_ENABLE__COMPAT
                flags |= (yax_getnextwall(searchwall, YAX_CEILING)>=0) + 2*(yax_getnextwall(searchwall, YAX_FLOOR)>=0);
#endif
                drawtileinfo("Current", WIND1X,WIND1Y,
                             AIMING_AT_WALL ? wall[w].picnum : wall[w].overpicnum,
                             wall[w].shade, wall[w].pal, wall[w].cstat,
                             wall[searchwall].lotag, wall[searchwall].hitag, wall[searchwall].extra, flags);

                dist = wallength(searchwall);

                if (wall[searchwall].nextsector >= 0 && wall[searchwall].nextsector < numsectors)
                {
                    int32_t nextsect = wall[searchwall].nextsector;
                    height1 = sector[searchsector].floorz - sector[nextsect].floorz;
                    height2 = sector[nextsect].floorz - sector[nextsect].ceilingz;
                    height3 = sector[nextsect].ceilingz - sector[searchsector].ceilingz;
                }

                Bsprintf_nowarn(lines[num++],"Panning:%s %d, %d", swappedbot?"^242":"",
                                TrackerCast(wall[w].xpanning), TrackerCast(wall[w].ypanning));
                Bsprintf_nowarn(lines[num++],"Repeat:  %d, %d", TrackerCast(wall[searchwall].xrepeat), TrackerCast(wall[searchwall].yrepeat));
                Bsprintf_nowarn(lines[num++],"Overpic: %d", TrackerCast(wall[searchwall].overpicnum));
                lines[num++][0]=0;

                if (getmessageleng)
                    break;

                if (searchwall != searchbottomwall)
                    Bsprintf(lines[num++],"^251Wall %d ->^242 %d", searchwall, searchbottomwall);
                else
                    Bsprintf(lines[num++],"^251Wall %d", searchwall);

                if (wall[searchwall].nextsector!=-1)
                    Bsprintf(lines[num++],"LoHeight:%d, HiHeight:%d, Length:%d",height1,height3,dist);
                else
                    Bsprintf(lines[num++],"Height:%d, Length:%d",height2,dist);
                break;
            }

            case SEARCH_CEILING:
            case SEARCH_FLOOR:
                drawtileinfo("Current", WIND1X, WIND1Y, AIMED_CEILINGFLOOR(picnum), AIMED_CEILINGFLOOR(shade),
                             AIMED_CEILINGFLOOR(pal), AIMED_CEILINGFLOOR(stat),
                             sector[searchsector].lotag, sector[searchsector].hitag, sector[searchsector].extra,0);

                {
                    int32_t xp=AIMED_CEILINGFLOOR(xpanning), yp=AIMED_CEILINGFLOOR(ypanning);
#ifdef YAX_ENABLE__COMPAT
                    int32_t notextended = 1;
                    if (yax_getbunch(searchsector, AIMING_AT_FLOOR) >= 0)
                        notextended = 0;
                    Bsprintf(lines[num++],"Panning:  %d%s, %d", xp, notextended?"":"*", yp);
#else
                    Bsprintf(lines[num++],"Panning:  %d, %d", xp, yp);
#endif
                }
                Bsprintf(lines[num++],"%sZ: %d", Typestr[searchstat], AIMED_CEILINGFLOOR(z));
                Bsprintf(lines[num++],"Slope:    %d", AIMED_CEILINGFLOOR(heinum));
                lines[num++][0]=0;

                if (getmessageleng)
                    break;

                Bsprintf(lines[num++],"^251Sector %d^31 %s, Lotag:%s", searchsector, typestr[searchstat], ExtGetSectorCaption(searchsector));
                Bsprintf_nowarn(lines[num++],"Height: %d, Visibility:%d", height2, TrackerCast(sector[searchsector].visibility));
                break;

            case SEARCH_SPRITE:
                drawtileinfo("Current", WIND1X, WIND1Y, sprite[searchwall].picnum, sprite[searchwall].shade,
                             sprite[searchwall].pal, sprite[searchwall].cstat, sprite[searchwall].lotag,
                             sprite[searchwall].hitag, sprite[searchwall].extra,0);

                Bsprintf_nowarn(lines[num++], "Repeat:  %d,%d",
                    TrackerCast(sprite[searchwall].xrepeat), TrackerCast(sprite[searchwall].yrepeat));
                Bsprintf_nowarn(lines[num++], "PosXY:   %d,%d%s",
                    TrackerCast(sprite[searchwall].x), TrackerCast(sprite[searchwall].y),
                         sprite[searchwall].xoffset|sprite[searchwall].yoffset ? " ^251*":"");
                Bsprintf_nowarn(lines[num++], "PosZ: ""   %d", TrackerCast(sprite[searchwall].z));// prevents tab character
                lines[num++][0]=0;

                if (getmessageleng)
                    break;

                if (sprite[searchwall].picnum<0 || sprite[searchwall].picnum>=MAXTILES)
                    break;

                if (names[sprite[searchwall].picnum][0])
                {
                    if (sprite[searchwall].picnum==SECTOREFFECTOR)
                        Bsprintf(lines[num++],"^251Sprite %d^31 %s", searchwall, SectorEffectorText(searchwall));
                    else
                        Bsprintf_nowarn(lines[num++],"^251Sprite %d^31 %s", searchwall, names[sprite[searchwall].picnum]);
                }
                else Bsprintf_nowarn(lines[num++],"^251Sprite %d^31, picnum %d", searchwall, TrackerCast(sprite[searchwall].picnum));

                Bsprintf(lines[num++], "Elevation:%d",
                         getflorzofslope(searchsector, sprite[searchwall].x, sprite[searchwall].y) - sprite[searchwall].z);
                break;
            }
        }

        x = (int32_t)(WIND1X*(xdimgame/320.));
        y = (int32_t)(WIND1Y*(ydimgame/200.));
        y += (ydimgame>>6)*8;

        if (getmessageleng)
        {
            while (num < 4)
                lines[num++][0] = 0;
            Bsprintf(lines[num++], "^251%s", getmessage);
        }

        begindrawing();
        for (i=0; i<num; i++)
        {
            printext256(x+2, y+2, 0, -1, lines[i], xdimgame<=640);
            printext256(x, y, whitecol, -1, lines[i], xdimgame<=640);
            y += ydimgame>>6;
        }
        enddrawing();
    }

    VM_OnEvent(EVENT_PREKEYS3D, -1);

    if (keystatus[KEYSC_QUOTE] && PRESSED_KEYSC(V)) // ' V
    {
        if (AIMING_AT_CEILING_OR_FLOOR)
            getnumberptr256("Sector visibility: ", &sector[searchsector].visibility, sizeof(sector[0].visibility), 255, 0, NULL);
    }

    if (keystatus[KEYSC_SEMI] && PRESSED_KEYSC(V))   // ; V
    {
        int16_t currsector;
        uint8_t visval;

        if (highlightsectorcnt == -1)
        {
            message("You didn't select any sectors!");
            return;
        }

        visval = getnumber256("Visibility of selected sectors: ", sector[searchsector].visibility, 255, 0);

        if (AskIfSure("Are you sure to change the visibility of all selected sectors?"))
            return;

        for (i=0; i<highlightsectorcnt; i++)
        {
            currsector = highlightsector[i];
            sector[currsector].visibility = visval;
        }

        message("Visibility changed on all selected sectors");
    }

    if (PRESSED_KEYSC(V))  //V
    {
        if (ASSERT_AIMING)
        {
            int32_t oldtile = AIMED_SELOVR_PICNUM;

            tempint = m32gettile(oldtile);
            AIMED_SELOVR_PICNUM = tempint;

            if (AIMING_AT_MASKWALL && wall[searchwall].nextwall >= 0)
                NEXTWALL(searchwall).overpicnum = tempint;

            if (AIMING_AT_SPRITE)
                correct_sprite_yoffset(searchwall);

            if (oldtile != tempint)
                asksave = 1;
        }
    }

    if (PRESSED_KEYSC(3))  /* 3 (toggle floor-over-floor (cduke3d only) */
    {
        floor_over_floor = !floor_over_floor;
        //        if (!floor_over_floor) ResetFOFSize();
        message("Floor-over-floor display %s",floor_over_floor?"enabled":"disabled");
    }

    if (PRESSED_KEYSC(F3))
    {
        mlook = !mlook;
        message("Mouselook: %s",mlook?"enabled":"disabled");
    }

    if (PRESSED_KEYSC(F4))
    {
        AmbienceToggle = !AmbienceToggle;
        message("Ambience sounds: %s",AmbienceToggle?"enabled":"disabled");
        if (!AmbienceToggle)
        {
            FX_StopAllSounds();
            S_ClearSoundLocks();
        }
    }

    // PK
    if (PRESSED_KEYSC(F5))
    {
        unrealedlook = !unrealedlook;
        message("UnrealEd mouse navigation: %s",unrealedlook?"enabled":"disabled");
    }

    if (keystatus[KEYSC_QUOTE] && PRESSED_KEYSC(DELETE)) // ' del
    {
        if (AIMING_AT_WALL_OR_MASK)
        {
#ifdef NEW_MAP_FORMAT
            wall[searchwall].cstat = 0;
#else
            wall[searchwall].cstat &= YAX_NEXTWALLBITS;
#endif
            message_nowarn("Wall %d cstat = %d", searchwall, TrackerCast(wall[searchwall].cstat));
        }
        else if (AIMING_AT_SPRITE)
        {
            sprite[searchwall].cstat = 0;
            message("Sprite %d cstat = 0", searchwall);
        }
    }

    // 'P and ;P - Will copy palette to all sectors/walls/sprites highlighted with R-Alt key
    if ((keystatus[KEYSC_QUOTE] || keystatus[KEYSC_SEMI]) && PRESSED_KEYSC(P))   // ' P  ; P
    {
        int16_t w, start_wall, end_wall, currsector;
        int8_t pal[4];

        if (highlightsectorcnt == -1)
        {
            message("You didn't select any sectors!");
            return;
        }

        if (keystatus[KEYSC_QUOTE])
        {
            pal[0] = getnumber256("Ceiling palette: ", -1, M32_MAXPALOOKUPS, 1);
            pal[1] = getnumber256("Floor palette: ", -1, M32_MAXPALOOKUPS, 1);
            pal[2] = getnumber256("Wall palette: ", -1, M32_MAXPALOOKUPS, 1);
            pal[3] = getnumber256("Sprite palette: ", -1, M32_MAXPALOOKUPS, 1);
        }
        else
        {
            pal[0] = getnumber256("Global palette: ", 0, M32_MAXPALOOKUPS, 0);
            pal[1] = pal[2] = pal[3] = pal[0];
        }

        if (AskIfSure(0)) return;

        for (i = 0; i < highlightsectorcnt; i++)
        {
            currsector = highlightsector[i];

            if (pal[0] > -1)
                sector[currsector].ceilingpal = pal[0];
            if (pal[1] > -1)
                sector[currsector].floorpal = pal[1];

            // Do all the walls in the sector
            start_wall = sector[currsector].wallptr;
            end_wall = start_wall + sector[currsector].wallnum;

            if (pal[2] > -1)
                for (w = start_wall; w < end_wall; w++)
                    wall[w].pal = pal[2];

            if (pal[3] > -1)
            {
                for (k=0; k<highlightsectorcnt; k++)
                    for (w=headspritesect[highlightsector[k]]; w >= 0; w=nextspritesect[w])
                        sprite[w].pal = pal[3];
            }
        }

        message("Palettes changed");
    }

    if (PRESSED_KEYSC(DELETE))
    {
        if (AIMING_AT_SPRITE)
        {
            deletesprite(searchwall);

            message("Sprite %d deleted",searchwall);
            if (AmbienceToggle)
            {
                clearbit(g_ambiencePlaying, searchwall);
                S_StopEnvSound(sprite[searchwall].lotag, searchwall);
            }
            asksave = 1;
        }
    }

    if (PRESSED_KEYSC(BS))  // backspace
    {
        if (AIMING_AT_WALL_OR_MASK)
        {
#ifdef YAX_ENABLE
            int16_t ynw, cf=-1;

            if (m32_script_expertmode)
            {
                if (eitherSHIFT && !eitherCTRL)
                    cf = 0;
                else if (!eitherSHIFT && eitherCTRL)
                    cf = 1;
            }

            if (cf >= 0)
            {
                // clear TROR uplink/downlink
                ynw = yax_getnextwall(searchwall, cf);
                if (ynw >= 0)
                {
                    yax_setnextwall(ynw, !cf, -1);
                    yax_setnextwall(searchwall, cf, -1);
                }

                message("Cleared wall %d's %s link to wall %d", searchwall, yupdownwall[cf], ynw);
            }
            else
#endif
            if (!eitherSHIFT && !eitherCTRL)
            {
                int32_t nw = wall[searchwall].nextwall;

                if ((unsigned)nw < (unsigned)numwalls)
                {
                    wall[nw].nextsector = wall[nw].nextwall = -1;
                    wall[searchwall].nextsector = wall[searchwall].nextwall = -1;

                    message("Cleared connection between wall %d and %d", searchwall, nw);
                }
            }
        }
    }

    if (PRESSED_KEYSC(F6))  //F6
    {
        autospritehelp = !autospritehelp;
        message("Automatic SECTOREFFECTOR help %s", autospritehelp?"enabled":"disabled");
    }
    if (PRESSED_KEYSC(F7))  //F7
    {
        autosecthelp = !autosecthelp;
        message("Automatic sector tag help %s", autosecthelp?"enabled":"disabled");
    }

    if (autospritehelp && helpon==0)
    {
        if (AIMING_AT_SPRITE && sprite[searchwall].picnum==SECTOREFFECTOR)
            ShowFileText("sehelp.hlp");
        else if (AIMING_AT_CEILING_OR_FLOOR)
            ShowFileText("sthelp.hlp");
    }

    // [.] or [,]: Search & fix panning to the right or left (3D)
    if (AIMING_AT_WALL_OR_MASK && ((tsign=PRESSED_KEYSC(PERIOD)) || PRESSED_KEYSC(COMMA)))
    {
        uint32_t flags = (!eitherSHIFT) | (tsign?0:16) |
            (eitherALT<<2) | ((!!keystatus[KEYSC_QUOTE])<<3) |
            32*(searchwall != searchbottomwall);

        int32_t naligned=AutoAlignWalls(searchwall, flags,