code:
#!/usr/bin/env luajit

local B = require("build")
local string = require("string")

local opt, fn
local do_canonicalize_sprite

if (arg[1] and arg[1]:sub(1,1)=="-") then
    opt = arg[1]
    fn = arg[2]
else
    fn = arg[1]
end

if (opt) then
    if (opt=="-c" or opt=="-C") then
        -- -C means to canonicalize but without adding the new->old mapping
        -- comment
        do_canonicalize_sprite = opt
    else
        print("Error: unrecognized option "..opt)
        return 1
    end
end

if (fn==nil) then
    print("Usage: map2text [-c/-C] <somefile.map>")
    return 1
end

local function printf(fmt, ...)
    print(string.format(fmt, ...))
end

local map, errmsg = B.loadboard(fn, do_canonicalize_sprite)
if (map == nil) then
    printf("Couldn't load \"%s\": %s", fn, errmsg)
    return 1
end

printf("numsectors = %d", map.numsectors)
printf("numwalls = %d", map.numwalls)
printf("numsprites = %d", map.numsprites)

printf("startpos = { %d, %d, %d }", map.start.x, map.start.y, map.start.z)
printf("startang = %d", map.start.ang)
printf("startsectnum = %d", map.start.sectnum)

local sector_members = {
    "wallptr", "wallnum",
    "ceilingz", "floorz",
    "ceilingstat", "floorstat",
    "ceilingpicnum", "ceilingheinum",
    "ceilingshade",
    "ceilingpal", "ceilingxpanning", "ceilingypanning",
    "floorpicnum", "floorheinum",
    "floorshade",
    "floorpal", "floorxpanning", "floorypanning",
    "visibility", "fogpal",
    "lotag", "hitag", "extra",
}

local wall_members = {
    "x", "y",
    "point2", "nextwall", "nextsector",
    "cstat",
    "picnum", "overpicnum",
    "shade",
    "pal", "xrepeat", "yrepeat", "xpanning", "ypanning",
    "lotag", "hitag", "extra",
}

local sprite_members = {
    "x", "y", "z",
    "cstat", "picnum",
    "shade",
    "pal", "clipdist", "blend",
    "xrepeat", "yrepeat",
    "xoffset", "yoffset",
    "sectnum", "statnum",
    "ang", "owner", "xvel", "yvel", "zvel",
    "lotag", "hitag", "extra",
}

local function print_members(map, struct, members)
    printf("%s = {", struct)
    for i=0,map["num"..struct.."s"]-1 do
        local comment = ""
        if (struct=="sprite" and do_canonicalize_sprite=="-c") then
            comment = "  --"..tostring(map.spriten2o[i])
        end

        printf("[%d]={%s", i, comment)

        for j=1,#members do
            local member = members[j]
            printf("%s = %d", member, map[struct][i][member])
        end

        print("}")
    end

    print("}")
end

print_members(map, "sector", sector_members)
print_members(map, "wall", wall_members)
print_members(map, "sprite", sprite_members)


code:
-- Print out some aggregate statistics for passed BUILD maps,
-- foreachmap module.

local string = require "string"
local math = require "math"

local print = print
local type = type

local stat = require "stat"

module(...)

local function printf(fmt, ...)
    print(string.format(fmt, ...))
end

local sumnumsectors = 0
local sumnumwalls = 0

local s = stat.new()

function success(map, fn)
    local ns = map.numsectors
    local nw = map.numwalls

    s:add(nw/ns)

    sumnumsectors = sumnumsectors+ns
    sumnumwalls = sumnumwalls+nw
end

function finish()
    res = s:getstats()

    printf("%d maps\n", res.n)
    printf("total sectors: %d", sumnumsectors)
    printf("total walls: %d", sumnumwalls)
    printf("total walls / total sectors: %.02f", sumnumwalls/sumnumsectors)
    printf("")
    printf("Walls/sector")
    print(res)
end

repo_name: orochi/nblood

license: mit


code:
#!/bin/sh

DIFF="git diff -U2 --no-index --color-words"
CMD="/usr/bin/env luajit ./map2text.lua"

opt=""

# Name of the 'tempfile' or 'mktemp' command (or full path).
tempfile_cmd=tempfile

tempfile_path=`which "$tempfile_cmd"`
if [ -z "$tempfile_path" ]; then
    echo "Error: tempfile_cmd ($tempfile_cmd) must be the name of existing 'tempfile' or 'mktemp' executable."
    exit 1
fi

if [ "$1" = "-c" -o "$1" = "-C" ]; then
    opt="$1"
    shift
fi

if [ -z "$1" -o -z "$2" ]; then
    echo "Usage: ./mapdiff.sh [-c] <file.map> <file2.map>"
    exit 1
fi

tf1=`"$tempfile_cmd"`
if [ -z "$tf1" ]; then
    echo Failed creating temp file
    exit 2
fi

tf2=`"$tempfile_cmd"`
if [ -z "$tf2" ]; then
    rm "$tf1"
    echo Failed creating temp file
    exit 2
fi

$CMD $opt "$1" > "$tf1"
$CMD $opt "$2" > "$tf2"

$DIFF "$tf1" "$tf2"

rm "$tf1"
rm "$tf2"


code:
-- Print out some statistics for a BUILD map,
-- foreachmap module.

local string = require "string"
local print = print

module(...)

local function printf(fmt, ...)
    print(string.format(fmt, ...))
end

function success(map, fn)
    printf("--- %s:", fn)

    printf("  version: %d", map.version)
    printf("  numsectors: %d\n  numwalls: %d\n  numsprites: %d",
           map.numsectors, map.numwalls, map.numsprites)
    printf("  walls/sector: %.02f\n  sprites/sector: %.02f",
          map.numwalls/map.numsectors, map.numsprites/map.numsectors)
    printf("")
end


code:
-- Display information about problematic wall ypannings,
-- foreachmap module.

local string = require "string"
local table = require "table"
local bit = require "bit"

local print = print
local pairs = pairs
local rawget = rawget
local setmetatable = setmetatable

local B = require "build"

module(...)

local function printf(fmt, ...)
    print(string.format(fmt, ...))
end

local tile

function init(arg)
    local artargend = nil
    for i=2,#arg do
        if (arg[i]=="--") then
            artargend = i
            break
        end
    end

    if (artargend==nil or artargend==0) then
        printf("Usage: luajit ./foreachmap.lua %s <tilesXXX.ART> [, ...] -- <filename1.map> ...\n", arg[1])
        return -1
    end

    local artfns = {}
    for i=2,artargend-1 do
        artfns[#artfns+1] = arg[i]
    end

    local i = 2
    local j = artargend+1

    while (arg[j]) do
        arg[i] = arg[j]
        arg[j] = nil

        i = i+1
        j = j+1
    end

    local tile_, errmsg = B.loadarts(artfns)
    if (tile_ == nil) then
        printf("%s", errmsg)
        return -2
    end

    tile = tile_  -- set 'tile' file-scope var
end

local table_default0_mt = {
    __index = function(tab, idx)
        if (rawget(tab, idx)==nil) then
            return 0
        end
        return rawget(tab, idx)
    end
}

local sector, wall, sprite

function success(map, fn)
    -- set file-scope vars for convenience
    sector = map.sector
    wall = map.wall
    sprite = map.sprite

    -- counts of non-pow2 tiles, per tilenum
    local np2tile = setmetatable({}, table_default0_mt)
    -- walls/overwall indices with non-pow2 tiles, [walidx]=true
    local np2wall = {}
    -- [i]=wallidx
    local np2walls = {}

    local badoverpicnum = false

    for i=0,map.numsectors-1 do
        local startwall = sector[i].wallptr
        local endwall = startwall+sector[i].wallnum-1

        for w=startwall,endwall do
            for n=1,2 do
                local pic, ysiz

                if (wall[w].nextwall < 0) then
                    -- We don't care for white walls
                elseif (n==1) then
                    pic = wall[w].picnum
                    ysiz = tile.sizy[pic]
                else
                    pic = wall[w].overpicnum
                    if (pic < 0 or pic > 30720) then  -- MAXTILES
                        badoverpicnum = true
                    else
                        if (bit.band(wall[w].cstat, 16+32)==0) then
                            -- we don't care about non-masked/1-way walls
                            ysiz = nil
                        else
                            ysiz = tile.sizy[pic]
                        end
                    end
                end

                if (pic == 560) then  -- Don't care for MIRROR
                    ysiz = nil
                end

                if (ysiz~=nil and ysiz > 0 and bit.band(ysiz, bit.bnot(ysiz-1))~=ysiz) then
                    -- non-pow2 ysize

                    np2tile[pic] = np2tile[pic]+1

                    if (not np2wall[w]) then
                        np2wall[w] = true
                        np2walls[#np2walls+1] = w
                    end
                end
            end
        end
    end

    -- report our findings

    if (#np2walls == 0) then
        return
    end

    -- sort in wall index order
    table.sort(np2walls)

    printf("--- %s:", fn)

--[[
    printf(" Walls:")
    for i=1,#np2walls do
        printf("  %d", np2walls[i])
    end
    printf("")
--]]
    printf(" %d red walls with non-pow2 ysize tiles", #np2walls)
    if (badoverpicnum) then
        printf(" (some red walls have out-of-bounds overpicnums)")
    end

    local np2tiles = {}
    for tilenum,_ in pairs(np2tile) do
        np2tiles[#np2tiles+1] = tilenum
    end
    table.sort(np2tiles)

    printf(" Tiles:")
    for i=1,#np2tiles do
        printf("  %d", np2tiles[i])
    end

    printf("")
end


code:
-- Search for maps that have parallaxed ceilings with different tile numbers.
-- foreachmap module.

local bit = require "bit"
local string = require "string"
local table = require "table"
local io = require "io"

local print = print
local tostring = tostring
local type = type

local function printf(fmt, ...)
    print(string.format(fmt, ...))
end

module(...)

local MULTI_PSKY_TILES = { 80, 84, 89 }

local IS_MULTI_PSKY_TILE = {
    [80] = true,  -- MOONSKY1
    [84] = true,  -- BIGORBIT1
    [89] = true,  -- LA
}

local tilecnt

-- foreachmap initialization
local opts = { p=false, s=false }  -- pretty? simple?

function init(args)
    if (#args == 1) then
        local wr = function(s) io.stderr:write(s) end
        wr("Usage: "..args[0].." "..args[1].." [-s] [-p] *.map ...\n")
        wr("  -s: short format\n")
        wr("  -p: pretty-print using ANSI escapes\n\n")
        wr("The long format is as follows:\n")
        wr("  <mapname.map>: <num-distinct-psky-tiles> (<of-those-multi-pskies>): <item1> <item2> ...\n")
        wr("Each <item> reads\n")
        wr("  #<tilenum>:<num-occurrences>(s<sectnum>)\n")
        wr("where <sectnum> is the index of an arbitrary sector containing such a psky ceiling.\n")
        return
    end

    for i=2,#args do
        local arg = args[i]

        if (arg:sub(1,1)=="-") then
            local letter = arg:sub(2,2)

            if (#arg==2 and type(opts[letter])=="boolean") then
                opts[letter] = true
            else
                io.stderr:write("Unrecognized option "..arg.."\n")
            end
        else
            return i
        end
    end
end

-- Sorting function for tiles, true if <tile1> "less-than" <tile2>.
-- "less-than" here means appearing earlier in the resulting table.
local function sortskytiles(tile1, tile2)
    local ismulti1 = IS_MULTI_PSKY_TILE[tile1]
    local ismulti2 = IS_MULTI_PSKY_TILE[tile2]

    -- First, check if <tile1> is a multi-psky tile and <tile2> not.
    if (ismulti1 and not ismulti2) then
        return true
    end

    -- Next, check if <tile1> appears more often in the map.
    if (tilecnt[tile1] > tilecnt[tile2]) then
        return true
    end

    -- Now, (tilecnt[tile1] >= tilecnt[tile2]) and (not ismulti1 or ismulti2).
    return false
end

local function bold(s)
    return opts.p and ("\x1b[1m"..s.."\x1b[0m") or tostring(s)
end

function success(map, fn)
    -- [<tilenum>] = <count of psky ceilings with that tile number>
    tilecnt = {}

    -- [<idx>] = <tilenum>
    local tiles = {}
    -- [<tilenum>] = <arbitrary sectnum with that psky ceiling tile>
    local tilesect = {}

    for i=0,map.numsectors-1 do
        local sec = map.sector[i]

        if (bit.band(sec.ceilingstat, 1) ~= 0) then
            local tile = sec.ceilingpicnum

            if (tilecnt[tile] == nil) then
                tiles[#tiles+1] = tile
            end

            tilecnt[tile] = 1 + (tilecnt[tile] or 0)
            tilesect[tile] = i
        end
    end

    if (#tiles > 0) then
        -- Do an in-place sort.
        table.sort(tiles, sortskytiles)

        local strbuf = {}

        for i=1,#tiles do
            local tile = tiles[i]
            local ismulti = IS_MULTI_PSKY_TILE[tile]

            if (i == 4) then
                strbuf[#strbuf+1] = "..."
                break
            end

            strbuf[#strbuf+1] = (ismulti and "*" or "#")..tile..
                ":"..tilecnt[tile]..
                (opts.s and "" or ("(s"..tilesect[tile]..")"))
        end

        local nummultiskies = 0
        for i=1,#MULTI_PSKY_TILES do
            if (tilecnt[MULTI_PSKY_TILES[i]]) then
                nummultiskies = nummultiskies+1
            end
        end

        -- DCPT: distinct ceiling-psky tiles
        printf("%s: %s%sDCPT: %s", fn, bold(#tiles),
               opts.s and " " or bold(" ("..nummultiskies..") "),
               table.concat(strbuf," "))
    end
end


code:
----------------------------------------------------------------------------
-- Verbose mode of the LuaJIT compiler.
--
-- Copyright (C) 2005-2015 Mike Pall. All rights reserved.
-- Released under the MIT license. See Copyright Notice in luajit.h
----------------------------------------------------------------------------
--
-- This module shows verbose information about the progress of the
-- JIT compiler. It prints one line for each generated trace. This module
-- is useful to see which code has been compiled or where the compiler
-- punts and falls back to the interpreter.
--
-- Example usage:
--
--   luajit -jv -e "for i=1,1000 do for j=1,1000 do end end"
--   luajit -jv=myapp.out myapp.lua
--
-- Default output is to stderr. To redirect the output to a file, pass a
-- filename as an argument (use '-' for stdout) or set the environment
-- variable LUAJIT_VERBOSEFILE. The file is overwritten every time the
-- module is started.
--
-- The output from the first example should look like this:
--
-- [TRACE   1 (command line):1 loop]
-- [TRACE   2 (1/3) (command line):1 -> 1]
--
-- The first number in each line is the internal trace number. Next are
-- the file name ('(command line)') and the line number (':1') where the
-- trace has started. Side traces also show the parent trace number and
-- the exit number where they are attached to in parentheses ('(1/3)').
-- An arrow at the end shows where the trace links to ('-> 1'), unless
-- it loops to itself.
--
-- In this case the inner loop gets hot and is traced first, generating
-- a root trace. Then the last exit from the 1st trace gets hot, too,
-- and triggers generation of the 2nd trace. The side trace follows the
-- path along the outer loop and *around* the inner loop, back to its
-- start, and then links to the 1st trace. Yes, this may seem unusual,
-- if you know how traditional compilers work. Trace compilers are full
-- of surprises like this -- have fun! :-)
--
-- Aborted traces are shown like this:
--
-- [TRACE --- foo.lua:44 -- leaving loop in root trace at foo:lua:50]
--
-- Don't worry -- trace aborts are quite common, even in programs which
-- can be fully compiled. The compiler may retry several times until it
-- finds a suitable trace.
--
-- Of course this doesn't work with features that are not-yet-implemented
-- (NYI error messages). The VM simply falls back to the interpreter. This
-- may not matter at all if the particular trace is not very high up in
-- the CPU usage profile. Oh, and the interpreter is quite fast, too.
--
-- Also check out the -jdump module, which prints all the gory details.
--
------------------------------------------------------------------------------

-- Cache some library functions and objects.
local jit = require("jit")
assert(jit.version_num == 20003, "LuaJIT core/library version mismatch")
local jutil = require("jit.util")
local vmdef = require("jit.vmdef")
local funcinfo, traceinfo = jutil.funcinfo, jutil.traceinfo
local type, format = type, string.format
local stdout, stderr = io.stdout, io.stderr

-- Active flag and output file handle.
local active, out

------------------------------------------------------------------------------

local startloc, startex

local function fmtfunc(func, pc)
  local fi = funcinfo(func, pc)
  if fi.loc then
    return fi.loc
  elseif fi.ffid then
    return vmdef.ffnames[fi.ffid]
  elseif fi.addr then
    return format("C:%x", fi.addr)
  else
    return "(?)"
  end
end

-- Format trace error message.
local function fmterr(err, info)
  if type(err) == "number" then
    if type(info) == "function" then info = fmtfunc(info) end
    err = format(vmdef.traceerr[err], info)
  end
  return err
end

-- Dump trace states.
local function dump_trace(what, tr, func, pc, otr, oex)
  if what == "start" then
    startloc = fmtfunc(func, pc)
    startex = otr and "("..otr.."/"..oex..") " or ""
  else
    if what == "abort" then
      local loc = fmtfunc(func, pc)
      if loc ~= startloc then
	out:write(format("[TRACE --- %s%s -- %s at %s]\n",
	  startex, startloc, fmterr(otr, oex), loc))
      else
	out:write(format("[TRACE --- %s%s -- %s]\n",
	  startex, startloc, fmterr(otr, oex)))
      end
    elseif what == "stop" then
      local info = traceinfo(tr)
      local link, ltype = info.link, info.linktype
      if ltype == "interpreter" then
	out:write(format("[TRACE %3s %s%s -- fallback to interpreter]\n",
	  tr, startex, startloc))
      elseif link == tr or link == 0 then
	out:write(format("[TRACE %3s %s%s %s]\n",
	  tr, startex, startloc, ltype))
      elseif ltype == "root" then
	out:write(format("[TRACE %3s %s%s -> %d]\n",
	  tr, startex, startloc, link))
      else
	out:write(format("[TRACE %3s %s%s -> %d %s]\n",
	  tr, startex, startloc, link, ltype))
      end
    else
      out:write(format("[TRACE %s]\n", what))
    end
    out:flush()
  end
end

------------------------------------------------------------------------------

-- Detach dump handlers.
local function dumpoff()
  if active then
    active = false
    jit.attach(dump_trace)
    if out and out ~= stdout and out ~= stderr then out:close() end
    out = nil
  end
end

-- Open the output file and attach dump handlers.
local function dumpon(outfile)
  if active then dumpoff() end
  if not outfile then outfile = os.getenv("LUAJIT_VERBOSEFILE") end
  if outfile then
    out = outfile == "-" and stdout or assert(io.open(outfile, "w"))
  else
    out = stderr
  end
  jit.attach(dump_trace, "trace")
  active = true
end

-- Public module functions.
module(...)

on = dumpon
off = dumpoff
start = dumpon -- For -j command line option.


code:
-- "Extended" math module for Lunatic.

local ffi = require("ffi")

local bit = require("bit")
local math = require("math")

local arshift = bit.arshift
local abs, sqrt = math.abs, math.sqrt

local assert = assert
local error = error
local type = type

local OUR_REQUIRE_STRING = [[
 local _xm=require'xmath'
 local _v,_iv=_xm.vec3,_xm.ivec3
]]
local function our_get_require()
    return OUR_REQUIRE_STRING
end

module(...)

---=== TRIGONOMETRY ===---

local BANG2RAD = math.pi/1024
local isintab = ffi.new("int16_t [?]", 2048)
local dsintab = ffi.new("double [?]", 2048)

for a=0,511 do
    local s = math.sin(a*BANG2RAD)
    isintab[a] = 16384*s
    dsintab[a] = s
end

isintab[512] = 16384
dsintab[512] = 1

for i=513,1023 do
    isintab[i] = isintab[1024-i];
    dsintab[i] = dsintab[1024-i];
end

for i=1024,2047 do
    isintab[i] = -isintab[i-1024];
    dsintab[i] = -dsintab[i-1024];
end

local band = bit.band

local function ksc_common(ang)
    ang = band(ang, 2047)
    assert(ang >= 0 and ang < 2048)  -- might have been passed NaN
    return ang
end

-- k{sin,cos}: 16384-scaled output, 2048-based angle input
function ksin(ang)
    return isintab[ksc_common(ang)]
end

function kcos(ang)
    return isintab[ksc_common(ang+512)]
end

local sin, cos = math.sin, math.cos

-- {sin,cos}b: [-1..1] output, 2048-based angle input
function sinb(ang)
    return dsintab[ksc_common(ang)]
end

function cosb(ang)
    return dsintab[ksc_common(ang+512)]
end

local cosb, sinb = cosb, sinb

---=== Approximations to 2D and 3D Euclidean distances ===---
-- (also see common.c)

local function dist_common(pos1, pos2)
    local x = abs(pos1.x - pos2.x)
    local y = abs(pos1.y - pos2.y)
    if (x < y) then
        x, y = y, x
    end
    return x, y
end

function ldist(pos1, pos2)
    local x, y = dist_common(pos1, pos2)

    local t = y + arshift(y,1)
    return x - arshift(x,5) - arshift(x,7) + arshift(t,2) + arshift(t,6)
end

function dist(pos1, pos2)
    local x, y = dist_common(pos1, pos2)
    local z = abs(arshift(pos1.z - pos2.z, 4))

    if (x < z) then
        x, z = z, x
    end

    local t = y + z
    return x - arshift(x,4) + arshift(t,2) + arshift(t,3)
end

---=== VECTOR TYPES ===---

-- The integer 3-vector can be useful for calculations expecting integer
-- values, e.g. ivec3(x, y, z) is a reasonable way to round a vec3. It can also
-- be used as the RHS to the vec2/vec3 arithmetic methods.
-- NOTE: We must have a typedef with that exact name, because for Lunatic
-- (i.e. not stand-alone), the type was already declared in defs_common.lua.
ffi.cdef "typedef struct { int32_t x, y, z; } vec3_t;"
local ivec3_t = ffi.typeof("vec3_t")

local dvec2_t = ffi.typeof("struct { double x, y; }")
local dvec3_t = ffi.typeof("struct { double x, y, z; }")

local vec2_mt = {
    __add = function(a, b) return dvec2_t(a.x+b.x, a.y+b.y) end,
    __sub = function(a, b) return dvec2_t(a.x-b.x, a.y-b.y) end,
    __unm = function(a) return dvec2_t(-a.x, -a.y) end,

    __mul = function(a,b)
        if (type(a)=="number") then
            return dvec2_t(a*b.x, a*b.y)
        end

        if (type(b)~="number") then
            error("number expected in vec2 multiplication", 2)
        end
        return dvec2_t(a.x*b, a.y*b)
    end,

    __div = function(a,b)
        if (type(b)~="number") then
            error("number expected in vec2 division", 2)
        end
        return dvec2_t(a.x/b, a.y/b)
    end,

    __tostring = function(a) return "vec2("..a.x..", "..a.y..")" end,

    __index = {
        lensq = function(a) return a.x*a.x + a.y*a.y end,

        mhlen = function(a) return abs(a.x)+abs(a.y) end,
    },
}

local l_rotate  -- fwd-decl (XXX: could be the other way around)

-- The vec3 metatable is shared between the integer- and double-based 3-vector
-- types. However, some operations are slightly different.
local vec3_mt = {
    -- Arithmetic operations. Note that they always return a dvec3.
    __add = function(a, b) return dvec3_t(a.x+b.x, a.y+b.y, a.z+b.z) end,
    __sub = function(a, b) return dvec3_t(a.x-b.x, a.y-b.y, a.z-b.z) end,
    __unm = function(a) return dvec3_t(-a.x, -a.y, -a.z) end,

    __mul = function(a,b)
        if (type(a)=="number") then
            return dvec3_t(a*b.x, a*b.y, a*b.z)
        end

        if (type(b)~="number") then
            error("number expected in vec3 multiplication", 2)
        end
        return dvec3_t(a.x*b, a.y*b, a.z*b)
    end,

    __div = function(a,b)
        if (type(b)~="number") then
            error("number expected in vec3 division", 2)
        end
        return dvec3_t(a.x/b, a.y/b, a.z/b)
    end,

    -- '^' is the "translate upwards" operator, returns same-typed vector.
    __pow = function(v, zofs)
        return v:_ctor(v.x, v.y, v.z-zofs)
    end,

    -- Convenience for human-readable display.
    __tostring = function(a)
        return (a:_isi() and "i" or "").."vec3("..a.x..", "..a.y..", "..a.z..")"
    end,

    __index = {
        -- Euclidean 3D length.
        len = function(a) return sqrt(a.x*a.x + a.y*a.y + a.z*a.z) end,
        -- Euclidean 3D squared length.
        lensq = function(a) return a.x*a.x + a.y*a.y + a.z*a.z end,

        -- Euclidean 2D length.
        len2 = function(a) return sqrt(a.x*a.x + a.y*a.y) end,
        -- Euclidean 2D squared length.
        len2sq = function(a) return a.x*a.x + a.y*a.y end,

        -- Manhattan-distance 3D length:
        mhlen = function(a) return abs(a.x)+abs(a.y)+abs(a.z) end,

        toivec3 = function(v) return ivec3_t(v.x, v.y, v.z) end,

        -- BUILD-coordinate (z scaled by 16) <-> uniform conversions.
        touniform = function(v)
            return v:_isi()
                and v:_ctor(v.x, v.y, arshift(v.z, 4))
                or v:_ctor(v.x, v.y, v.z/16)
        end,

        tobuild = function(v) return v:_ctor(v.x, v.y, 16*v.z) end,

        rotate = function(v, ang, pivot) return l_rotate(v, ang, pivot) end,

        -- PRIVATE methods --

        -- Get the type constructor for this vector.
        _ctor = function(v, ...)
            return v:_isi() and ivec3_t(...) or dvec3_t(...)
        end,
        -- Is <v> integer vec3? INTERNAL.
        _isi = function(v)
            return ffi.istype(ivec3_t, v)
        end,

        --- Serialization ---
        _get_require = our_get_require,

        _serialize = function(v)
            return (v:_isi() and "_iv" or "_v").."("..v.x..","..v.y..","..v.z..")"
        end,
    },
}

ffi.metatype(dvec2_t, vec2_mt)
ffi.metatype(dvec3_t, vec3_mt)
ffi.metatype(ivec3_t, vec3_mt)

-- VEC2 user data constructor.
--  * vec2([x [, y]]), assuming that x and y are numbers. Vacant positions are
--    assumed to be 0.
--  * vec2(<compound>), <compound> can be anything indexable with "x" and "y"
function vec2(...)
    local x, y = ...
    if (type(x)=="number" or x==nil) then
        return dvec2_t(...)
    else
        return dvec2_t(x.x, x.y)
    end
end

-- VEC3 user data constructor.
--  Analogous to VEC2.
function vec3(...)
    local x, y, z = ...
    if (type(x)=="number" or x==nil) then
        return dvec3_t(...)
    else
        return dvec3_t(x.x, x.y, x.z)
    end
end

-- IVEC3 user data constructor.
function ivec3(...)
    local x, y, z = ...
    if (type(x)=="number" or x==nil) then
        return ivec3_t(...)
    else
        return ivec3_t(x.x, x.y, x.z)
    end
end

local vec2, vec3 = vec2, vec3

---=== MISCELLANEOUS MATH ===---

local intarg = ffi.new("int32_t [1]")
function bangvec(bang)
    intarg[0] = bang  -- round towards zero
    return dvec3_t(cosb(intarg[0]), sinb(intarg[0]))
end

function kangvec(bang, z)
    intarg[0] = bang  -- round towards zero
    return ivec3_t(kcos(intarg[0]), ksin(intarg[0]), z or 0)
end

function angvec(ang)
    return dvec3_t(cos(ang), sin(ang))
end

local zerovec = vec3()
-- Point rotation. Note the different order of arguments from engine function.
-- XXX: passing mixed vec2/vec3 is problematic. Get rid of vec2?
-- <ang>: BUILD angle (0-2047 based)
function rotate(pos, ang, pivot)
    pivot = pivot or zerovec
    local p = vec3(pos)-pivot
    local c, s = cosb(ang), sinb(ang)
    local x, y = p.x, p.y
    p.x = pivot.x + (c*x - s*y)
    p.y = pivot.y + (c*y + s*x)
    return p
end

l_rotate = rotate

-- Two-element vector cross product.
-- Anti-commutative, distributive.
local function cross2(v, w)
    return v.y*w.x - v.x*w.y
end

-- Finds the intersection point of two lines given by
-- point a and vector v
--  and
-- point b and vector w
--
-- Returns:
--  if <TODO>, nil
--  if retpoint_p evaluates to a non-true value, coefficients cv and cw such that <TODO>
--  else, the intersection point
function intersect(a,v, b,w, retpoint_p)
    local vxw = cross2(v,w)

    if (vxw ~= 0) then
        local btoa = vec2(a) - vec2(b)
        local cv, cw = cross2(w, btoa)/vxw, cross2(v, btoa)/vxw

        if (retpoint_p) then
            return vec2(a) + cv*vec2(v)
        else
            return cv, cw
        end
    end

    -- return nil if v and w parallel (or either of them is a point), or if
    -- they contain NaNs
end


code:
// This object is shared by the editors of *all* Build games!

#include "compat.h"
#include "keys.h"
#include "build.h"
#include "cache1d.h"
#ifdef POLYMER
# include "polymer.h"
#endif
#include "editor.h"
#include "renderlayer.h"

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

#include "lz4.h"
#include "xxhash.h"

// XXX: This breaks editors for games other than Duke. The OSD needs a way to specify colors in abstract instead of concatenating palswap escape sequences.
#include "common_game.h"

//////////////////// 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))

////

// All these variables need verification that all relevant editor stubs are actually implementing them correctly.

char getmessage[162], getmessageleng;
int32_t getmessagetimeoff; //, charsperline;

int32_t mousxplc, mousyplc;
int32_t mouseaction;

char *scripthist[SCRIPTHISTSIZ];
int32_t scripthistend;

int32_t g_lazy_tileselector;
int32_t fixmaponsave_sprites = 1;

int32_t showambiencesounds=2;

int32_t autosave=180;

int32_t autocorruptcheck;
int32_t corruptcheck_noalreadyrefd, corruptcheck_heinum=1;
int32_t corruptcheck_game_duke3d=1;  // TODO: at startup, make conditional on which game we are editing for?
int32_t corrupt_tryfix_alt;
int32_t corruptlevel, numcorruptthings, corruptthings[MAXCORRUPTTHINGS];

////

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

////

void drawgradient(void)
{
    int32_t i, col = editorcolors[25];
    videoBeginDrawing();
    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);
    videoEndDrawing();
}

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);
}

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);
}

////////// 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++)
        DO_FREE_AND_NULL(g_taglab.label[i]);
}

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 *)Xmalloc(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
        DO_FREE_AND_NULL(g_taglab.label[tag]);
        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')
            DO_FREE_AND_NULL(g_taglab.label[olabeltag]);
            diddel |= 2;
        }
    }

    if (!diddel)
        g_taglab.numlabels++;
    g_taglab.label[tag] = Xstrdup(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);
}
////////// end tag labeling system //////////

////////// UNDO/REDO SYSTEM //////////
#if M32_UNDO
mapundo_t *mapstate = NULL;

int32_t map_revision = 1;

static int32_t try_match_with_prev(int32_t idx, int32_t numsthgs, uintptr_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, uintptr_t crc)
{
    uint32_t j;

    // allocate
    int const compressed_size = LZ4_compressBound(size);
    mapstate->sws[idx] = (char *)Xmalloc(4 + compressed_size);

    // compress & realloc
    j = LZ4_compress_default((const char*)srcdata, mapstate->sws[idx]+4, size, compressed_size);
    mapstate->sws[idx] = (char *)Xrealloc(mapstate->sws[idx], 4 + j);

    // 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 *)Xcalloc(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 *)Xcalloc(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)
    {
#if !defined UINTPTR_MAX
# error Need UINTPTR_MAX define to select between 32- and 64-bit functions
#endif
#if UINTPTR_MAX == 0xffffffff
        /* 32-bit */
#define XXH__ XXH32
#else
        /* 64-bit */
#define XXH__ XXH64
#endif
        uintptr_t temphash = XXH__((uint8_t *)sector, numsectors*sizeof(sectortype), numsectors*sizeof(sectortype));

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

        if (numwalls)
        {
            temphash = XXH__((uint8_t *)wall, numwalls*sizeof(walltype), numwalls*sizeof(walltype));

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

        if (Numsprites)
        {
            temphash = XXH__((uint8_t *)sprite, MAXSPRITES*sizeof(spritetype), MAXSPRITES*sizeof(spritetype));

            if (!try_match_with_prev(2, Numsprites, temphash))
            {
                int32_t i = 0;
                auto const uspri = (uspritetype *)Xmalloc(Numsprites*sizeof(spritetype) + 4);
                auto spri = uspri;

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

                create_compressed_block(2, uspri, Numsprites*sizeof(spritetype), temphash);
                Xfree(uspri);
            }
        }
#undef XXH__
    }

    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() && videoGetRenderMode() == REND_POLYMER)
        polymer_loadboard();
#endif
#ifdef YAX_ENABLE
    yax_update(0);
    yax_updategrays(pos.z);
#endif
    CheckMapCorruption(4, 0);

    return 0;
}
#endif

////

//// port of a.m32's corruptchk ////
// Compile wall loop checks? 0: no, 1: partial, 2: full.
#define CCHK_LOOP_CHECKS 0
// returns value from 0 (all OK) to 5 (panic!)
#define CCHK_PANIC OSDTEXT_DARKRED "PANIC!!!^O "
//#define CCHKPREF OSDTEXT_RED "^O"
#define CCHK_CORRECTED OSDTEXT_GREEN " -> "

#define CORRUPTCHK_PRINT(errlev, what, fmt, ...) do  \
{ \
    bad = max(bad, errlev); \
    if (numcorruptthings>=MAXCORRUPTTHINGS) \
        goto too_many_errors; \
    corruptthings[numcorruptthings++] = (what); \
    if (errlev >= printfromlev) \
        OSD_Printf("#%d: " fmt "\n", numcorruptthings, ## __VA_ARGS__); \
} while (0)

#ifdef YAX_ENABLE
static int32_t walls_have_equal_endpoints(int32_t w1, int32_t w2)
{
    int32_t n1 = wall[w1].point2, n2 = wall[w2].point2;

    return (wall[w1].x==wall[w2].x && wall[w1].y==wall[w2].y &&
            wall[n1].x==wall[n2].x && wall[n1].y==wall[n2].y);
}

static void correct_yax_nextwall(int32_t wallnum, int32_t bunchnum, int32_t cf, int32_t tryfixingp)
{
    int32_t i, j, startwall, endwall;
    int32_t nummatching=0, lastwall[2] = { -1, -1 };

    for (SECTORS_OF_BUNCH(bunchnum, !cf, i))
        for (WALLS_OF_SECTOR(i, j))
        {
            //  v v v shouldn't happen, 'stupidity safety'
            if (j!=wallnum && walls_have_equal_endpoints(wallnum, j))
            {
                lastwall[nummatching++] = j;
                if (nummatching==2)
                    goto outofloop;
            }
        }
outofloop:
    if (nummatching==1)
    {
        if (!tryfixingp)
        {
            OSD_Printf("    will set wall %d's %s to %d on tryfix\n",
                       wallnum, yupdownwall[cf], lastwall[0]);
        }
        else
        {
            int32_t setreverse = 0;
            yax_setnextwall(wallnum, cf, lastwall[0]);
            if (yax_getnextwall(lastwall[0], !cf) < 0)
            {
                setreverse = 1;
                yax_setnextwall(lastwall[0], !cf, wallnum);
            }

            OSD_Printf("auto-correction: set wall %d's %s to %d%s\n",
                       wallnum, yupdownwall[cf], lastwall[0], setreverse?" and its reverse link":"");
        }
    }
    else if (!tryfixingp)
    {
        if (nummatching > 1)
        {
            OSD_Printf("    found more than one matching wall: at least %d and %d\n",
                       lastwall[0], lastwall[1]);
        }
        else if (nummatching == 0)
        {
            OSD_Printf("    found no matching walls!\n");
        }
    }
}
#endif

// in reverse orientation
static int32_t walls_are_consistent(int32_t w1, int32_t w2)
{
    return (wall[w2].x==POINT2(w1).x && wall[w2].y==POINT2(w1).y &&
            wall[w1].x==POINT2(w2).x && wall[w1].y==POINT2(w2).y);
}

static void suggest_nextsector_correction(int32_t nw, int32_t j)
{
    // wall j's nextsector is inconsistent with its nextwall... what shall we do?

    if (nw>=0 && nw<numwalls)
    {
        // maybe wall[j].nextwall's nextwall is right?
        if (wall[nw].nextwall==j && walls_are_consistent(nw, j))
            OSD_Printf("   suggest setting wall[%d].nextsector to %d\n",
                       j, sectorofwall_noquick(nw));
    }

    // alternative
    if (wall[j].nextsector>=0 && wall[j].nextsector<numsectors)
    {
        int32_t w, startwall, endwall;
        for (WALLS_OF_SECTOR(wall[j].nextsector, w))
        {
            // XXX: need clearing some others?
            if (walls_are_consistent(w, j))
            {
                OSD_Printf(" ? suggest setting wall[%d].nextwall to %d\n",
                           j, w);
                break;
            }
        }
    }

    OSD_Printf(" ?? suggest making wall %d white\n", j);
}

static void do_nextsector_correction(int32_t nw, int32_t j)
{
    if (corrupt_tryfix_alt==0)
    {
        if (nw>=0 && nw<numwalls)
            if (wall[nw].nextwall==j && walls_are_consistent(nw, j))
            {
                int32_t newns = sectorofwall_noquick(nw);
                wall[j].nextsector = newns;
                OSD_Printf(CCHK_CORRECTED "auto-correction: set wall[%d].nextsector=%d\n",
                           j, newns);
            }
    }
    else if (corrupt_tryfix_alt==1)
    {
        if (wall[j].nextsector>=0 && wall[j].nextsector<numsectors)
        {
            int32_t w, startwall, endwall;
            for (WALLS_OF_SECTOR(wall[j].nextsector, w))
                if (walls_are_consistent(w, j))
                {
                    wall[j].nextwall = w;
                    OSD_Printf(CCHK_CORRECTED "auto-correction: set wall[%d].nextwall=%d\n",
                               j, w);
                    break;
                }
        }
    }
    else if (corrupt_tryfix_alt==2)
    {
        wall[j].nextwall = wall[j].nextsector = -1;
        OSD_Printf(CCHK_CORRECTED "auto-correction: made wall %d white\n", j);
    }
}

static int32_t csc_s, csc_i;
// 1: corrupt, 0: OK
static int32_t check_spritelist_consistency()
{
    int32_t ournumsprites=0;
    static uint8_t havesprite[MAXSPRITES>>3];

    csc_s = csc_i = -1;

    if (Numsprites < 0 || Numsprites > MAXSPRITES)
        return 1;

    for (bssize_t i=0; i<MAXSPRITES; i++)
    {
        const int32_t sectnum=sprite[i].sectnum, statnum=sprite[i].statnum;

        csc_i = i;

        if ((statnum==MAXSTATUS) != (sectnum==MAXSECTORS))
            return 2;  // violation of .statnum==MAXSTATUS iff .sectnum==MAXSECTORS

        if ((unsigned)statnum > MAXSTATUS || (sectnum!=MAXSECTORS && (unsigned)sectnum > (unsigned)numsectors))
            return 3;  // oob sectnum or statnum

        if (statnum != MAXSTATUS)
            ournumsprites++;
    }

    if (ournumsprites != Numsprites)
    {
        initprintf("ournumsprites=%d, Numsprites=%d\n", ournumsprites, Numsprites);
        return 4;  // counting sprites by statnum!=MAXSTATUS inconsistent with Numsprites
    }

    // SECTOR LIST

    Bmemset(havesprite, 0, (Numsprites+7)>>3);

    for (bssize_t s=0; s<numsectors; s++)
    {
        int i;
        csc_s = s;

        for (i=headspritesect[s]; i>=0; i=nextspritesect[i])
        {
            csc_i = i;

            if (i >= MAXSPRITES)
                return 5;  // oob sprite index in list, or Numsprites inconsistent

            if (havesprite[i>>3]&(1<<(i&7)))
                return 6;  // have a cycle in the list

            havesprite[i>>3] |= (1<<(i&7));

            if (sprite[i].sectnum != s)
                return 7;  // .sectnum inconsistent with list
        }

        if (i!=-1)
            return 8;  // various code checks for -1 to break loop
    }

    csc_s = -1;
    for (bssize_t i=0; i<MAXSPRITES; i++)
    {
        csc_i = i;

        if (sprite[i].statnum!=MAXSTATUS && !(havesprite[i>>3]&(1<<(i&7))))
            return 9;  // have a sprite in the world not in sector list
    }

    // STATUS LIST -- we now clear havesprite[] bits

    for (bssize_t s=0; s<MAXSTATUS; s++)
    {
        int i;
        csc_s = s;

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

            if (i >= MAXSPRITES)
                return 10;  // oob sprite index in list, or Numsprites inconsistent

            // have a cycle in the list, or status list inconsistent with
            // sector list (*)
            if (!(havesprite[i>>3]&(1<<(i&7))))
                return 11;

            havesprite[i>>3] &= ~(1<<(i&7));

            if (sprite[i].statnum != s)
                return 12;  // .statnum inconsistent with list
        }

        if (i!=-1)
            return 13;  // various code checks for -1 to break loop
    }

    csc_s = -1;
    for (bssize_t i=0; i<Numsprites; i++)
    {
        csc_i = i;

        // Status list contains only a proper subset of the sprites in the
        // sector list.  Reverse case is handled by (*)
        if (havesprite[i>>3]&(1<<(i&7)))
            return 14;
    }

    return 0;
}

#if CCHK_LOOP_CHECKS
// Return the least wall index of the outer loop of sector <sectnum>, or
//  -1 if there is none,
//  -2 if there is more than one.
static int32_t determine_outer_loop(int32_t sectnum)
{
    int32_t j, outerloopstart = -1;

    const int32_t startwall = sector[sectnum].wallptr;
    const int32_t endwall = startwall + sector[sectnum].wallnum - 1;

    for (j=startwall; j<=endwall; j=get_nextloopstart(j))
    {
        if (clockdir(j) == CLOCKDIR_CW)
        {
            if (outerloopstart == -1)
                outerloopstart = j;
            else if (outerloopstart >= 0)
                return -2;
        }
    }

    return outerloopstart;
}
#endif

#define TRYFIX_NONE() (tryfixing == 0ull)
#define TRYFIX_CNUM(onumct) (onumct < MAXCORRUPTTHINGS && (tryfixing & (1ull<<onumct)))

int32_t CheckMapCorruption(int32_t printfromlev, uint64_t tryfixing)
{
    int32_t i, j;
    int32_t ewall=0;  // expected wall index

    int32_t errlevel=0, bad=0;
    int32_t heinumcheckstat = 0;  // 1, 2

    uint8_t *seen_nextwalls = NULL;
    int16_t *lastnextwallsource = NULL;

    numcorruptthings = 0;

    if (numsectors>MAXSECTORS)
        CORRUPTCHK_PRINT(5, 0, CCHK_PANIC "SECTOR LIMIT EXCEEDED (MAXSECTORS=%d)!!!", MAXSECTORS);

    if (numwalls>MAXWALLS)
        CORRUPTCHK_PRINT(5, 0, CCHK_PANIC "WALL LIMIT EXCEEDED (MAXWALLS=%d)!!!", MAXWALLS);

    if (numsectors>MAXSECTORS || numwalls>MAXWALLS)
    {
        corruptlevel = bad;
        return bad;
    }

    if (numsectors==0 || numwalls==0)
    {
        if (numsectors>0)
            CORRUPTCHK_PRINT(5, 0, CCHK_PANIC " Have sectors but no walls!");
        if (numwalls>0)
            CORRUPTCHK_PRINT(5, 0, CCHK_PANIC " Have walls but no sectors!");
        return bad;
    }

    if (!corruptcheck_noalreadyrefd)
    {
        seen_nextwalls = (uint8_t *)Xcalloc((numwalls+7)>>3,1);
        lastnextwallsource = (int16_t *)Xmalloc(numwalls*sizeof(lastnextwallsource[0]));
    }

    for (i=0; i<numsectors; i++)
    {
        const int32_t w0 = sector[i].wallptr;
        const int32_t numw = sector[i].wallnum;
        const int32_t endwall = w0 + numw - 1;  // inclusive

        bad = 0;

        if (w0 < 0 || w0 > numwalls)
        {
            if (w0 < 0 || w0 >= MAXWALLS)
                CORRUPTCHK_PRINT(5, CORRUPT_SECTOR|i, "SECTOR[%d].WALLPTR=%d INVALID!!!", i, w0);
            else
                CORRUPTCHK_PRINT(5, CORRUPT_SECTOR|i, "SECTOR[%d].WALLPTR=%d out of range (numwalls=%d)", i, w0, numw);
        }

        if (w0 != ewall)
            CORRUPTCHK_PRINT(4, CORRUPT_SECTOR|i, "SECTOR[%d].WALLPTR=%d inconsistent, expected %d", i, w0, ewall);

        if (numw <= 1)
            CORRUPTCHK_PRINT(5, CORRUPT_SECTOR|i, CCHK_PANIC "SECTOR[%d].WALLNUM=%d INVALID!!!", i, numw);
        else if (numw==2)
            CORRUPTCHK_PRINT(3, CORRUPT_SECTOR|i, "SECTOR[%d].WALLNUM=2, expected at least 3", i);

        ewall += numw;

        if (endwall >= numwalls)
            CORRUPTCHK_PRINT(5, CORRUPT_SECTOR|i, "SECTOR[%d]: wallptr+wallnum=%d out of range: numwalls=%d", i, endwall, numwalls);

        // inconsistent cstat&2 and heinum checker
        if (corruptcheck_heinum)
        {
            const char *cflabel[2] = {"ceiling", "floor"};

            for (j=0; j<2; j++)
            {
                const int32_t cs = !!(SECTORFLD(i,stat, j)&2);
                const int32_t hn = !!SECTORFLD(i,heinum, j);

                if (cs != hn && heinumcheckstat <= 1)
                {
                    if (numcorruptthings < MAXCORRUPTTHINGS &&
                        (heinumcheckstat==1 || (heinumcheckstat==0 && (tryfixing & (1ull<<numcorruptthings)))))
                    {
                        setslope(i, j, 0);
                        OSD_Printf(CCHK_CORRECTED "auto-correction: reset sector %d's %s slope\n",
                                   i, cflabel[j]);
                        heinumcheckstat = 1;
                    }
                    else if (corruptcheck_heinum==2 && heinumcheckstat==0)
                    {
                        CORRUPTCHK_PRINT(1, CORRUPT_SECTOR|i,
                                         "SECTOR[%d]: inconsistent %sstat&2 and heinum", i, cflabel[j]);
                    }

                    if (heinumcheckstat != 1)
                        heinumcheckstat = 2;
                }
            }
        }

        errlevel = max(errlevel, bad);

        if (bad < 4)
        {
            for (j=w0; j<=endwall; j++)
            {
                const int32_t nw = wall[j].nextwall;
                const int32_t ns = wall[j].nextsector;

                bad = 0;

                // First, some basic wall sanity checks.

                if (wall[j].point2 < w0 || wall[j].point2 > endwall)
                {
                    if (wall[j].point2 < 0 || wall[j].point2 >= MAXWALLS)
                        CORRUPTCHK_PRINT(5, CORRUPT_WALL|j, CCHK_PANIC "WALL[%d].POINT2=%d INVALID!!!",
                                         j, TrackerCast(wall[j].point2));
                    else
                        CORRUPTCHK_PRINT(4, CORRUPT_WALL|j, "WALL[%d].POINT2=%d out of range [%d, %d]",
                                         j, TrackerCast(wall[j].point2), w0, endwall);
                }

                if (nw >= numwalls)
                {
                    const int32_t onumct = numcorruptthings;

                    if (TRYFIX_NONE())
                    {
                        if (nw >= MAXWALLS)
                            CORRUPTCHK_PRINT(5, CORRUPT_WALL|j, "WALL[%d].NEXTWALL=%d INVALID!!!",
                                             j, nw);
                        else
                            CORRUPTCHK_PRINT(4, CORRUPT_WALL|j, "WALL[%d].NEXTWALL=%d out of range: numwalls=%d",
                                             j, nw, numwalls);
                        OSD_Printf("    will make wall %d white on tryfix\n", j);
                    }
                    else if (TRYFIX_CNUM(onumct))  // CODEDUP MAKE_WALL_WHITE
                    {
                        wall[j].nextwall = wall[j].nextsector = -1;
                        OSD_Printf(CCHK_CORRECTED "auto-correction: made wall %d white\n", j);
                    }
                }

                if (ns >= numsectors)
                {
                    const int32_t onumct = numcorruptthings;

                    if (TRYFIX_NONE())
                    {
                        if (ns >= MAXSECTORS)
                            CORRUPTCHK_PRINT(5, CORRUPT_WALL|j, "WALL[%d].NEXTSECTOR=%d INVALID!!!",
                                             j, ns);
                        else
                            CORRUPTCHK_PRINT(4, CORRUPT_WALL|j, "WALL[%d].NEXTSECTOR=%d out of range: numsectors=%d",
                                             j, ns, numsectors);
                        OSD_Printf("    will make wall %d white on tryfix\n", j);
                    }
                    else if (TRYFIX_CNUM(onumct))  // CODEDUP MAKE_WALL_WHITE
                    {
                        wall[j].nextwall = wall[j].nextsector = -1;
                        OSD_Printf(CCHK_CORRECTED "auto-correction: made wall %d white\n", j);
                    }
                }

                if (nw>=w0 && nw<=endwall)
                    CORRUPTCHK_PRINT(4, CORRUPT_WALL|j, "WALL[%d].NEXTWALL is its own sector's wall", j);

                if (wall[j].x==POINT2(j).x && wall[j].y==POINT2(j).y)
                    CORRUPTCHK_PRINT(3, CORRUPT_WALL|j, "WALL[%d] has length 0", j);

#ifdef YAX_ENABLE
                // Various TROR checks.
                {
                    int32_t cf;

                    for (cf=0; cf<2; cf++)
                    {
                        const int32_t ynw = yax_getnextwall(j, cf);

                        if (ynw >= 0)
                        {
                            if (ynw >= numwalls)
                                CORRUPTCHK_PRINT(4, CORRUPT_WALL|j, "WALL %d's %s=%d out of range: numwalls=%d",
                                                 j, YUPDOWNWALL[cf], ynw, numwalls);
                            else
                            {
                                int32_t ynextwallok = 1;

                                if (j == ynw)
                                {
                                    CORRUPTCHK_PRINT(4, CORRUPT_WALL|j, "WALL %d's %s is itself",
                                                     j, YUPDOWNWALL[cf]);
                                    ynextwallok = 0;
                                }
                                else if (!walls_have_equal_endpoints(j, ynw))
                                {
                                    CORRUPTCHK_PRINT(4, CORRUPT_WALL|j, "WALL %d's and its %s=%d's "
                                                     "endpoints are inconsistent", j, YUPDOWNWALL[cf], ynw);
                                    ynextwallok = 0;
                                }

                                {
                                    const int16_t bunchnum = yax_getbunch(i, cf);
                                    const int32_t onumct = numcorruptthings;

                                    if (bunchnum < 0 || bunchnum >= numyaxbunches)
                                    {
                                        if (tryfixing == 0ull)
                                        {
                                            CORRUPTCHK_PRINT(4, CORRUPT_WALL|j, "WALL %d has %s=%d, "
                                                             "but its %s bunchnum=%d is invalid",
                                                             j, YUPDOWNWALL[cf], ynw,
                                                             cf==YAX_CEILING? "ceiling":"floor", bunchnum);
                                            OSD_Printf("    will clear wall %d's %s to -1 on tryfix\n",
                                                       j, yupdownwall[cf]);

                                        }
                                        else if (tryfixing & (1ull<<onumct))
                                        {
                                            yax_setnextwall(j, cf, -1);
                                            OSD_Printf(CCHK_CORRECTED "auto-correction: cleared wall %d's %s to -1\n",
                                                       j, yupdownwall[cf]);
                                        }
                                    }
                                    else if (!ynextwallok && onumct < MAXCORRUPTTHINGS)
                                    {
                                        if ((tryfixing & (1ull<<onumct)) || 4>=printfromlev)
                                            correct_yax_nextwall(j, bunchnum, cf, tryfixing!=0ull);
                                    }
                                }

                                if (ynextwallok)
                                {
                                    const int32_t onumct = numcorruptthings;
                                    const int32_t ynwp2 = yax_getnextwall(ynw, !cf);

                                    if (ynwp2 != j)
                                    {
                                        CORRUPTCHK_PRINT(4, CORRUPT_WALL|j, "WALL %d's %s=%d's reverse link wrong"
                                                         " (expected %d, have %d)", j, YUPDOWNWALL[cf], ynw, j, ynwp2);
                                        if (onumct < MAXCORRUPTTHINGS)
                                        {
                                            if (tryfixing & (1ull<<onumct))
                                            {
                                                yax_setnextwall(ynw, !cf, j);
                                                OSD_Printf(CCHK_CORRECTED "auto-correction: set wall %d's %s=%d's %s to %d\n",
                                                           j, yupdownwall[cf], ynw, yupdownwall[!cf], j);
                                            }
                                            else if (4>=printfromlev)
                                            {
                                                OSD_Printf("   will set wall %d's %s=%d's %s to %d on tryfix\n",
                                                           j, yupdownwall[cf], ynw, yupdownwall[!cf], j);
                                            }
                                        }
                                    }
                                }   // brace woot!
                            }
                        }
                    }
                }
#endif
                // Check for ".nextsector is its own sector"
                if (ns == i)
                {
                    if (!bad)
                    {
                        const int32_t onumct = numcorruptthings;
                        const int32_t safetoclear = (nw==j || (wall[nw].nextwall==-1 && wall[nw].nextsector==-1));

                        CORRUPTCHK_PRINT(4, CORRUPT_WALL|j, "WALL[%d].NEXTSECTOR is its own sector", j);
                        if (onumct < MAXCORRUPTTHINGS)
                        {
                            if (tryfixing & (1ull<<onumct))
                            {
                                if (safetoclear)
                                {
                                    wall[j].nextwall = wall[j].nextsector = -1;
                                    OSD_Printf(CCHK_CORRECTED "auto-correction: cleared wall %d's nextwall"
                                               " and nextsector\n", j);
                                }
                                else
                                    do_nextsector_correction(nw, j);
                            }
                            else if (4>=printfromlev)
                            {
                                if (safetoclear)
                                    OSD_Printf("   will clear wall %d's nextwall and nextsector on tryfix\n", j);
                                else
                                    suggest_nextsector_correction(nw, j);
                            }
                        }
                    }
                }

                // Check for ".nextwall already referenced from wall ..."
                if (!corruptcheck_noalreadyrefd && nw>=0 && nw<numwalls)
                {
                    if (seen_nextwalls[nw>>3]&(1<<(nw&7)))
                    {
                        const int32_t onumct = numcorruptthings;

                        const int16_t lnws = lastnextwallsource[nw];
                        const int16_t nwnw = wall[nw].nextwall;

                        CORRUPTCHK_PRINT(3, CORRUPT_WALL|j, "WALL[%d].NEXTWALL=%d already referenced from wall %d",
                                         j, nw, lnws);

                        if (onumct < MAXCORRUPTTHINGS && (nwnw==j || nwnw==lnws))
                        {
                            const int32_t walltoclear = nwnw==j ? lnws : j;

                            if (tryfixing & (1ull<<onumct))
                            {
                                wall[walltoclear].nextsector = wall[walltoclear].nextwall = -1;
                                OSD_Printf(CCHK_CORRECTED "auto-correction: cleared wall %d's nextwall and nextsector tags to -1\n",
                                           walltoclear);
                            }
                            else if (3 >= printfromlev)
                                OSD_Printf("    wall[%d].nextwall=%d, suggest clearing wall %d's nextwall and nextsector tags to -1\n",
                                           nw, nwnw, walltoclear);
                        }
                    }
                    else
                    {
                        seen_nextwalls[nw>>3] |= 1<<(nw&7);
                        lastnextwallsource[nw] = j;
                    }
                }

                // Various checks of .nextsector and .nextwall
                if (bad < 4)
                {
                    const int32_t onumct = numcorruptthings;

                    if ((ns^nw)<0)
                    {
                        CORRUPTCHK_PRINT(4, CORRUPT_WALL|j, "WALL[%d].NEXTSECTOR=%d and .NEXTWALL=%d inconsistent:"
                                         " missing one next pointer", j, ns, nw);
                        if (onumct < MAXCORRUPTTHINGS)
                        {
                            if (tryfixing & (1ull<<onumct))
                                do_nextsector_correction(nw, j);
                            else if (4>=printfromlev)
                                suggest_nextsector_correction(nw, j);
                        }
                    }
                    else if (ns>=0)
                    {
                        if (nw<sector[ns].wallptr || nw>=sector[ns].wallptr+sector[ns].wallnum)
                        {
                            CORRUPTCHK_PRINT(4, CORRUPT_WALL|j, "WALL[%d].NEXTWALL=%d out of .NEXTSECTOR=%d's bounds [%d .. %d]",
                                             j, nw, ns, TrackerCast(sector[ns].wallptr), sector[ns].wallptr+sector[ns].wallnum-1);
                            if (onumct < MAXCORRUPTTHINGS)
                            {
                                if (tryfixing & (1ull<<onumct))
                                    do_nextsector_correction(nw, j);
                                else if (4 >= printfromlev)
                                    suggest_nextsector_correction(nw, j);
                            }
                        }
#if 0
                        // this one usually appears together with the "already referenced" corruption
                        else if (wall[nw].nextsector != i || wall[nw].nextwall != j)
                        {
                            CORRUPTCHK_PRINT(4, CORRUPT_WALL|nw, "WALL %d nextwall's backreferences inconsistent. Expected nw=%d, ns=%d; got nw=%d, ns=%d",
                                             nw, i, j, wall[nw].nextsector, wall[nw].nextwall);
                        }
#endif
                    }
                }

                errlevel = max(errlevel, bad);
            }
        }
#if CCHK_LOOP_CHECKS
        // Wall loop checks.
        if (bad < 4 && numw >= 4)
        {
            const int32_t outerloopstart = determine_outer_loop(i);

            if (outerloopstart == -1)
                CORRUPTCHK_PRINT(2, CORRUPT_SECTOR|i, "SECTOR %d contains no outer (clockwise) loop", i);
            else if (outerloopstart == -2)
                CORRUPTCHK_PRINT(2, CORRUPT_SECTOR|i, "SECTOR %d contains more than one outer (clockwise) loops", i);
# if CCHK_LOOP_CHECKS >= 2
            else
            {
                // Now, check for whether every wall-point of every inner loop of
                // this sector (<i>) is inside the outer one.

                for (j=w0; j<=endwall; /* will step by loops */)
                {
                    const int32_t nextloopstart = get_nextloopstart(j);

                    if (j != outerloopstart)
                    {
                        int32_t k;

                        for (k=j; k<nextloopstart; k++)
                            if (!loopinside(wall[k].x, wall[k].y, outerloopstart))
                            {
                                CORRUPTCHK_PRINT(4, CORRUPT_WALL|k, "WALL %d (of sector %d) is outside the outer loop", k, i);
                                goto end_wall_loop_checks;
                            }
                    }

                    j = nextloopstart;
                }
end_wall_loop_checks:
                ;
            }
# endif
            errlevel = max(errlevel, bad);
        }
#endif
    }

    bad = 0;
    for (i=0; i<MAXSPRITES; i++)
    {
        if (sprite[i].statnum==MAXSTATUS)
            continue;

        if (sprite[i].sectnum<0 || sprite[i].sectnum>=numsectors)
            CORRUPTCHK_PRINT(4, CORRUPT_SPRITE|i, "SPRITE[%d].SECTNUM=%d. Expect problems!", i, TrackerCast(sprite[i].sectnum));

        if (sprite[i].statnum<0 || sprite[i].statnum>MAXSTATUS)
            CORRUPTCHK_PRINT(4, CORRUPT_SPRITE|i, "SPRITE[%d].STATNUM=%d. Expect problems!", i, TrackerCast(sprite[i].statnum));

        if (sprite[i].picnum<0 || sprite[i].picnum>=MAXTILES)
        {
            sprite[i].picnum = 0;
            CORRUPTCHK_PRINT(0, CORRUPT_SPRITE|i, "SPRITE[%d].PICNUM=%d out of range, resetting to 0", i, TrackerCast(sprite[i].picnum));
        }

        if (corruptcheck_game_duke3d)
        {
            const int32_t tilenum = sprite[i].picnum;

            if (tilenum >= 1 && tilenum <= 9 && (sprite[i].cstat&48))
            {
                const int32_t onumct = numcorruptthings;

                CORRUPTCHK_PRINT(1, CORRUPT_SPRITE|i, "%s sprite %d is not face-aligned",
                                 names[tilenum], i);

                if (onumct < MAXCORRUPTTHINGS)
                {
                    if (tryfixing & (1ull<<onumct))
                    {
                        sprite[i].cstat &= ~(32+16);
                        OSD_Printf(CCHK_CORRECTED "auto-correction: cleared sprite[%d].cstat bits 16 and 32\n", i);
                    }
                    else if (1 >= printfromlev)
                        OSD_Printf("   suggest clearing sprite[%d].cstat bits 16 and 32\n", i);
                }
            }
        }

        if (klabs(sprite[i].x) > BXY_MAX || klabs(sprite[i].y) > BXY_MAX)
        {
            const int32_t onumct = numcorruptthings;

            CORRUPTCHK_PRINT(3, CORRUPT_SPRITE|i, "SPRITE %d at [%d, %d] is outside the maximal grid range [%d, %d]",
                             i, TrackerCast(sprite[i].x), TrackerCast(sprite[i].y), -BXY_MAX, BXY_MAX);

            if (onumct < MAXCORRUPTTHINGS)
            {
                int32_t x=0, y=0, ok=0;
                const int32_t sect = sprite[i].sectnum;

                if ((unsigned)sect < (unsigned)numsectors)
                {
                    const int32_t firstwall = sector[sect].wallptr;

                    if ((unsigned)firstwall < (unsigned)numwalls)
                    {
                        x = wall[firstwall].x;
                        y = wall[firstwall].y;
                        ok = 1;
                    }
                }

                if (!(tryfixing & (1ull<<onumct)))
                {
                    if (ok && 3 >= printfromlev)
                        OSD_Printf("   will reposition to its sector's (%d) first"
                                   " point [%d,%d] on tryfix\n", sect, x, y);
                }
                else
                {
                    if (ok)
                    {
                        sprite[i].x = x;
                        sprite[i].y = y;
                        OSD_Printf(CCHK_CORRECTED "auto-correction: repositioned sprite %d to "
                                   "its sector's (%d) first point [%d,%d]\n", i, sect, x, y);
                    }
                }
            }
        }
    }

    i = check_spritelist_consistency();
    if (i)
        CORRUPTCHK_PRINT(5, i<0?0:(CORRUPT_SPRITE|i), CCHK_PANIC "SPRITE LISTS CORRUPTED: error code %d, s=%d, i=%d!",
                         i, csc_s, csc_i);

    if (0)
    {
too_many_errors:
        if (printfromlev<=errlevel)
            OSD_Printf("!! too many errors, stopping. !!\n");
    }

    errlevel = max(errlevel, bad);

    if (errlevel)
    {
        if (printfromlev<=errlevel)
            OSD_Printf("-- corruption level: %d\n", errlevel);
        if (tryfixing)
            OSD_Printf("--\n");
    }

    if (seen_nextwalls)
    {
        Bfree(seen_nextwalls);
        Bfree(lastnextwallsource);
    }

    corruptlevel = errlevel;

    return errlevel;
}
////

////////// STATUS BAR MENU "class" //////////

#define MENU_MAX_ENTRIES (8*3)
#define MENU_ENTRY_SIZE 25  // max. length of label (including terminating NUL)

#define MENU_Y_SPACING 8
#define MENU_BASE_Y (ydim-STATUS2DSIZ+32)

#define MENU_FG_COLOR editorcolors[11]
#define MENU_BG_COLOR editorcolors[0]
#define MENU_BG_COLOR_SEL editorcolors[1]

# define MENU_HAVE_DESCRIPTION 0

typedef struct StatusBarMenu_ {
    const char *const menuname;
    const int32_t custom_start_index;
    int32_t numentries;

    void (*process_func)(const struct StatusBarMenu_ *m, int32_t col, int32_t row);

    intptr_t auxdata[MENU_MAX_ENTRIES];
    char *description[MENU_MAX_ENTRIES];  // strdup'd description string, NULL if non
    char name[MENU_MAX_ENTRIES][MENU_ENTRY_SIZE];
} StatusBarMenu;

#define MENU_INITIALIZER_EMPTY(MenuName, ProcessFunc) \
    { MenuName, 0, 0, ProcessFunc, {}, {}, {} }
#define MENU_INITIALIZER(MenuName, CustomStartIndex, ProcessFunc, ...) \
    { MenuName, CustomStartIndex, CustomStartIndex, ProcessFunc, {}, {}, ## __VA_ARGS__ }

// NOTE: Does not handle description strings! (Only the Lua menu uses them.)
static void M_UnregisterFunction(StatusBarMenu *m, intptr_t auxdata)
{
    int32_t i, j;

    for (i=m->custom_start_index; i<m->numentries; i++)
        if (m->auxdata[i]==auxdata)
        {
            for (j=i; j<m->numentries-1; j++)
            {
                m->auxdata[j] = m->auxdata[j+1];
                Bmemcpy(m->name[j], m->name[j+1], MENU_ENTRY_SIZE);
            }

            m->auxdata[j] = 0;
            Bmemset(m->name[j], 0, MENU_ENTRY_SIZE);

            m->numentries--;

            break;
        }
}

static void M_RegisterFunction(StatusBarMenu *m, const char *name, intptr_t auxdata, const char *description)
{
    int32_t i;

    for (i=8; i<m->numentries; i++)
    {
        if (m->auxdata[i]==auxdata)
        {
            // same auxdata, different name
            Bstrncpyz(m->name[i], name, MENU_ENTRY_SIZE);
            return;
        }
        else if (!Bstrncmp(m->name[i], name, MENU_ENTRY_SIZE))
        {
            // same name, different auxdata
            m->auxdata[i] = auxdata;
            return;
        }
    }

    if (m->numentries == MENU_MAX_ENTRIES)
        return;  // max reached

    Bstrncpyz(m->name[m->numentries], name, MENU_ENTRY_SIZE);
    m->auxdata[m->numentries] = auxdata;

#if MENU_HAVE_DESCRIPTION
    // NOTE: description only handled here (not above).
    if (description)
        m->description[m->numentries] = Xstrdup(description);
#else
    UNREFERENCED_PARAMETER(description);
#endif

    m->numentries++;
}

static void M_DisplayInitial(const StatusBarMenu *m)
{
    int32_t x = 8, y = MENU_BASE_Y+16;
    int32_t i;

    for (i=0; i<m->numentries; i++)
    {
        if (i==8 || i==16)
        {
            x += 208;
            y = MENU_BASE_Y+16;
        }

        printext16(x,y, MENU_FG_COLOR, MENU_BG_COLOR, m->name[i], 0);
        y += MENU_Y_SPACING;
    }

    printext16(m->numentries>8 ? 216 : 8, MENU_BASE_Y, MENU_FG_COLOR, -1, m->menuname, 0);

    clearkeys();
}

static void M_EnterMainLoop(StatusBarMenu *m)
{
    char disptext[80];
    const int32_t dispwidth = MENU_ENTRY_SIZE-1;

    int32_t i, col=0, row=0;
    int32_t crowmax[3] = {-1, -1, -1};
    int32_t xpos = 8, ypos = MENU_BASE_Y+16;

    if (m->numentries == 0)
    {
        printmessage16("%s menu has no entries", m->menuname);
        return;
    }

    Bmemset(disptext, 0, sizeof(disptext));

    Bassert((unsigned)m->numentries <= MENU_MAX_ENTRIES);
    for (i=0; i<=(m->numentries-1)/8; i++)
        crowmax[i] = (m->numentries >= (i+1)*8) ? 7 : (m->numentries-1)&7;

    drawgradient();
    M_DisplayInitial(m);

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

        _printmessage16("Select an option, press <Esc> to exit");

        if (PRESSED_KEYSC(DOWN))
        {
            if (row < crowmax[col])
            {
                printext16(xpos, ypos+row*MENU_Y_SPACING, MENU_FG_COLOR, MENU_BG_COLOR, disptext, 0);
                row++;
            }
        }
        else if (PRESSED_KEYSC(UP))
        {
            if (row > 0)
            {
                printext16(xpos, ypos+row*MENU_Y_SPACING, MENU_FG_COLOR, MENU_BG_COLOR, disptext, 0);
                row--;
            }
        }
        else if (PRESSED_KEYSC(LEFT))
        {
            if (col > 0)
            {
                printext16(xpos, ypos+row*8, MENU_FG_COLOR, 0, disptext, 0);
                col--;
                xpos -= 208;
                disptext[dispwidth] = 0;
                row = min(crowmax[col], row);
            }
        }
        else if (PRESSED_KEYSC(RIGHT))
        {
            if (col < 2 && crowmax[col+1]>=0)
            {
                printext16(xpos, ypos+row*8, MENU_FG_COLOR, 0, disptext, 0);
                col++;
                xpos += 208;
                disptext[dispwidth] = 0;
                row = min(crowmax[col], row);
            }
        }

        for (i=Bsnprintf(disptext, dispwidth, "%s", m->name[col*8 + row]); i < dispwidth; i++)
            disptext[i] = ' ';

        if (PRESSED_KEYSC(ENTER))
        {
            Bassert(m->process_func != NULL);
            m->process_func(m, col, row);
            break;
        }

#if MENU_HAVE_DESCRIPTION
        if (M_HaveDescription(m))
        {
            const int32_t maxrows = 20;
            int32_t r;

            for (r=0; r<maxrows+1; r++)
                printext16(16-4, 16-4 + r*8, 0, MENU_BG_COLOR,  // 71 blanks:
                           "                                                                       ", 0);
            if (m->description[col*8 + row] != NULL)
                printext16(16, 16, MENU_FG_COLOR, MENU_BG_COLOR, m->description[col*8 + row], 0);
        }
#endif
        printext16(xpos, ypos+row*MENU_Y_SPACING, MENU_FG_COLOR, MENU_BG_COLOR_SEL, disptext, 0);
        videoShowFrame(1);
    }

    printext16(xpos, ypos+row*MENU_Y_SPACING, MENU_FG_COLOR, MENU_BG_COLOR, disptext, 0);
    videoShowFrame(1);

    keystatus[KEYSC_ESC] = 0;
}

////////// SPECIAL FUNCTIONS MENU //////////

static void FuncMenu_Process(const StatusBarMenu *m, int32_t col, int32_t row);

static StatusBarMenu g_specialFuncMenu = MENU_INITIALIZER(
    "Special functions", 8, FuncMenu_Process,

    {
        "Replace invalid tiles",
        "Delete all spr of tile #",
        "Set map sky shade",
        "Set map sky height",
        "Global Z coord shift",
        "Resize selection",
        "Global shade divide",
        "Global visibility divide"
    }
);

// "External functions":

void FuncMenu(void)
{
    M_EnterMainLoop(&g_specialFuncMenu);
}

void registerMenuFunction(const char *funcname, int32_t stateidx)
{
    if (funcname)
        M_RegisterFunction(&g_specialFuncMenu, funcname, stateidx, NULL);
    else
        M_UnregisterFunction(&g_specialFuncMenu, stateidx);
}

// The processing function...

static int32_t correct_picnum(int16_t *picnumptr)
{
    int32_t picnum = *picnumptr;

    if ((unsigned)picnum >= MAXTILES || tilesiz[picnum].x <= 0)
    {
        *picnumptr = 0;
        return 1;
    }

    return 0;
}

static void FuncMenu_Process(const StatusBarMenu *m, int32_t col, int32_t row)
{
    int32_t i, j;

    switch (col)
    {

    case 1:
    case 2:
    {
        const int32_t stateidx = (Bassert(m->auxdata[col*8 + row] < g_stateCount),
                                  m->auxdata[col*8 + row]);

        const char *statename = statesinfo[stateidx].name;
        int32_t snlen = Bstrlen(statename);
        char *tmpscript = (char *)Xmalloc(1+5+1+snlen+1);

        tmpscript[0] = ' ';  // don't save in history
        Bmemcpy(&tmpscript[1], "state", 5);
        tmpscript[1+5] = ' ';
        Bmemcpy(&tmpscript[1+5+1], statename, snlen);
        tmpscript[1+5+1+snlen] = 0;

        M32RunScript(tmpscript);
        Bfree(tmpscript);

        if (vm.flags&VMFLAG_ERROR)
            printmessage16("There were errors while executing the menu function");
        else if (lastpm16time != totalclock)
            printmessage16("Menu function executed successfully");
    }
    break;

    case 0:
    {
        switch (row)
        {

        case 0:
        {
            j = 0;

            for (i=0; i<MAXSECTORS; i++)
            {
                j += correct_picnum(&sector[i].ceilingpicnum);
                j += correct_picnum(&sector[i].floorpicnum);
            }

            for (i=0; i<MAXWALLS; i++)
            {
                j += correct_picnum(&wall[i].picnum);
                j += correct_picnum(&wall[i].overpicnum);
            }
            for (i=0; i<MAXSPRITES; i++)
                j += correct_picnum(&sprite[i].picnum);

            printmessage16("Replaced %d invalid tiles",j);
        }
        break;

        case 1:
        {
            char tempbuf[64];
            Bsprintf(tempbuf,"Delete all sprites of tile #: ");
            i = getnumber16(tempbuf,-1,MAXSPRITES-1,1);
            if (i >= 0)
            {
                int32_t k = 0;
                for (j=0; j<MAXSPRITES; j++)
                    if (sprite[j].picnum == i)
                        deletesprite(j), k++;
                printmessage16("%d sprite(s) deleted",k);
            }
            else printmessage16("Aborted");
        }
        break;

        case 2:
        {
            j=getnumber16("Set map sky shade:    ",0,128,1);

            for (i=0; i<numsectors; i++)
            {
                if (sector[i].ceilingstat&1)
                    sector[i].ceilingshade = j;
            }
            printmessage16("All parallax skies adjusted");
        }
        break;

        case 3:
        {
            j=getnumber16("Set map sky height:    ",0,16777216,1);
            if (j != 0)
            {
                for (i=0; i<numsectors; i++)
                {
                    if (sector[i].ceilingstat&1)
                        sector[i].ceilingz = j;
                }
                printmessage16("All parallax skies adjusted");
            }
            else printmessage16("Aborted");
        }
        break;

        case 4:
        {
            j=getnumber16("Z offset:    ",0,16777216,1);
            if (j!=0)
            {
                for (i=0; i<numsectors; i++)
                {
                    sector[i].ceilingz += j;
                    sector[i].floorz += j;
                }
                for (i=0; i<MAXSPRITES; i++)
                    sprite[i].z += j;
                printmessage16("Map adjusted");
            }
            else printmessage16("Aborted");
        }
        break;

        case 5:
        {
            j=getnumber16("Percentage of original:    ",100,1000,0);

            if (j!=100 && j!=0)
            {
                int32_t w, currsector, start_wall, end_wall;
                double size = (j/100.f);

                for (i = 0; i < highlightsectorcnt; i++)
                {
                    currsector = highlightsector[i];
                    sector[currsector].ceilingz = (int32_t)(sector[currsector].ceilingz*size);
                    sector[currsector].floorz = (int32_t)(sector[currsector].floorz*size);

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

                    for (w = start_wall; w < end_wall; w++)
                    {
                        wall[w].x = (int32_t)(wall[w].x*size);
                        wall[w].y = (int32_t)(wall[w].y*size);
                        wall[w].yrepeat = min((int32_t)(wall[w].yrepeat/size),255);
                    }

                    w = headspritesect[highlightsector[i]];
                    while (w >= 0)
                    {
                        sprite[w].x = (int32_t)(sprite[w].x*size);
                        sprite[w].y = (int32_t)(sprite[w].y*size);
                        sprite[w].z = (int32_t)(sprite[w].z*size);
                        sprite[w].xrepeat = min(max((int32_t)(sprite[w].xrepeat*size),1),255);
                        sprite[w].yrepeat = min(max((int32_t)(sprite[w].yrepeat*size),1),255);
                        w = nextspritesect[w];
                    }
                }
                printmessage16("Map scaled");
            }
            else printmessage16("Aborted");
        }
        break;

        case 6:
        {
            j=getnumber16("Shade divisor:    ",1,128,1);
            if (j > 1)
            {
                for (i=0; i<numsectors; i++)
                {
                    sector[i].ceilingshade /= j;
                    sector[i].floorshade /= j;
                }
                for (i=0; i<numwalls; i++)
                    wall[i].shade /= j;
                for (i=0; i<MAXSPRITES; i++)
                    sprite[i].shade /= j;
                printmessage16("Shades adjusted");
            }
            else printmessage16("Aborted");
        }
        break;

        case 7:
        {
            j=getnumber16("Visibility divisor:    ",1,128,0);
            if (j > 1)
            {
                for (i=0; i<numsectors; i++)
                {
                    if (sector[i].visibility < 240)
                        sector[i].visibility /= j;
                    else sector[i].visibility = 240 + (sector[i].visibility>>4)/j;
                }
                printmessage16("Visibility adjusted");
            }
            else printmessage16("Aborted");
        }
        break;

        }  // switch (row)
    }
    break;  // switch (col) / case 0

    }  // switch (col)
}