code:
gamevar vol 0 0
gamevar lev 0 0

// out-of-bounds volume/level numbers
gamevar badvol_oob 99 0
gamevar badlev_oob 999 0

// volume/level numbers for which no level is defined
gamevar badvol_nd 3 0
gamevar badlev_nd 32 0

define Q_tmp 255
definequote Q_tmp <temp>
define Q_tmp2 256
definequote Q_tmp2 <temp>

define Q_last_level 1000
definequote Q_last_level Last level (1-based): %d
define Q_dtime 1001
definequote Q_dtime Designer time for last level: %s

state print_designer_time
    // This must not invoke an oob access on the C side if ud.last_level < 0!
    qgetsysstr Q_tmp2 STR_DESIGNERTIME

    qsprintf Q_tmp Q_dtime Q_tmp2
    userquote Q_tmp
ends

onevent EVENT_NEWGAME
    getuserdef .last_level lev
    qsprintf Q_tmp Q_last_level lev
    userquote Q_tmp

    state print_designer_time
endevent

onevent EVENT_ENTERLEVEL
    state print_designer_time

    // must fail, since the current player is -1 in this event
    qgetsysstr Q_tmp STR_PLAYERNAME
endevent

onevent EVENT_USESTEROIDS
    getuserdef .volume_number vol
    setuserdef .volume_number badvol_oob  // LunaCON errors here
    qgetsysstr Q_tmp STR_VOLUMENAME  // C-CON errors here
    setuserdef .volume_number vol
endevent

onevent EVENT_USEJETPACK
    getuserdef .volume_number vol
    getuserdef .level_number lev

    setuserdef .volume_number badvol_oob  // LunaCON errors here
    setuserdef .level_number badlev_oob
    qgetsysstr Q_tmp STR_MAPNAME  // C-CON errors here

    setuserdef .volume_number vol
    setuserdef .level_number lev
endevent

onevent EVENT_JUMP
    getuserdef .volume_number vol
    getuserdef .level_number lev

    setuserdef .volume_number badvol_nd
    setuserdef .level_number badlev_nd
    qgetsysstr Q_tmp STR_MAPFILENAME  // LunaCON, C-CON error here

    setuserdef .volume_number vol
    setuserdef .level_number lev
endevent


code:
// Tests for the qsubstr command.
// This one should be safe to run in C-CON.

// overlong string at definition (should warn)
define QSRC 400
definequote QSRC -123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789=123456789
define QSRC_SHORT 401
definequote QSRC_SHORT -123456789

define QDST 500
definequote QDST

// for gettimedate
gamevar sec 0 0
gamevar x 0 0

onevent EVENT_ENTERLEVEL
    // first, a plain, non-border-case call
    qsubstr QDST QSRC 10 10  // -> "-123456789"
    userquote QDST

    // length zero yields the empty string
    qsubstr QDST QSRC 20 0  // -> ""
    userquote QDST

    // start+length > strlen of source: no error
    qsubstr QDST QSRC_SHORT 8 6  // -> "89"
    userquote QDST

    // start == strlen of source
    qsubstr QDST QSRC_SHORT 10 1  // -> ""
    userquote QDST

    // Make QDST contain
    // -123456789<NUL>123456789-...
    qstrcpy QDST QSRC_SHORT
    qstrcpy QSRC_SHORT QSRC
    qstrcpy QSRC_SHORT QDST
    userquote QSRC_SHORT  // -> "-123456789"

    // start == strlen of source, trailing bytes after source <NUL>
    qsubstr QDST QSRC_SHORT 10 2  // -> ""
    userquote QDST

    // start is beyond-<NUL> of the source
    qsubstr QDST QSRC_SHORT 11 9  // -> ""
    userquote QDST

    // copy whole string (127 chars + 1 <NUL> == MAXQUOTELEN)
    qsubstr QDST QSRC 0 1000  // -> "-123456789-123456789-...=123456"
    userquote QDST

    /** TESTS FOR source == dest **/

    qsubstr QDST QDST 0 20  // -> "-123456789-123456789"
    userquote QDST

    qsubstr QDST QDST 5 10  // -> "56789-1234"
    userquote QDST

    /** TESTS FOR invalid input **/

    gettimedate sec x x x x x x x
    ifvarand sec 1
        qsubstr QDST QSRC -1 10  // invalid start
    else
        qsubstr QDST QSRC 0 -1  // invalid length

    // Reached only in C-CON:
    qsubstr QDST QSRC 128 0  // invalid start
endevent

repo_name: orochi/nblood

license: mit


code:
// Don't try in C-CON!

gamevar slen 0 0
gamevar n -12345678 0  // a number having length 9 in the decimal representation

// overlong string at definition (should warn)
definequote 400 123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789=123456789-

// alloc some quotes, test empty quote syntax
definequote 401
definequote 402
definequote 403
definequote 404
definequote 405
definequote 406

onevent EVENT_ENTERLEVEL
    redefinequote 114 QWE

    redefinequote 115 012345678|
    // qstrcat X Y, where X==Y: duplicates the quote
    qstrcat 115 115
    userquote 115  // "012345678|012345678|"

    // simple qstrcpy test
    qstrcpy 116 115
    userquote 116  // "012345678|012345678|"

    redefinequote 117 %s -- %d -- %s -- %d slen=%d
    // test:
    // - same destination quote as the format quote
    // - multiple conversions
    qstrlen slen 115  // -> 10
    qsprintf 117 /*<-*/ 117 /*args:*/ 116 9999 114 5555 slen
    userquote 117  // "012345678|012345678| -- 9999 -- QWE -- 5555 slen=10"

    redefinequote 117 %s -- %d -- %s -- %d slen=%d
    // test:
    // - same destination quote as one of the source quotes under %s conversion
    qsprintf 116 /*<-*/ 117 /*args:*/ 116 9999 116 5555 slen
    userquote 116  // "012345678|012345678| -- 9999 -- 012345678|012345678| -- 5555 slen=10"

    // aliased qstrcpy test (a no-op)
    qstrcpy 115 115
    userquote 115  // still "012345678|012345678|"

    qstrcat 115 115  // len 40
    qstrcat 115 115  // len 80
    qstrcat 115 115
    // result now: 12 x "012345678|" concantenated with "0123456" (= total length 127 = MAXQUOTELEN-1)
    qstrcat 115 115  // this one is redundant, but must not overflow the quote buffer
    userquote 115

    ////////// qstrncat test
    redefinequote 300 012345678|
    qstrncat 300 300 2
    userquote 300  // "012345678|01"
    //////////

    redefinequote 117 X%s
    qsprintf 117 /*<-*/ 117 /*args:*/ 115
    userquote 117  // result: "X" .. 12 x "012345678|" .. "012345" (= total length 127 = MAXQUOTELEN-1)

    // 32 %d (or %ld) conversions
    redefinequote 117 %ld|%d|%ld|%d|%ld|%ld|%d|%ld|%d|%ld/%ld|%d|%ld|%d|%ld|%ld|%d|%ld|%d|%ld/%ld|%d|%ld|%d|%ld|%ld|%d|%ld|%d|%ld/%d/%d
    // string shorter than MAXQUOTELEN-1:
    qsprintf 116 /*<-*/ 117 /*args:*/ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    // result: "0|1|2|3|4|5|6|7|8|9/0|1|2|3|4|5|6|7|8|9/0|1|2|3|4|5|6|7|8|9/0/1"
    userquote 116

    // string longer than MAXQUOTELEN-1:
    qsprintf 116 /*<-*/ 117 /*args:*/ n n n n n n n n n n /**/ n n n n n n n n n n /**/ n n
    userquote 116
    // result: 10 x "-12345678" .. "/-12345678|-12345678|-123456" (= total length 127 = MAXQUOTELEN-1)

    // overlong string at redefinition
    redefinequote 117 123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789=123456789-
    userquote 117  // should end in "=1234567" (= total length 127 = MAXQUOTELEN-1)
    userquote 400  // same thing

    // test passing empty quote
    userquote 401

    // qgetsysstr test
    qgetsysstr 401 STR_MAPNAME
    qgetsysstr 402 STR_MAPFILENAME
//    qgetsysstr 403 STR_PLAYERNAME
    qgetsysstr 404 STR_VERSION
    qgetsysstr 405 STR_GAMETYPE
    qgetsysstr 406 STR_VOLUMENAME

    redefinequote 400 %s | %s | %s | %s | %s | %s
    qsprintf 400 /*<-*/ 400 /*args:*/ 401 402 403 404 405 406
    userquote 400
endevent

// Test of 'getpname' command.
onevent EVENT_JUMP
    getpname 400 THISACTOR
    userquote 400
endevent

// Following code by Hendricks266, from
// http://forums.duke4.net/topic/1382-duke-64-mod-thread/page__view__findpost__p__150497
gamevar temp 0 0
gamevar x 0 0

definequote 666 This quote sucks

onevent EVENT_DISPLAYREST
    qstrlen temp 666
    setvarvar x tilesizx[STARTALPHANUM] // rough approximation
    mulvarvar x temp
    divvar x 2
    addvar x 160 // put your centered position here
    gametext STARTALPHANUM x 30 666 0 0 16 0 0 xdim ydim
endevent

//////////


code:
// Use as root file, not as additional CON module.

// Test dynamic tile remapping.
dynamicremap
// Make MASKWALL2 (fence in E1L1 rooftop) execute the C side code of DUKECAR.
// This must be before the original DUKECAR definition, because label
// redefinitions are ignored in CON.
// XXX: Does MASKWALL2's code get executed, too?
define DUKECAR 913

include GAME.CON
// NOTE:
// DEFS.CON 520:6: warning: label "DUKECAR" not redefined with new value 2491 (old: 913)

gamevar LOGO_FLAGS 6399 0  // 255+2048+4096, test "no E4 intro", "no E1 bonus" bits

useractor  4  58 1 NO NO 0
    // usertype 4 is "rotation-fixed actor"
    // tile 58 is the space suit

    state killme
enda

// Test useractor type "enemy"
define CYCLOIDHEAD 490
// Will have a flat-sprite-on-floor shadow and be an autoaim target.
useractor 1 CYCLOIDHEAD 10
    ifpdistl 1024 tip
    state killme
enda

onevent EVENT_JUMP
    mail 2
    money 5
    paper 3
endevent

// Speed up sector effects a little 8-)
gamevar ra_temp 0 0
eventloadactor GPSPEED
    getactor[THISACTOR].lotag ra_temp
    mulvar ra_temp 4
    setactor[THISACTOR].lotag ra_temp
enda

gamevar ii 0 0
onevent EVENT_PROCESSINPUT
    setvar ii 0
    whilevarvarn ii MAXSPRITES
    {
        ifvare sprite[ii].picnum 58  // space suit
            ssp ii CLIPMASK0

        addvar ii 1
    }
endevent


code:
// ScreenText Test

/*
This is half a test and half a demonstration.
For example, some of the spacing and offset stuff could be replaced with tests of other things, like the backwards bit, line wrapping, and space lengths.
Also, if/when Lunatic adds support for \n characters.
*/

definequote 999

definequote 1000 AbCdEfGhIjKlM
definequote 1001 nOpQrStUvWxYz

definequote 1010 X1234567890-.!?;:'\ / %

definequote 1020 ^2R^8G^1B^7Y

definequote 1337 1337

definequote 2000 Left
definequote 2001 Center
definequote 2002 Right

definequote 2003 Top
definequote 2004 Center
definequote 2005 Bottom

definequote 3000 Spacing
definequote 3001 X-Offset
definequote 3002 %d: UiUiUiUiUi
definequote 3003 Justification

definequote 4000 Normal: aBcDeF
definequote 4001 Upper: aBcDeF
definequote 4002 Lower: aBcDeF
definequote 4003 Inverted: aBcDeF

definequote 5000 Rotate
definequote 5001 Top, Left:
definequote 5002 Center, Center:
definequote 5003 Bottom, Right:

gamevar font STARTALPHANUM 0
gamevar x 0 0
gamevar y 0 0
gamevar z 65536 0
gamevar blockangle 0 0
gamevar charangle 0 0
gamevar q 1000 0
gamevar shade 0 0
gamevar pal 0 0
gamevar o 16 0
gamevar alpha 0 0
gamevar xspace 5 0
gamevar yline 8 0
gamevar xbetween 0 0
gamevar ybetween 0 0
gamevar f 0 0
gamevar x1 0 0
gamevar y1 0 0
gamevar x2 0 0
gamevar y2 0 0

gamevar temp 0 0
gamevar doaddtvblend 0 0

state resetbounds
    setvarvar x1 windowx1
    setvarvar y1 windowy1
    setvarvar x2 windowx2
    setvarvar y2 windowy2
ends

onevent EVENT_ENTERLEVEL
    state resetbounds
endevent

define TEXT_XRIGHT            0x00000001
define TEXT_XCENTER           0x00000002
define TEXT_YBOTTOM           0x00000004
define TEXT_YCENTER           0x00000008
define TEXT_INTERNALSPACE     0x00000010
define TEXT_TILESPACE         0x00000020
define TEXT_INTERNALLINE      0x00000040
define TEXT_TILELINE          0x00000080
define TEXT_XOFFSETZERO       0x00000100
define TEXT_XJUSTIFY          0x00000200
define TEXT_YOFFSETZERO       0x00000400
define TEXT_YJUSTIFY          0x00000800
define TEXT_LINEWRAP          0x00001000
define TEXT_UPPERCASE         0x00002000
define TEXT_INVERTCASE        0x00004000
define TEXT_IGNOREESCAPE      0x00008000
define TEXT_LITERALESCAPE     0x00010000
define TEXT_BACKWARDS         0x00020000
define TEXT_GAMETEXTNUMHACK   0x00040000
define TEXT_DIGITALNUMBER     0x00080000
define TEXT_BIGALPHANUM       0x00100000
define TEXT_GRAYFONT          0x00200000

state increment_line
    ifvarg q 0
        screentext font x y z blockangle charangle q shade pal o alpha xspace yline xbetween ybetween f x1 y1 x2 y2
    addvar y 8
ends

state increment_line_test
    ifvarg q 0
    {
        qsprintf 999 q temp
        screentext font x y z blockangle charangle 999 shade pal o alpha xspace yline temp ybetween f x1 y1 x2 y2
    }
    addvar temp 1
    addvar y 8
ends

onevent EVENT_DISPLAYREST
    state resetbounds

    // screentext font x y z blockangle charangle q shade pal o alpha xspace yline xbetween ybetween f x1 y1 x2 y2

    // spacing
    setvar f 0
    setvar x 35
    setvar y 3
    setvar pal 0

    setvar temp 0
    setvar q 3000
    state increment_line_test

    setvar temp -3
    setvar q 3002
    state increment_line_test
    state increment_line_test
    state increment_line_test
    state increment_line_test
    state increment_line_test
    state increment_line_test
    state increment_line_test

    // justification
    setvar f 0
    orvar f TEXT_XCENTER
    orvar f TEXT_XJUSTIFY
    screentext font 160 y z blockangle charangle 3003 shade 12 o alpha xspace yline 320 ybetween f x1 y1 x2 y2
    addvar y 8
    setvar f 0

    // x-offset
    setvar temp 0
    setvar q 3001
    state increment_line_test

    orvar f TEXT_XOFFSETZERO
    setvar temp 5
    setvar q 3002
    state increment_line_test
    state increment_line_test
    state increment_line_test
    state increment_line_test
    state increment_line_test
    state increment_line_test

    addvar y 4

    // case
    setvar f 0
    setvar q 4000
    setvar pal 23
    state increment_line
    setvar q 4001
    orvar f TEXT_UPPERCASE
    state increment_line
    setvar q 4002
    orvar f TEXT_INVERTCASE
    state increment_line
    setvar q 4003
    xorvar f TEXT_UPPERCASE
    state increment_line

    // Calculate periodically cycling alpha value. PERIODIC_ALPHA.
    setvarvar temp totalclock
    shiftvarl temp 2

    sin alpha temp   // alpha is now in [-2^14 .. 2^14]
    shiftvarr alpha 7  // [-2^7 .. 2^7]
    addvar alpha 128  // [0 .. 256]
    ifvarg alpha 254 { setvar alpha 255 setvar doaddtvblend 1 }
    else ifvarl alpha 2 { setvar doaddtvblend 0 }

    ifvare doaddtvblend 1
    {
        shiftvarr alpha 2  // [0 63]

        ifvarg alpha 31
        {
            setvar temp 63
            subvarvar temp alpha
            setvarvar alpha temp
            orvar o 32  // RS_TRANS2
        }
        // alpha now in [0 .. 31]

        addvar alpha 101  // additive translucency tables are assumed to be at [101 .. 132]
        mulvar alpha -1
    }

    // divider line
    rotatespritea 240 65 32768 512 WINDOWBORDER1 0 2 0 85 x1 y1 x2 y2

    setvar pal 12

    // x-alignment
    setvar f 0
    setvar x 220
    screentext font x 127 z blockangle charangle 2000 shade pal o alpha xspace yline 0 ybetween f x1 y1 x2 y2
    orvar f TEXT_XCENTER
    screentext font x 137 z blockangle charangle 2001 shade pal o alpha xspace yline 0 ybetween f x1 y1 x2 y2
    xorvar f TEXT_XCENTER
    orvar f TEXT_XRIGHT
    screentext font x 147 z blockangle charangle 2002 shade pal o alpha xspace yline 0 ybetween f x1 y1 x2 y2
    rotatespritea x 132 32768 0 WINDOWBORDER2 127 0 0 85 x1 y1 x2 y2

    // y-alignment
    setvar f TEXT_UPPERCASE
    screentext font 180 162 z blockangle charangle 2003 shade pal o alpha xspace yline 0 ybetween f x1 y1 x2 y2
    orvar f TEXT_YCENTER
    screentext font 208 162 z blockangle charangle 2004 shade pal o alpha xspace yline 0 ybetween f x1 y1 x2 y2
    xorvar f TEXT_YCENTER
    orvar f TEXT_YBOTTOM
    screentext font 260 162 z blockangle charangle 2005 shade pal o alpha xspace yline 0 ybetween f x1 y1 x2 y2
    rotatespritea 240 163 32768 512 WINDOWBORDER1 127 0 0 85 x1 y1 x2 y2

    setvar pal 0

    // bluefont vs. redfont XOFFSETZERO comparison
    setvar f 0
    orvar f TEXT_XOFFSETZERO
    screentext font 0 175 65536 blockangle charangle 1010 shade pal o alpha xspace yline 13 ybetween f x1 y1 x2 y2
    orvar f TEXT_BIGALPHANUM
    orvar f TEXT_YBOTTOM
    screentext BIGALPHANUM 0 200 z blockangle charangle 1010 shade pal o alpha xspace yline 13 ybetween f x1 y1 x2 y2

    // block rotation
    setvar y 2
    screentext MINIFONT 170 y z blockangle charangle 5001 shade 10 o alpha 2 yline 1 ybetween 0 x1 y1 x2 y2
    screentext MINIFONT 237 y z blockangle charangle 5002 shade 14 o alpha 2 yline 1 ybetween TEXT_XCENTER x1 y1 x2 y2
    screentext MINIFONT 317 y z blockangle charangle 5003 shade 16 o alpha 2 yline 1 ybetween TEXT_XRIGHT x1 y1 x2 y2

    setvar f 0
    setvar x 175
    setvar y 12
    setvar pal 10
    setvar blockangle 0
    screentext font x y z blockangle charangle 5000 shade pal o alpha xspace yline xbetween ybetween f x1 y1 x2 y2
    addvar blockangle 128
    screentext font x y z blockangle charangle 5000 shade pal o alpha xspace yline xbetween ybetween f x1 y1 x2 y2
    addvar blockangle 128
    screentext font x y z blockangle charangle 5000 shade pal o alpha xspace yline xbetween ybetween f x1 y1 x2 y2
    addvar blockangle 128
    screentext font x y z blockangle charangle 5000 shade pal o alpha xspace yline xbetween ybetween f x1 y1 x2 y2
    addvar blockangle 128
    screentext font x y z blockangle charangle 5000 shade pal o alpha xspace yline xbetween ybetween f x1 y1 x2 y2

    setvar blockangle -256
    setvar x 245
    setvar y 40
    setvar f 0
    setvar pal 14
    orvar f TEXT_XCENTER
    orvar f TEXT_YCENTER
    screentext font x y z blockangle charangle 5000 shade pal o alpha xspace yline xbetween ybetween f x1 y1 x2 y2
    addvar blockangle 128
    screentext font x y z blockangle charangle 5000 shade pal o alpha xspace yline xbetween ybetween f x1 y1 x2 y2
    addvar blockangle 128
    screentext font x y z blockangle charangle 5000 shade pal o alpha xspace yline xbetween ybetween f x1 y1 x2 y2
    addvar blockangle 128
    screentext font x y z blockangle charangle 5000 shade pal o alpha xspace yline xbetween ybetween f x1 y1 x2 y2
    addvar blockangle 128
    screentext font x y z blockangle charangle 5000 shade pal o alpha xspace yline xbetween ybetween f x1 y1 x2 y2

    setvar blockangle -512
    setvar x 317
    setvar y 19
    setvar f 0
    setvar pal 16
    orvar f TEXT_XRIGHT
    orvar f TEXT_YBOTTOM
    screentext font x y z blockangle charangle 5000 shade pal o alpha xspace yline xbetween ybetween f x1 y1 x2 y2
    addvar blockangle 128
    screentext font x y z blockangle charangle 5000 shade pal o alpha xspace yline xbetween ybetween f x1 y1 x2 y2
    addvar blockangle 128
    screentext font x y z blockangle charangle 5000 shade pal o alpha xspace yline xbetween ybetween f x1 y1 x2 y2
    addvar blockangle 128
    screentext font x y z blockangle charangle 5000 shade pal o alpha xspace yline xbetween ybetween f x1 y1 x2 y2
    addvar blockangle 128
    screentext font x y z blockangle charangle 5000 shade pal o alpha xspace yline xbetween ybetween f x1 y1 x2 y2

    setvar pal 15

    // character rotation
    setvar blockangle 0
    setvar charangle 256
    setvar f 0
    setvar x 175
    setvar y 78
    screentext font x y z blockangle charangle 5000 shade pal o alpha xspace yline xbetween ybetween f x1 y1 x2 y2
    addvar charangle 256
    addvar y 13
    screentext font x y z blockangle charangle 5000 shade pal o alpha xspace yline xbetween ybetween f x1 y1 x2 y2
    addvar charangle 256
    addvar y 13
    screentext font x y z blockangle charangle 5000 shade pal o alpha xspace yline xbetween ybetween f x1 y1 x2 y2
    addvar charangle 256
    addvar y 13
    screentext font x y z blockangle charangle 5000 shade pal o alpha xspace yline xbetween ybetween f x1 y1 x2 y2
    setvar charangle 0

    setvar x 225
    setvar y 78
    screentext font x y z 128 -128 5000 shade pal o alpha xspace yline xbetween ybetween f x1 y1 x2 y2
    screentext font x y z 256 -256 5000 shade pal o alpha xspace yline xbetween ybetween f x1 y1 x2 y2
    screentext font x y z 384 -384 5000 shade pal o alpha xspace yline xbetween ybetween f x1 y1 x2 y2
    screentext font x y z 512 -512 5000 shade pal o alpha xspace yline xbetween ybetween f x1 y1 x2 y2

    setvar x 312
    setvar y 90
    orvar f TEXT_XRIGHT
    orvar f TEXT_YCENTER
    screentext font x y z -128 128 5000 shade pal o alpha xspace yline xbetween ybetween f x1 y1 x2 y2
    screentext font x y z -256 256 5000 shade pal o alpha xspace yline xbetween ybetween f x1 y1 x2 y2
    screentext font x y z -384 384 5000 shade pal o alpha xspace yline xbetween ybetween f x1 y1 x2 y2
    screentext font x y z -512 512 5000 shade pal o alpha xspace yline xbetween ybetween f x1 y1 x2 y2

    // alpha, sideways, and escape colors
    setvar pal 13
    setvar blockangle 0
    setvar f 0
    orvar f TEXT_XCENTER
    screentext font 0 100 z 1536 charangle 1020 shade pal o 0 xspace yline xbetween ybetween f x1 y1 x2 y2
    orvar f TEXT_IGNOREESCAPE
    screentext font 10 100 z 1536 charangle 1020 shade pal o 85 xspace yline xbetween ybetween f x1 y1 x2 y2
    orvar f TEXT_LITERALESCAPE
    screentext font 20 100 z 1536 charangle 1020 shade pal o 170 xspace yline xbetween ybetween f x1 y1 x2 y2

    ifvarand o 32 xorvar o 32  // clear RS_TRANS2
    setvar alpha 0  // Clear alpha so it's only active from PERIODIC_ALPHA to here.
endevent


code:
--[[
 Usage: in Mapster32,
  > lua "shadexfog=reload'shadexfog'"
  -- for example
  > lua "shadexfog.create(100, 255,255,255)"
  > lua "shadexfog.translate(100, 2)"
 In EDuke32, simply pass this module at the command line.
--]]

local assert = assert
local error = error
local print = print
local printf = printf
local tonumber = tonumber
local type = type
local unpack = unpack

local bit = require("bit")
local math = require("math")
local string = require("string")
local min, max = math.min, math.max
local floor = math.floor

local sector, wall, sprite = sector, wall, sprite

local engine = require("engine")
local gv = gv

----------

local shadexfog = {}

-- Example:
--  lua "shadexfog.createremap(30, {[2]=0, [3]=1, [12]=0, [13]=1})"
-- creates a pal 30 which maps the blue and orange ramps to the gray ones.
-- (Compare with the rows of http://wiki.eduke32.com/wiki/File:Pala.png)
--
-- Sexdecatuple remappings of Duke3D pals loaded from LOOKUP.DAT:
-- Remappings that are not expressible as such and identity maps (pal 3 and 9)
-- omitted.
--
--  2:  { [0]=8, [1]=13, [2]=8, [3]=13, [4]=13, [5]=8, [6]=8, [7]=13, [9]=8, [10]=8, [11]=13, [12]=8, [14]=8, }
--  5:  { [8]=2, [13]=3, }
--  7:  { [0]=10, [1]=9, [2]=10, [3]=9, [4]=9, [5]=10, [6]=10, [7]=9, [8]=10, [11]=9, [12]=9, [13]=9, [14]=9, }
--  8:  { [0]=6, [1]=7, [2]=6, [3]=7, [4]=7, [5]=6, [8]=6, [9]=7, [10]=6, [11]=7, [12]=7, [13]=7, [14]=6, }
-- 11:  { [4]=7, [5]=6, }
-- 12:  { [4]=1, [5]=0, }
-- 15:  { [4]=3, [5]=2, }
-- 17:  { [2]=5, [3]=4, [4]=7, [5]=6, [6]=5, [7]=4, [12]=5, [14]=4, }
-- 18:  { [4]=1, [5]=0, }
-- 19:  { [2]=8, [3]=13, [4]=1, [5]=0, [6]=8, [7]=13, [12]=8, [14]=13, }
-- 20:  { [2]=5, [3]=4, [4]=1, [5]=0, [6]=5, [7]=4, [12]=5, [14]=4, }
-- 21:  { [4]=13, [5]=8, }
-- 22:  { [4]=7, [5]=6, }
-- 25:  { [6]=8, [7]=13, }
function shadexfog.createremap(palnum, remaptab)
    local sht = engine.getshadetab(0)
    engine.setshadetab(palnum, sht:remap16(remaptab))
end

-- Create 32 palookups corrensponding to different *shade levels* of a fog
-- palookup, called a "shade-x-fog" palookup set in the following.
--
-- Pals <startpalnum> .. <startpalnum>+31 will be taken.
-- <fogr>, <fogg>, <fogb>: intensities of the fog color, [0 .. 255]
function shadexfog.create(startpalnum, fogr, fogg, fogb)
    local MAXPALNUM = 255-31-engine.RESERVEDPALS
    if (not (startpalnum >= 1 and startpalnum <= MAXPALNUM)) then
        error("invalid startpalnum, max="..MAXPALNUM, 2)
    end

    local basesht = engine.getshadetab(0)

    -- Encode the shade in different pal numbers! The shade tables are
    -- constructed with a fog in their place.
    for dummyshade=0,31 do
        local sht = engine.shadetab()

        for f=0,31 do
            for i=0,255 do
                local r, g, b = engine.getrgb(basesht[dummyshade][i])

                local nr, ng, nb =
                    (r*(32-f) + fogr*f) / 32,
                    (g*(32-f) + fogg*f) / 32,
                    (b*(32-f) + fogb*f) / 32

                sht[f][i] = engine.nearcolor(nr, ng, nb)
            end
        end

        engine.setshadetab(startpalnum + dummyshade, sht)
    end
end

local function trans(what, startpalnum, fogintensity)
    if (what.pal >= startpalnum and what.pal <= startpalnum+31) then
        -- Auto-detect earlier translation with the same <startpalnum>.
        what.shade = what.pal - startpalnum
    end

    local shade = min(max(what.shade, 0), 31)
    what.pal = startpalnum + shade
    what.shade = fogintensity
end

-- shadexfog.translate(startpalnum, fogintensity [, vis])
--
-- Translate the whole map for use with a shade-x-fog palookup set.
--  .pal becomes the <startpalnum> + former .shade
--  .shade becomes the <fogintensity> [0 .. 31]
-- If <vis> is passed and >= 0, set all sector's visibility to that value.
--
-- Notes:
--  - auto-detects when the translation has been applied with the *same*
--   <startpalnum> (if a different one is desired, must reload map).
--  - if shades < 0 or > 31 present, loss of information
function shadexfog.translate(startpalnum, fogintensity, vis)
    for i=0,gv.numsectors-1 do
        trans(sector[i].ceiling, startpalnum, fogintensity)
        trans(sector[i].floor, startpalnum, fogintensity)
        if (vis and vis >= 0) then
            sector[i].visibility = vis
        end
    end

    for i=0,gv.numwalls-1 do
        trans(wall[i], startpalnum, fogintensity)
    end
end

if (gv.LUNATIC_CLIENT == gv.LUNATIC_CLIENT_EDUKE32 and LUNATIC_FIRST_TIME) then
    shadexfog.create(100, 255,255,255)
    print("created shadexfog palookups")
end

---------- BASE SHADE TABLE TESTS ----------

-- sht = shadexfog.create_depth_shtab([palnum])
function shadexfog.create_depth_shtab(palnum)
    local sht = engine.shadetab()

    for s=0,31 do
        for i=0,255 do
            sht[s][i] = s
        end
    end

    if (palnum) then
        engine.setshadetab(palnum, sht)
    end
    return sht
end

function shadexfog.create_vismarker_shtab(palnum)
    local sht = engine.getshadetab(0)

    for i=0,255 do
        sht[1][i] = 242
        sht[30][i] = 245
    end

    if (palnum) then
        engine.setshadetab(palnum, sht)
    end
    return sht
end

-- Basic test of whether for a color index i corresponding to a color (r,g,b),
-- getclosestcol() returns a color index ii corresponding to the same color.
-- (In the Duke3D palette, there are duplicates, so the requirement i==ii is
-- too strict.)
function shadexfog.test_nearcolor()
    for i=0,255 do
        local r, g, b = engine.getrgb(i)
        local ii = engine.nearcolor(r, g, b)
        local rr, gg, bb = engine.getrgb(ii)

        if (r~=rr or g~=gg or b~=bb) then
            printf("diff %d: %d,%d,%d  %d,%d,%d", i, r,g,b, rr,gg,bb)
        end
    end
end

-- Change the .pal member of all sector ceilings/floors, walls and sprites to
-- <palnum>.
function shadexfog.challpal(palnum)
    for i=0,gv.numsectors-1 do
        sector[i].ceilingpal = palnum
        sector[i].floorpal = palnum
    end
    for i=0,gv.numwalls-1 do
        wall[i].pal = palnum
    end
    for i in sprite.all() do
        sprite[i].pal = palnum
    end
end

-- Create our version of the base shade table (palookup 0)
--
-- NOTE: Nope, the base shade table is NOT created by applying a linear ramp to
-- the base palette colors!!!
local function create_base_shtab(basesht)
    local basesht = basesht or engine.getshadetab(0)

    local sht = engine.shadetab()
    sht[0] = basesht[0]
    for sh=1,31 do
        for i=0,255-16 do
            -- NOTE that this fails, see BASESHT_0_NOT_IDENTITY:
--            assert(basesht[0][i] == i)
            local r, g, b = engine.getrgb(i)
            local f = 1
            r = ((32-f*sh+0.5)*r)/32
            g = ((32-f*sh+0.5)*g)/32
            b = ((32-f*sh+0.5)*b)/32
            r, g, b = max(0,r), max(0,g), max(0,b)  -- if f is > 1
            sht[sh][i] = engine.nearcolor(r, g, b)
        end

        for i=255-16+1,255 do
            -- fullbrights
            sht[sh][i] = basesht[0][i]
        end
    end

    return sht
end

local function create_base_shtab_2(basesht)
    local basesht = basesht or engine.getshadetab(0)

    local perm16 = { [0]=0,1, 2,3, 5,4, 6,7, 8,13, 10,11, 12,9, 14,15 }
    basesht = basesht:remap16(perm16)

    local iperm16 = {}
    for i=0,15 do
        iperm16[perm16[i]] = i
    end

    local iperm = {}
    for i=0,255 do
        iperm[i] = 16*(iperm16[floor(i/16)]) + i%16
    end

    local baseidx = {}
    for i=0,255-16 do
        baseidx[i] = i < 192 and 32*floor(i/32) or 16*floor(i/16)
    end

    local sht = engine.shadetab()

    for sh=0,31 do
        for i=0,255-16 do
            local bi = baseidx[i]
            local cidx = bi + floor(((31-sh)*(i - bi))/31)
            sht[sh][i] = iperm[cidx]
        end

        for i=255-16+1,255 do
            -- fullbrights
            sht[sh][i] = basesht[0][i]
        end
    end

    return sht:remap16(iperm16)
end

local ismapster32 = (gv.LUNATIC_CLIENT == gv.LUNATIC_CLIENT_MAPSTER32)

if (ismapster32) then
    -- Wrapper around engine.savePaletteDat() that errors on unexpected events.
    function shadexfog.save(filename, palnum, blendnum, moreblends, lognumalphatabs)
        local ok, errmsg, nummoreblends = engine.savePaletteDat(
            filename, palnum, blendnum, moreblends, lognumalphatabs)
        if (not ok) then
            error(errmsg)
        end

        printf('Wrote base palette, shade and translucency tables to "%s".', filename)
        if (nummoreblends > 0) then
            printf("  Also wrote %d additional translucency tables.", nummoreblends)
        end
    end

    function shadexfog.saveLookupDat(filename, lookups)
        local ok, errmsg, numlookups = engine.saveLookupDat(filename, lookups)
        if (not ok) then
            error(errmsg)
        end

        printf('Wrote %d lookup tables and 5 base palettes to "%s".',
               numlookups, filename)
    end
end

-- Create our (failed) version of the base shade table at set it to palookup
-- number <palnum>.
-- <secver>: use second attempt?
function shadexfog.create0(palnum, secver)
    local sht0 = secver and create_base_shtab_2() or create_base_shtab()
    engine.setshadetab(palnum, sht0)
end

function shadexfog.test_create0()
    local basesht = engine.getshadetab(0)

    for i=0,255 do
        if (basesht[0][i] ~= i) then
            -- BASESHT_0_NOT_IDENTITY
            printf("Base shade table at index %d: %d", i, basesht[0][i])
        end
    end

    local sht = create_base_shtab(basesht)

    local ok = true
    for sh=1,31 do
        for i=0,255 do
            local ouri, origi = sht[sh][i], basesht[sh][i]
--            if (sht[sh][i] ~= basesht[sh][i]) then
            if (math.abs(ouri - origi) > 1) then
                printf("Constructed shade table DIFFERS AT shade %d index %d: orig %d ours %d",
                       sh, i, basesht[sh][i], sht[sh][i])
                ok = false
                goto out
            end
        end
    end

    ::out::
    if (ok) then
        printf("Constructed shade table IDENTICAL WITH original one")
    end
end

---------- Blending table tests ----------

-- shadexfog.create_trans(startblendidx, func [, numtables [, fullbrightsOK]])
--
-- <func>: must be
--  rr, gg, bb = f(r,g,b, R,G,B, level, numtables)
--  If reverse translucency bit clear, (r,g,b) is background and (R,G,B) is
--  foreground (incoming).
--  ('level' is the table index, from 1 to <numtables>)
-- <numtables>: number of tables to create, from <startblendidx> on. Default: 1
function shadexfog.create_trans(startblendidx, func, numtables, fullbrightsOK)
    numtables = numtables or 1
    local lastokcol = fullbrightsOK and 255 or 255-16

    local tab = engine.blendtab()

    for level=1,numtables do
        for i=0,255 do
            local r,g,b = engine.getrgb(i)
            for j=0,255 do
                local R,G,B = engine.getrgb(j)

                local rr, gg, bb = func(r,g,b, R,G,B, level, numtables)
                tab[i][j] = engine.nearcolor(rr, gg, bb, lastokcol)
            end
        end

        engine.setblendtab(startblendidx + level-1, tab)
    end
end

local function check_numtables(numtables)
    if (numtables ~= nil) then
        if (type(numtables) ~= "number" or not (numtables >= 1 and numtables <= 128)) then
            error("invalid argument #2: must be a number in [1 .. 128]", 2)
        end

        if (bit.band(numtables, numtables-1) ~= 0) then
            error("invalid argument #2: must be a power of two", 2)
        end
    end
end

-- shadexfog.create_alpha_trans(startblendidx [, numtables [, fullbrightsOK]])
--
-- Creates <numtables> blending tables of smooth alpha translucency, with
-- fractions 1/(2*numtables), 2/(2*numtables) ... numtables/(2*numtables).
-- <numtables> must be a power of two in [1 .. 128].
function shadexfog.create_alpha_trans(startblendidx, numtables, fullbrightsOK)
    check_numtables(numtables)

    shadexfog.create_trans(
        startblendidx,

        function(r,g,b, R,G,B, alpha, numtabs)
            local f = alpha/(2*numtabs)
            local F = 1-f
            return f*r+F*R, f*g+F*G, f*b+F*B
        end,

        numtables, fullbrightsOK
    )
end

-- shadexfog.create_additive_trans(startblendidx [, numtables [, fullbrightsOK]])
function shadexfog.create_additive_trans(startblendidx, numtables, fullbrightsOK)
    shadexfog.create_trans(
        startblendidx,

        function(r,g,b, R,G,B, level, numtabs)
            local f = level/numtabs
            return min(f*r+R, 255), min(f*g+G, 255), min(f*b+B, 255)
        end,

        numtables, fullbrightsOK
    )
end

-- shadexfog.create_brightpass_trans(startblendidx [, numtables [, fullbrightsOK]])
function shadexfog.create_brightpass_trans(startblendidx, numtables, fullbrightsOK)
    shadexfog.create_trans(
        startblendidx,

        function(r,g,b, R,G,B, alpha, numtabs)
            local a = alpha/numtabs
            local F = 1 - min(a, (R+G+B) / (3*255))
            local f = 1 - F
            return f*r+F*R, f*g+F*G, f*b+F*B
        end,

        numtables, fullbrightsOK
    )
end

if (not ismapster32) then
    return shadexfog
end

--========== Mapster32 Lua menu hooks ==========--

local getnumber16 = engine.getnumber16
local GNF = engine.GETNUMFLAG
local GNF_BOOL = GNF.NEXTFREE

local df = GNF.RET_M1_ON_CANCEL  -- default getnumber16() flags

local MAXUSERPALOOKUP = 256-1-8  -- KEEPINSYNC engine.lua:check_palidx()

-- wrapped_func = CreateMenuFunction(argdesc)
--
-- <argdesc>: table with [0]=<func> and then, entries { name, init, max [, noret] }
local function CreateMenuFunction(argdesc)
    return function()
        local func = argdesc[0]
        assert(type(func) == "function")
        local args = {}

        for i=1,#argdesc do
            local ad = argdesc[i]
            assert(type(ad) == "table" and #ad == 3 or #ad == 4)

            local moreflags = ad[4] or 0
            args[i] = getnumber16(ad[1]..": ", ad[2], ad[3], bit.bor(df, moreflags))
            if (bit.band(moreflags, GNF.NEG_ALLOWED)==0 and args[i] < 0) then
                return
            end
            if (bit.band(moreflags, GNF_BOOL)~=0) then
                args[i] = (args[i] > 0)
            end
        end

        func(unpack(args))
    end
end

-- Replace chevrons (angle brackets!) with printext16 markup.
local function replchev(matchstr) return "^15"..matchstr:sub(2,-2).."^O" end
-- Replace ASCII code escapes like \XXX. We can't use these escapes in Lua [[ ... ]] strings.
local function replascii(matchstr) return string.char(tonumber(matchstr)) end
-- Format a whole string for the menu system:
local function formatHelp(str)
    return str:gsub(
        "(%b<>)", replchev):gsub(
        "%*(.-)%*", "^014%1^O"):gsub(
        "%\\(%d+)", replascii)
end

----------

engine.clearMenu()

engine.registerMenuFunc(
    "Create shadexfog palset",
    CreateMenuFunction{
        [0] = shadexfog.create,
        { "Starting palnum", 100, MAXUSERPALOOKUP-31 },
        { "Red fog color [0-255]", 0, 255 },
        { "Green fog color [0-255]", 0, 255 },
        { "Blue fog color [0-255]", 0, 255 },
    },

formatHelp
[[
<shadexfog.create(startpalnum, fogr, fogg, fogb)>
<_______________________________________________>

Creates 32 shade tables corresponding to different *shade levels*
of a fog palookup, together called a *shade-x-fog* palookup set.

 Pals <startpalnum> to <startpalnum>+31 will be taken.
 <fogr>, <fogg>, <fogb>: intensities of the fog color, [0 .. 255]
]]
)

engine.registerMenuFunc(
    "Translate map for shxfog",
    CreateMenuFunction{
        [0] = shadexfog.translate,
        { "Starting palnum", 100, MAXUSERPALOOKUP-31 },
        { "Fog intensity [0-31]", 0, 31 },
        { "Change all sectors' visibility to (Esc: don't)", 0, 255, GNF.NEG_ALLOWED },
    },

formatHelp
[[
<shadexfog.translate(startpalnum, fogintensity [, vis])>
<______________________________________________________>

Translates the whole map for use with a shade-x-fog palookup set.

 .pal becomes the <startpalnum> + former .shade
 .shade becomes the <fogintensity> [0 .. 31]

 If <vis> is passed and >= 0, set all sector's visibility to that
 value.

 *Notes:*
  - auto-detects when the translation has been applied with the *same*
    <startpalnum> (if a different one is desired, must reload map).
  - if shades > 31 or < 0 present, there is loss of information
]]
)

engine.registerMenuFunc(
    "Change pal of everything",
    CreateMenuFunction{
        [0] = shadexfog.challpal,
        { "Pal to change to", 0, MAXUSERPALOOKUP },
    },

formatHelp
[[
<shadexfog.challpal(palnum)>
<__________________________>

Changes the .pal member of all sector ceilings/floors, walls and
sprites to <palnum>.
]]
)

engine.registerMenuFunc(
    "Create alpha trans. tabs",
    CreateMenuFunction{
        [0] = shadexfog.create_alpha_trans,
        { "Starting blendnum", 1, 255 },
        { "Number of blending tables", 32, 255 },
        { "Fullbright result colors OK?", 0, 1, GNF_BOOL },
    },

formatHelp
[=[
<shadexfog.create_alpha_trans(startblend [, numtables [, fullbriOK]])>
<____________________________________________________________________>

Creates <numtables> blending tables of smooth alpha translucency,
starting with the blending number <startblend>, with values of alpha

  1/(2\255<numtables>), 2/(2\255<numtables>) ... <numtables>/(2\255<numtables>).

 <numtables> must be a power of two in [1 .. 128].

 <fullbriOK>: should fullbright color indices (>= 240) be permitted as
            the blending result of two color indices?
]=]
)

engine.registerMenuFunc(
    "Create addtv. trans. tabs",
    CreateMenuFunction{
        [0] = shadexfog.create_additive_trans,
        { "Starting blendnum", 1, 255 },
        { "Number of blending tables", 32, 255 },
        { "Fullbright result colors OK?", 0, 1, GNF_BOOL },
    },

formatHelp
[=[
<shadexfog.create_additive_trans(startbl [, numtables [, fullbriOK]])>
<____________________________________________________________________>

Creates <numtables> blending tables of smooth additive translucency,
starting with the blending number <startbl>, with factors for the
background color

  1/<numtables>, 2/<numtables> ... <numtables>/<numtables>.

 <numtables> must be a power of two in [1 .. 128].

 <fullbriOK>: should fullbright color indices (>= 240) be permitted as
            the blending result of two color indices?
]=]
)

engine.registerMenuFunc(
    "Create bri.pass tr. tabs",
    CreateMenuFunction{
        [0] = shadexfog.create_brightpass_trans,
        { "Starting blendnum", 1, 255 },
        { "Number of blending tables", 32, 255 },
        { "Fullbright result colors OK?", 0, 1, GNF_BOOL },
    },

formatHelp
[=[
<shadexfog.create_brightpass_trans(startbl [, numtabs [, fullbriOK]])>
<____________________________________________________________________>

Creates <numtabs> blending tables of "brightpass" translucency,
starting with the blending number <startbl>, with fractions

  1/<numtables>, 2/<numtables> ... <numtables>/<numtables>.

 <fullbriOK>: should fullbright color indices (>= 240) be permitted as
            the blending result of two color indices?
]=]
)

engine.registerMenuFunc(
    "Create base shade table",
    CreateMenuFunction{
        [0] = shadexfog.create0,
        { "Pal number", 100, MAXUSERPALOOKUP },
        { "Second attempt?", 1, 1, GNF_BOOL },
    },

formatHelp
[[
<shadexfog.create0(palnum, secver)>
<_________________________________>

Creates our version of the base shade table at set it to palookup
number <palnum>.

 <secver>: use second attempt instead of the first? This one is more
   similar to the base shade table shipped with Duke3D, but still
   shows significant differences.
]]
)

engine.registerMenuFunc(
    "Create c.index remapping",
    function()
        local palnum = getnumber16("Pal number: ", 100, MAXUSERPALOOKUP)
        if (palnum < 0) then return end

        local remaptab = {}
        while (true) do
            local srchex = getnumber16("Source hexadecatuple (0: finish): ", 0, 14)
            if (srchex < 0) then return end
            local dsthex = getnumber16("Destn. hexadecatuple (0: finish): ", 0, 14)
            if (dsthex < 0) then return end

            if (srchex == 0 and dsthex == 0) then
                break
            end

            remaptab[srchex] = dsthex
        end

        shadexfog.createremap(palnum, remaptab)
    end,

formatHelp
[[
<shadexfog.createremap(palnum, remaptab)>
<_______________________________________>

Creates a color index remapping expressed as mappings of sexdeca-
tuples (16-tuples) of the base palette at pal <palnum>.

 Duke3D's default base palette can be considered to consist of six
 ramps of 32 colors each, three ramps of 16 colors each and a
 remaining fullbright color set. The sexdecatuples are as follows:

<  0,  1>: gray ramp
<  2,  3>: skin color ramp
<  5,  4>: blue ramp (note that the 16-tuples are in reverse order)
<  6,  7>: nightvision yellow/green
<  8, 13>: red ramp
< 10, 11>: almost gray ramp, but with a slight red hue

    <  9>: yellow (slightly more red than green)
    < 12>: "dirty" orange
    < 14>: blue-purple-red
]]
)

local function getNumberRange(what, whating)
    local str = engine.getstring("Additional "..what.." numbers (e.g. '64,100-131,255'): ")
    if (str == nil) then return end
    if (str == "") then return {} end

    if (not str:find("^[%d,%-]+$")) then
        error("Additional "..whating.." numbers string must contain only digits or ',' or '-'", 2)
    end

    local moreblends = {}
    local didnumstr = {}

    for n1, n2 in str:gmatch("(%d+)%-(%d+)") do  -- parse number ranges
        moreblends[#moreblends+1] = { tonumber(n1), tonumber(n2) }
        didnumstr[n1] = true
        didnumstr[n2] = true
    end

    for n in str:gmatch("%d+") do  -- parse single numbers
        if (not didnumstr[n]) then
            moreblends[#moreblends+1] = tonumber(n)
        end
    end

    return moreblends
end

engine.registerMenuFunc(
    "Save pal+sh+trans DAT f.",
    function()
        local filename = engine.getstring("File name: ")
        if (filename == nil) then return end

        local palnum = getnumber16("Pal number of base shade table: ", 0, MAXUSERPALOOKUP)
        if (palnum < 0) then return end
        local blendnum = getnumber16("Blendnum of base transluc. table: ", 0, 255)
        if (blendnum < 0) then return end

        local moreblends = getNumberRange("blend", "blending")
        if (moreblends == nil) then return end

        local lognumalphatabs
        if (#moreblends > 0) then
            lognumalphatabs = getnumber16("log2 of last alpha blending table index (1-7, 0: none): ", 0, 7)
            if (lognumalphatabs < 0) then return end
            if (lognumalphatabs == 0) then lognumalphatabs = nil end
        end

        shadexfog.save(filename, palnum, blendnum, moreblends, lognumalphatabs)
    end,

formatHelp
[[
<shadexfog.save(filename, palnum, blendnum, moreblends, lognumalpha)>
<___________________________________________________________________>

Writes out a full PALETTE.DAT-formatted file named <filename> with the
base shade table numbered <palnum> and the base translucency table
numbered <blendnum>.

Finally, you are asked to specify additional blending tables that can
be stored in EDuke32's extended PALETTE.DAT format. If one or more
additional blending table is specified, you are also queried for the
log2 of the last alpha blending table index, <lognumalpha>. Since alpha
blending tables are assumed to be set up at indices 1 to
exp(2, <lognumalpha>), it is also the log2 of their total count.
]]
)

engine.registerMenuFunc(
    "Save lookups DAT file",
    function()
        local filename = engine.getstring("File name: ")
        if (filename ~= nil and filename ~= "") then
            local lookups = {
                -- Duke3D 1.5 LOOKUP.DAT order
                1,2,6,7,8, 3,4,5,9,10,
                12,13,15,16,18, 19,11,14,17,20,
                21,22,23,24,25
            }

            local morelookups = getNumberRange("lookup", "lookup")
            if (morelookups == nil) then return end

            if (#morelookups > 0) then
                for i=1,#morelookups do
                    lookups[#lookups+1] = morelookups[i]
                end
            end

            shadexfog.saveLookupDat(filename, lookups)
        end
    end,

formatHelp
[[
<shadexfog.saveLookupDat(filename, lookups)>
<__________________________________________>

Saves the color index lookups (i.e. first 256 values of each shade
table) of the pal numbers which have lookups in Duke3D's unaltered
LOOKUP.DAT, plus optional ones provided by the user.

The default ones are, in this order:
1,2,6,7,8, 3,4,5,9,10, 12,13,15,16,18, 19,11,14,17,20, 21,22,23,24,25.
(All pal numbers from 1 to 25 are present.)

 <filename>: the name of the LOOKUP.DAT-formatted file to create
]]
)

engine.registerMenuFunc("_________DEBUG_________", function() end
--[[
,
" \01\02\03\04\05\06\07\08\09\10\11\12\13\14\15\
\16\17\18\19\20\21\22\23\24\25\26\27\28\29\30\31\
\128\129\130\131\132\133\134\135\136\137\138\139\140\141\142\143\
\144\145\146\147\148\149\150\151\152\153\154\155\156\157\158\159\
\160\161\162\163\164\165\166\167\168\169\170\171\172\173\174\175\
\176\177\178\179\180\181\182\183\184\185\186\187\188\189\190\191\
\192\193\194\195\196\197\198\199\200\201\202\203\204\205\206\207\
\208\209\210\211\212\213\214\215\216\217\218\219\220\221\222\223\
\224\225\226\227\228\229\230\231\232\233\234\235\236\237\238\239\
\240\241\242\243\244\245\246\247\248\249\250\251\252\253\254\255"
--]]
)

engine.registerMenuFunc("Setup dbg. water basepal", engine.setupDebugBasePal,
formatHelp
[[
<engine.setupDebugBasePal()>
<__________________________>

Overwrites the water base palette with one where each 16-tuple
(except the fullbrights) consists of a single representative
color.

This can be used to get a quick glimpse about what ramps are present
in particular tiles. With this information, custom lookups can be
created more directedly with the <Create c.index remapping> menu
entry.
]]
)

engine.registerMenuFunc(
    "Create depth shade tab",
    CreateMenuFunction{
        [0] = shadexfog.create_depth_shtab,
        { "Pal number", 100, MAXUSERPALOOKUP },
    },

formatHelp
[[
<shadexfog.create_depth_shtab(palnum)>
<____________________________________>

Creates a shade table for debugging purposes at pal <palnum>.

 For every color index, the shade table maps shade index <i> to
 color index <i> (of the first ramp of gray colors, assuming Build
 has loaded a base shade table with 32 shade levels).
]]
)

engine.registerMenuFunc(
    "Create vismarker sh. tab",
    CreateMenuFunction{
        [0] = shadexfog.create_vismarker_shtab,
        { "Pal number", 100, MAXUSERPALOOKUP },
    },

formatHelp
[[
<shadexfog.create_vismarker_shtab(palnum)>
<________________________________________>

Creates a shade table for debugging purposes at pal <palnum>.

 For every color index, the shade table maps shade index 1 to
 a ^14bright yellow^O color and shade index 30 to a ^13purple^O color.
 Thus, it can be useful in visualizing the limits of the
 fog/visibility attenuation.
]]
)

engine.registerMenuFunc("Linearize default basep.", engine.linearizeBasePal,
formatHelp
[[
<engine.linearizeBasePal()>
<_________________________>

Overwrites the default base palette with one where certain ramps have
their attenuation linearized. This is mainly useful for debugging
purposes as it excludes the effect of this nonlinearity for
comparison of fog/visibility between classic and OpenGL modes.
]]
)

do
    return shadexfog
end


code:
// Timing test for four 0..MAXSPRITES-1 access loops,
// fwd->back->fwd->back.
// Results with a LTO=1 RELEASE=1 C-CON build on x86: 42 ms for N=10.
// (Compare with Lunatic results in test.elua)

define N 10
gamevar n 0 0
gamevar i 0 0
gamevar t 0 0
gamevar t2 0 0
define MAXSPRITES 16384
define MAXSPRM1 16383  // MAXSPRITES-1

onevent EVENT_ENTERLEVEL
    getticks t

    setvar n 0
    whilevarn n N
    {
        setvar i 0
        whilevarvarn i MAXSPRITES
        {
            setactor[i].blend 1
            addvar i 1
        }

        setvar i MAXSPRM1
        whilevarvarn i -1
        {
            setactor[i].shade 1
            subvar i 1
        }

        setvar i 0
        whilevarvarn i MAXSPRITES
        {
            setactor[i].xoffset 0
            addvar i 1
        }

        setvar i MAXSPRM1
        whilevarvarn i -1
        {
            setactor[i].yoffset 0
            subvar i 1
        }

        addvar n 1
    }

    getticks t2
    subvarvar t2 t

    // qsprintf test
    redefinequote 116 0..MAXSPITES-1
    redefinequote 117 in total

    redefinequote 114 %d x four %s iterations took %d ms %s
    qsprintf 115 /*<-*/ 114 /*args:*/ n 116 t2 117

    echo 115
endevent

////////// Periodically alpha-faded liztroop //////////

gamevar alpha 0 0
gamevar tmp 0 0

onevent EVENT_GAME  // XXX: better: in EVENT_EGS + EVENT_LOADACTOR
    ifactor LIZTROOP
    {
        getactor[THISACTOR].mdflags tmp
        orvar tmp 16
        setactor[THISACTOR].mdflags tmp
    }
endevent

define NUMALPHATABS 128
define C1 0  //must be log2(128/NUMALPHATABS)
define C2 257  // must be 2*NUMALPHATABS+1

define ALPHA_TABS_BY_EDUKE32 1  // assume alpha->blend handling by EDuke32

onevent EVENT_ANIMATESPRITES
    setvarvar tmp totalclock
    shiftvarl tmp 2

    sin alpha tmp   // alpha is now in [-2^14 .. 2^14]
    shiftvarr alpha 7  // [-2^7 .. 2^7]
    addvar alpha 128  // [0 .. 256]
    shiftvarr alpha C1  // [0 .. 2*NUMALPHATABS]

    ifvarn ALPHA_TABS_BY_EDUKE32 0
    {
        subvar alpha 1
        ifvarl alpha 0 setvar alpha 0
        setactor[THISACTOR].alpha alpha

        break
    }

    ifvare alpha 0
    {
        // clear translucent bits, 0xfdfe == 0xffff-(1+512)
        gettspr[THISACTOR].tsprcstat tmp
        andvar tmp 0xfdfe
        settspr[THISACTOR].tsprcstat tmp
    }
    else
    {
        gettspr[THISACTOR].tsprcstat tmp
        orvar tmp 2
        settspr[THISACTOR].tsprcstat tmp

        // Assume blending tables [1 .. NUMALPHATABS] are installed, like
        // generated by shadexfog.lua:create_alpha_trans(1, NUMALPHATABS).
        ifvarg alpha NUMALPHATABS
        {
            setvarvar tmp C2
            subvarvar tmp alpha
            setvarvar alpha tmp

            // Set "reverse translucent" cstat bit
            gettspr[THISACTOR].tsprcstat tmp
            orvar tmp 512
            settspr[THISACTOR].tsprcstat tmp
        }
        else
        {
            // Clear cstat bit 512
            gettspr[THISACTOR].tsprcstat tmp
            andvar tmp 0xfdff
            settspr[THISACTOR].tsprcstat tmp
        }

        settspr[THISACTOR].tsprblend alpha
    }
endevent


code:
gamevar tmp 0 0
gamevar cs 0 0

state setup_animatesprites
    getactor[THISACTOR].mdflags tmp
    orvar tmp 16
    setactor[THISACTOR].mdflags tmp
ends

onevent EVENT_EGS
    state setup_animatesprites
endevent

onevent EVENT_LOADACTOR
    state setup_animatesprites
endevent

// Assuming a PALETTE.DAT with e.g.:
//  blend 0: 50/50 alpha
//  blend 1: factor 1.0 additive
//
// See discussion starting from
// http://forums.duke4.net/topic/775-eduke32-20-and-polymer/page__view__findpost__p__213408
onevent EVENT_ANIMATESPRITES
    getactor[THISACTOR].cstat cs
    ifvarand cs 2 ifvarand cs 512
        setactor[THISACTOR].blend 1
    else
        setactor[THISACTOR].blend 0
endevent


code:
gamevar j 0 0
gamevar k 0 0

definequote 400 QWE

definequote 500 ===

onevent EVENT_ENTERLEVEL
    switch j
      case 0
      case -1
        userquote 29
      break
      case 1
        userquote 30
      break
      default
        userquote 31
      break
      case 0
        redefinequote 400 ASD
        userquote 400
      break
    endswitch

    addvar j 1

    switch j
      case 0
      case -2
        userquote 29
      break
      case 3
        userquote 30
      break
      default
        userquote 31
      break
    endswitch

    // test "-" codegen (must not generate comment!).
    subvar j -3

    // result:
    // ASD  (from second case 0)
    // BRIGHTNESS LEVEL: THREE  (from default)

    ////////// test nested switch/while //////////

    ifvare 0 1
        break  // outer level: "do return end"

    setvar j 0
    whilevarn j 3
    {
        addvar j 1
        userquote 500
        break  // while is inner: "continue"
        userquote 400
    }  // 3x "===", no quote 400

    setvar j 0
    whilevarn j 1
    {
        switch j
          case 0
            addvar j 1
            // switch is inner: "do return end" (likewise with following
            // "break" commands):
            break
            addvar j 1
          break
          case 1
            nullop
          break
          default
            nullop
          break
        endswitch
    }

    // NOTE: this is syntactically invalid, though may be useful:
    //  case X:
    //  default:
    //   <code...>

    setvar j 0
    switch j
      case 0,
        whilevarn j 1
        {
            addvar j 1
            break  // while is inner: "continue"
        }
      break
    endswitch
/*
    // LunaCON only: nested switches.
    // Untested, but the generated code looks sane.
    switch j
      case 0,
        switch k
          default
            userquote 29  // BRIGHTNESS LEVEL: ONE
        endswitch
      case 1,
        userquote 30
    endswitch
*/

    // nested whilevar*n
    setvar j -1
    setvar k -1
    whilevarn j 0
    {
        whilevarn k 0
        {
            setvar k 0
            break
            setvar k 1
        }

        setvar j 0
        break
        setvar j -1
    }
endevent


code:
#!/usr/bin/env luajit

local ffi = require "ffi"
local io = require "io"
local os = require "os"
local math = require "math"

local tablesfn = arg[1]
local taylor_n = tonumber(arg[2])

if (tablesfn==nil) then
    -- taylor_n is a number: use that n for Taylor approximation of atan
    -- taylor_n==nil: use math.atan
    print("Usage: ./tables.lua ../path/to/TABLES.DAT [talyor_n]")
    return 1
end

local fd, errmsg = io.open(tablesfn)
if (fd==nil) then
    print("Failed opening "..tablesfn..": "..errmsg)
    return 1  -- XXX: not working, neither os.exit(1) as of 20120901
end

local sintab = ffi.new("int16_t [?]", 2048)
local sinstr = fd:read(2048*2)
if (sinstr==nil or #sinstr<2048*2) then
    print("Failed reading whole sine table")
    fd:close()
    return 1
end

ffi.copy(sintab, sinstr, 2048*2)
sinstr = nil

--== sintable

local oursin = ffi.new("int16_t [?]", 2048)
local BANG2RAD = math.pi/1024
for a=0,2047 do
    local s = 16384*math.sin(a*BANG2RAD)
    oursin[a] = s

--    if (a < 1024) then print(a..": "..s) end
end
--print("")

for a=0,2047 do
    local dif = oursin[a]-sintab[a]
    assert(dif == 0)
--[[
    if (dif ~= 0) then
        print(a..": "..dif)
    end
--]]
    if (a < 512) then
        assert(sintab[a] == sintab[1024-a])
    end

    if (a >= 1024) then
        assert(sintab[a] == -sintab[a-1024])
    end
end

--== radarang

local ratab = ffi.new("int16_t [?]", 640)
local rastr = fd:read(640*2)
fd:close()

ffi.copy(ratab, rastr, 640*2)
rastr = nil

local function atan_taylor(x, n)
    local absx = math.abs(x)

    local at = 0
    for i=0,n do
        local twoip2 = 2*i+1
        local xp = x^twoip2
        local pmone = (-1)^i

        if (absx < 1) then
            at = at + (pmone * xp)/twoip2
        else
            at = at + pmone/(xp * twoip2)
        end
    end

    if (absx > 1) then
        at = math.pi/2 - at
    end

    return at
end

local atan
if (taylor_n==nil) then
    atan = math.atan
else
    atan = function (x) return atan_taylor(x, taylor_n) end
end

for i=0,639 do
    local at = -64*atan((640-0.5-i)/160)/BANG2RAD
    local ra = ratab[i]

    at = math.ceil(at)

--    print(i ..": ".. ra .." ".. at .." ".. at/ra .." ".. at-ra)
end

--[[
local ourra = ffi.new("int16_t [?]", 640)
--]]


code:
#!/usr/bin/env luajit

-- Usage: luajit bittest.lua <number or "x"> [-ffi] [-bchk]

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

local bitar = require "bitar"

local print = print
local tonumber = tonumber

local getticks

if (string.dump) then
    -- stand-alone
    local os = require "os"
    function getticks()
        return os.clock()*1000
    end
else
    -- embedded
    getticks = gv.gethiticks

    module(...)
end

-- based on example from http://bitop.luajit.org/api.html

local m = string.dump and tonumber(arg[1]) or 1e7
local maxidx = math.floor(m/32)

local ffiar_p, boundchk_p = false, false

if (string.dump) then
    if (arg[2]=="-ffi" or arg[3]=="-ffi") then
        ffiar_p = true
    end

    if (arg[2]=="-bchk" or arg[3]=="-bchk") then
        boundchk_p = true
    end
end

function sieve()
    local count = 0
    local p = {}

    if (ffiar_p) then
        -- stand-alone using unchecked int32_t array: on x86_64 approx. 80 ms
        -- for m = 1e7 (enabling bound checking makes it be around 100 ms)
        local ffi = require "ffi"
        local pp = ffi.new("int32_t [?]", maxidx + 1)

        p = pp

        if (boundchk_p) then
            local mt = {
                __index = function(tab,idx)
                    if (idx >= 0 and idx <= maxidx) then
                        return pp[idx]
                    end
                end,

                __newindex = function(tab,idx,val)
                    if (idx >= 0 and idx <= maxidx) then
                        pp[idx] = val
                    end
                end,
            }

            p = setmetatable({}, mt)
        end

        for i=0,maxidx do p[i] = -1; end
    else
        p = bitar.new(m, 1)
    end

    local t = getticks()

    if (ffiar_p) then
        local bit = require "bit"

        for i=2,m do
            if (bit.band(p[bit.rshift(i, 5)], bit.lshift(1, i)) ~= 0) then
                count = count + 1
                for j=i+i,m,i do
                    local jx = bit.rshift(j, 5)
                    p[jx] = bit.band(p[jx], bit.rol(0xfffffffe, j));
                end
            end
        end
    else
        for i=2,m do
            if (p:isset(i)) then
                count = count + 1
                for j=i+i,m,i do p:set0(j); end
            end
        end
    end

    -- When using bitar module: x86_64: approx. 110 ms
    print(string.format("[%s] Found %d primes up to %d (%.02f ms)",
                        ffiar_p and "ffi-ar"..(boundchk_p and ", bchk" or "") or "tab-ar",
                        count, m, getticks()-t))

    return p, count
end

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

    local p, count = sieve()
    local t = getticks()

    if (ffiar_p) then
        return
    end

    -- test serialization
    local ser = tostring(p)
    local maxbidx_str = string.match(ser, '%(([0-9]+),')
    local p2 = bitar.new(tonumber(maxbidx_str), string.match(ser, "'(.*)'"))
    printf("serialization + new: %.02f ms", tostring(getticks()-t))

    assert(p==p2)
    if (m >= 2) then
        assert(#p == count+2)  -- +2 is because 0 and 1 are set even though they're not primes
    end

    if (not ffiar_p) then
        math.randomseed(os.time())
        local maxbidx = math.random(0, 65536)
        local p3 = bitar.new(maxbidx, 1)
        assert(#p3 == maxbidx+1)  -- bits 0 to maxbidx inclusive are set
    end
--[[
    print(p)
    print(p-p)  -- test set difference
    print(-p)
--]]

    -- Set difference of self with self is the same as set intersection of self
    -- with complement of self:
    assert(p-p == p*(-p))
end


code:
#!/usr/bin/env luajit

local ffi = require "ffi"
local math = require "math"
local os = require "os"

local xmath = require "xmath"

local ldist = xmath.ldist
local sqrt = math.sqrt

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

local function edist(p1, p2)
    return sqrt(p1.x*p1.x + p2.x*p2.x)
end

-- z dummy is so that there's no error with xmath.rotate()
local vec2 = ffi.typeof("struct { int32_t x, y, z /* dummy */; }")

local numpoints = tonumber(arg[1]) or 1e4
local Nsq = numpoints*numpoints
printf("number of points: %d, testing %d distances", numpoints, Nsq)

local B = 8192

local pts = {}
for i=1,numpoints do
    pts[i] = vec2(math.random(-B, B), math.random(B, -B))
end

-- test edist
local t = os.clock()

local sum = 0
for i=1,numpoints do
    for j=1,numpoints do
        sum = sum+edist(pts[i], pts[j])
    end
end

t = os.clock()-t
printf("edist: %.03fns per call, mean=%.03f", (1e9*t)/Nsq, sum/Nsq)

-- test ldist
t = os.clock()

local sum = 0
for i=1,numpoints do
    for j=1,numpoints do
        sum = sum+ldist(pts[i], pts[j])
    end
end

t = os.clock()-t
printf("ldist: %.03fns per call, mean=%.03f", (1e9*t)/Nsq, sum/Nsq)

-- test rotation
t = os.clock()

-- from control.lua (the CON version of rotatepoint)
local function _rotatepoint(pivotx, pivoty, posx, posy, ang)
    local pos = xmath.ivec3(posx, posy)
    local pivot = xmath.ivec3(pivotx, pivoty)
    pos = xmath.rotate(pos, ang, pivot):toivec3()
    return pos.x, pos.y
end

sum = 0
for i=1,numpoints do
    for j=1,numpoints do
--        local p = xmath.rotate(pts[i], j, pts[j])
--        sum = sum+p.x
        sum = sum + _rotatepoint(pts[j].x, pts[j].y, pts[i].x, pts[i].y, j)
    end
end

t = os.clock()-t

printf("rotate: %.03fns per call", (1e9)/Nsq)

-- Results (helixhorned x86, x86_64)
-- number of points: 10000, testing 100000000 distances
-- edist: 6.300ns per call, mean=6286.597
-- ldist: 17.600ns per call, mean=8692.612
-- rotate: 10.000ns per call  [even with _rotatepoint()!]


code:
#!/usr/bin/env luajit

local os = require("os")

local xmath = require("xmath")

-- XXX: perf regression? (See below PERF_REGRESSION)
-- No, happens only with Clang build. (Why?)
local N = os.exit and (arg[1] and tostring(arg[1])) or 1e5  --1e6

local A,B = {}, {}
local V,W = {}, {}

local randvec

local args = { ... }
local ourname = args[1]

if (os.exit) then
    local math = require("math")

    randvec = function()
        return xmath.vec2(math.random(), math.random())
    end

    print("Running stand-alone. ourname: "..tostring(ourname))
else
    local randgen = require("randgen")
    local s = randgen.new(true)

    -- NOTE: factoring out the inner s:getdbl() into a separate function
    -- reduces performance seriously (about an order of magnitude!)
    randvec = function()
        return xmath.vec2(s:getdbl(), s:getdbl())
    end

    -- Test optional arguments from our_require().
    printf("Running %s embedded with opt arg %s", ourname, tostring(args[2]))
end

local t1 = os.clock()

if (os.exit == nil) then
    local randgen = require("randgen")
    local r = randgen.new(true)

    for i=1,4*2*N do
        -- This is to test the performance compared to a direct
        -- ffiC.rand_jkiss_dbl() call in randgen.lua
        r:getdbl()
    end
end

local t2 = os.clock()

-- init random points and vectors
for i=1,N do
    A[i] = randvec()
    B[i] = randvec()
    V[i] = randvec()
    W[i] = randvec()
end

local t3 = os.clock()

local v = xmath.vec2(0, 0)
for i=1,N do
    local intersp = xmath.intersect(A[i],V[i], B[i],W[i], true)
    if (intersp ~= nil) then
        v = v + intersp
    end
end

local t4 = os.clock()

-- x86_64 (embedded): approx. 200 ms (vs. the 100 ms of direct
-- ffiC.rand_jkiss_dbl()):
-- x86: 170 ms
print("getdbl:    ".. 1000*(t2-t1))
print("genpoints: ".. 1000*(t3-t2))  -- x86_64: 500 ms, x86: 700 ms
print("intersect: ".. 1000*(t4-t3))  -- x86_64, x86: about 35 ms  <- thanks to allocation sinking (else, about 500 ms?)
print("result:    ".. tostring(v))

-- PERF_REGRESSION: with N==1e6 getdbl, genpoints now about 1000ms from EDuke32!


code:
local require = require
local con = require("con")
local bit = require("bit")
local math = require("math")

local printf = printf
local tostring = tostring
local rs = con.rotatesprite

local gameevent = gameevent

local gv = gv
local player = player

module(...) --====================

test_gamevar = 123
local a_local_gamevar = "yes, this one too"

test_gamevar2 = 'qwe'

a_table = { ELT1='ELT1!', ELT2=4444, ATAB={ q=333, [4]="!four!", w=444, ["true"]=false } }
local ref_to_a_table = a_table
ref_to_tabtab = ref_to_a_table.ATAB
local l_ref_to_tabtab = ref_to_tabtab

ref_to_a_table.selfref = a_table
a_table[false] = { 1,2,3,con.actorvar(512) }

require "end_gamevars" --==========

not_a_gamevar = "no"

printf("a_table.ATAB[4]=%s", tostring(a_table.ATAB[4]))
a_table.ATAB[4] = "!FOUR!"

local DOT1x5 = 3135
local BAR1x5 = 3163

local function draw_hline_dotted(x1, x2, y, pal,stat)
    for x=x1,x2,2 do
        local pl = player[0]
        local tile = (x==x1 or x==x2) and BAR1x5 or DOT1x5

        if (pl.curr_weapon==2) then
            x = x + 16*math.sin(2*math.pi*gv.totalclock/120)
        elseif (pl.curr_weapon==1) then
            x = x + (pl.ang - 1024)/100
        end

        rs(x,y, 65536, 0, tile, 0,pal,stat,0, 0,0,gv.xdim-1,gv.ydim-1)
    end
end

local function draw_two_hlines(y, stat)
    if (player[0].curr_weapon ~= 3) then
        stat = bit.bor(stat, 8)
    end
    draw_hline_dotted(0,160, y, 0, stat)
    -- XXX: rotatesprite(*, 320, ...) with stat 1024 and classic draws out of
    -- bounds (wraps to lower left corner).  (Or is it the left one? In any case,
    -- weird stuff happens.)
    draw_hline_dotted(162,318, y, 2, stat)
end

local function rotatesprite_test()
    local stats = { [0]=0, 256, 512, 1024 }
    for i=0,3 do
        local stat = stats[i]
        draw_two_hlines(50+25*i, stats[i])
    end

    -- NUKEBUTTON
    rs(30,170, 32768, 2047*((gv.totalclock/240)%1), 142, 0,0,8+1024,0, 0,0,gv.xdim-1,gv.ydim-1)
end

gameevent{gv.EVENT_DISPLAYREST, rotatesprite_test}


code:
// This CON module provides 'state thisactor_getzrange', reimplementing
// VM_GetZRange() from the C source. This routine obtains actor[].ceilingz and
// actor[].floorz for the current actor (THISACTOR), using getzrange() with the
// initial z position displaced by -256.
//
// Original author: Helixhorned.

// In
gamevar tagz_x 0 0
gamevar tagz_y 0 0
gamevar tagz_z 0 0
gamevar tagz_sectnum 0 0

// Temp
gamevar tagz_cstat 0 0

// Out
gamevar tagz_ceilz 0 0
gamevar tagz_ceilhit 0 0
gamevar tagz_florz 0 0
gamevar tagz_florhit 0 0

// ZOFFSET in the EDuke32 source.
define TAGZ_ZOFFSET 256
// In the EDuke32 source, the <walldist> passed to VM_GetZRange().
define TAGZ_WALLDIST 127

// Updates the current actor's actor[].floorz and actor[].ceilngz members in
// the same way A_GetZRange() does.
state thisactor_getzrange
    setvarvar tagz_x sprite[THISACTOR].x
    setvarvar tagz_y sprite[THISACTOR].y
    setvarvar tagz_z sprite[THISACTOR].z, subvar tagz_z TAGZ_ZOFFSET
    setvarvar tagz_sectnum sprite[THISACTOR].sectnum

    // Back up and clear cstat.
    setvarvar tagz_cstat sprite[THISACTOR].cstat
    setactor[THISACTOR].cstat 0

    getzrange tagz_x tagz_y tagz_z tagz_sectnum
     /*out:*/ tagz_ceilz tagz_ceilhit tagz_florz tagz_florhit
      /*in:*/ TAGZ_WALLDIST CLIPMASK0

    // Restore cstat.
    setactor[THISACTOR].cstat tagz_cstat

    // Set actor[] members for the current actor.
    setactor[THISACTOR].htceilingz tagz_ceilz
    setactor[THISACTOR].htfloorz tagz_florz
ends


code:
onevent EVENT_DRAW3DSCREEN
    rotatesprite  40  80 131072 0 2523 0 0 330 windowx1 windowy1 windowx2 windowy2
    rotatesprite 280  80 131072 0 2523 0 0 522 windowx1 windowy1 windowx2 windowy2
endevent


code:
gamevar snd 351 0  // thunder sound
gamevar tmp 0 0
gamevar tmp2 0 0
gamevar shoots 2605 2  // RPG

gamevar WEAPON1_SHOOTS 2605 0

define MORTER2 1653
// NOTE: the custom MORTER2 doesn't spawn FRAMEEFFECT like the MORTER, which
// has some hard-wired code running.
defineprojectile MORTER2 PROJ_WORKSLIKE 6150
defineprojectile MORTER2 PROJ_SPAWNS EXPLOSION2
defineprojectile MORTER2 PROJ_SOUND RPG_SHOOT
defineprojectile MORTER2 PROJ_VEL 600
defineprojectile MORTER2 PROJ_EXTRA 165
defineprojectile MORTER2 PROJ_EXTRA_RAND 10
defineprojectile MORTER2 PROJ_DROP -200  // tested in test.elua
defineprojectile MORTER2 PROJ_ISOUND PIPEBOMB_EXPLODE
defineprojectile MORTER2 PROJ_HITRADIUS 2800
defineprojectile MORTER2 PROJ_BOUNCES 4
defineprojectile MORTER2 PROJ_OFFSET 128
defineprojectile MORTER2 PROJ_CLIPDIST 24
defineprojectile MORTER2 PROJ_TRAIL -1  // overridden in test.elua
defineprojectile MORTER2 PROJ_TNUM 6
defineprojectile MORTER2 PROJ_USERDATA 32  // test setting initial value

onevent EVENT_GAME
    setvarvar WEAPON1_FIRESOUND snd
//    setvarvar WEAPON1_SHOOTS shoots
endevent

definequote 400 Failed setting projectile userdata

useractor notenemy 909  // tree trunk
    ifcount 120
    {
        zshoot -4096 BOUNCEMINE

        // userdata: bitwise-OR in 64
        getprojectile[MORTER2].userdata tmp
        orvar tmp 64
        setprojectile[MORTER2].userdata tmp

        ezshoot -4096 MORTER2
        getthisprojectile[RETURN].vel tmp
        randvar tmp2 500
        subvarvar tmp tmp2
        setthisprojectile[RETURN].vel tmp
        resetcount

        // userdata: must be 32+64 now
        getthisprojectile[RETURN].userdata tmp
        ifvarn tmp 96
            quote 400
    }
enda

onevent EVENT_JUMP
    tossweapon
endevent


code:
#!/usr/bin/env luajit

if (arg[1]==nil) then
    print("Usage: "..arg[0].." ../path/to/*.ART")
    return 1
end

B = require "build"

tile = B.loadarts(arg)

for i=0,B.MAX.TILES-1 do
    if (tile.sizy[i] > 256) then
        print(i..": "..tile.sizy[i])
    end
end


code:
-- Loaders for various BUILD structures for LuaJIT

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

local error = error
local assert = assert
local pairs = pairs
local print = print
local setmetatable = setmetatable
local tostring = tostring
local tonumber = tonumber

module(...)

local STRUCTDEF = {
    sector = [[
    int16_t wallptr, wallnum;
    int32_t ceilingz, floorz;
    uint16_t ceilingstat, floorstat;
    int16_t ceilingpicnum, ceilingheinum;
    int8_t ceilingshade;
    uint8_t ceilingpal, ceilingxpanning, ceilingypanning;
    int16_t floorpicnum, floorheinum;
    int8_t floorshade;
    uint8_t floorpal, floorxpanning, floorypanning;
    uint8_t visibility, fogpal;
    int16_t lotag, hitag, extra;
]],

    wall = [[
    int32_t x, y;
    int16_t point2, nextwall, nextsector;
    uint16_t cstat;
    int16_t picnum, overpicnum;
    int8_t shade;
    uint8_t pal, xrepeat, yrepeat, xpanning, ypanning;
    int16_t lotag, hitag, extra;
]],

    sprite = [[
    int32_t x, y, z;
    uint16_t cstat;
    int16_t picnum;
    int8_t shade;
    uint8_t pal, clipdist, blend;
    uint8_t xrepeat, yrepeat;
    int8_t xoffset, yoffset;
    int16_t sectnum, statnum;
    int16_t ang, owner, xvel, yvel, zvel;
    int16_t lotag, hitag, extra;
]],
}

ffi.cdef([[
typedef struct
{
]]..STRUCTDEF.sector..[[
} sectortype;

typedef struct
{
]]..STRUCTDEF.wall..[[
} walltype;

typedef struct
{
]]..STRUCTDEF.sprite..[[
} spritetype;
]])

ffi.cdef[[
size_t fread(void *ptr, size_t size, size_t nmemb, void *stream);
]]

local C = ffi.C

-- [<sector/wall/sprite>].<membername> = true
local is_member_tab = {
    sector = {},
    wall = {},
    sprite = {},
}

do
    for what, sdef in pairs(STRUCTDEF) do
        for membname in string.gmatch(sdef, "([a-z0-9_]+)[,;]") do
            is_member_tab[what][membname] = true
        end
    end
end

function ismember(what, membname)
    return (is_member_tab[what][membname] ~= nil)
end

MAX =
{
    SECTORS = { [7]=1024, [8]=4096, [9]=4096 },
    WALLS = { [7]=8192, [8]=16384, [9]=16384 },
    SPRITES = { [7]=4096, [8]=16384, [9]=16384 },

    TILES = 30720,
}
local MAX = MAX

-- <dontclose>: if true, don't close file on error
local function doread(fh, basectype, numelts, dontclose)
    assert(numelts > 0)
    local typ = ffi.typeof("$ [?]", ffi.typeof(basectype))
    local cd = ffi.new(typ, numelts)

    if (C.fread(cd, ffi.sizeof(basectype), numelts, fh) ~= numelts) then
        if (not dontclose) then
            fh:close()
        end
        return nil, "Failed reading"
    end

    return cd
end

-- Read base palette (i.e. first 768 bytes as R,G,B triplets) from a PALETTE.DAT.
-- <noquad>: if true, don't multiply components by 4
-- Returns:
--  on success: <uint8_t [768]> cdata (palette values scaled by 4)
--  on failure: nil, <errmsg>
function read_basepal(filename, noquad)
    local fh, errmsg = io.open(filename, "rb")
    if (fh == nil) then
        return nil, errmsg
    end

    local palette, errmsg = doread(fh, "uint8_t", 768, true)
    fh:close()

    local f = noquad and 1 or 4

    for i=0,768-1 do
        palette[i] = f*palette[i]
    end

    return palette, errmsg
end

local function set_secwalspr_mt(structar, maxidx)
    local mt = {
        __index = function(tab, idx)
            if (not (idx >= 0 and idx < maxidx)) then
                error("Invalid structure array read access", 2)
            end
            return structar[idx]
        end,

        __newindex = function(tab, idx, newval)
            error('cannot write directly to structure array', 2)
        end,
    }

    return setmetatable({}, mt)
end

local function get_numyaxbunches(map)
    if (map.version < 9) then
        return 0
    end

    local numbunches = 0
    local sectsperbunch = { [0]={}, [1]={} }

    for i=0,map.numsectors-1 do
        for cf=0,1 do
            local sec = map.sector[i]
            local stat = (cf==0) and sec.ceilingstat or sec.floorstat
            local xpan = (cf==0) and sec.ceilingxpanning or sec.floorxpanning

            if (bit.band(stat, 1024) ~= 0) then
                if (xpan+1 > numbunches) then
                    numbunches = xpan+1
                end

                if (sectsperbunch[cf][xpan]==nil) then
                    sectsperbunch[cf][xpan] = 1
                else
                    sectsperbunch[cf][xpan] = sectsperbunch[cf][xpan]+1
                end
            end
        end
    end

    map.numbunches = numbunches
    map.sectsperbunch = sectsperbunch
end

--== sprite canonicalizer ==--
local function sprite2str(s)
    local FMT = "%+11d_"
    -- NOTE: this canonicalization isn't very useful except for debugging
    -- copy-paste in the editor.
    -- tostring(s): make sort stable
    return string.format(FMT:rep(4).."%s", s.x, s.y, s.z, s.ang, tostring(s))
end

local function canonicalize_sprite_order(map)
    local numsprites = map.numsprites

    map.spriten2o = {}  -- mapping of new to old sprite index

    if (numsprites == 0) then
        return
    end

    local spriteidx = {}

    for i=0,numsprites-1 do  -- 0->1 based indexing
        spriteidx[i+1] = i
    end

    table.sort(spriteidx,
               function(i1, i2)
                   return sprite2str(map.sprite[i1]) < sprite2str(map.sprite[i2])
               end)

    -- deep-copied sprite structs
    local spritedup = {}

    for i=0,numsprites-1 do
        -- save sorting permutation (0-based -> 0-based)
        map.spriten2o[i] = assert(spriteidx[i+1])

        -- back up struct
        spritedup[i] = ffi.new("spritetype")
        ffi.copy(spritedup[i], map.sprite[i], ffi.sizeof("spritetype"))
    end

    for i=0,numsprites-1 do  -- do the actual rearrangement
        map.sprite[i] = spritedup[spriteidx[i+1]]
    end
end

--== LOADBOARD ==--
-- returns:
--  on failure, nil, errmsg
--  on success, a table
--    {
--      version = <num>,
--      numsectors=<num>, numwalls=<num>, numsprites=<num>,
--      sector=<cdata (array of sectortype)>,
--      wall=<cdata (array of walltype)>,
--      sprite=nil or <cdata> (array of spritetype),
--      start =
--        { x=<num>, y=<num>, z=<num>, ang=<num>, sectnum=<num> },
--      numbunches = <num>,
--      sectsperbunch = {
--          [0] = { [<bunchnum>]=<number of ceilings> },
--          [1] = { [<bunchnum>]=<number of floors> }
--      }
--    }
function loadboard(filename, do_canonicalize_sprite)
    local fh, errmsg = io.open(filename, "rb")

    if (fh==nil) then
        return nil, errmsg
    end

    local cd = doread(fh, "int32_t", 4)
    if (cd==nil) then
        return nil, "Couldn't read header"
    end

    -- The table we'll return on success
    local map = {
        version = cd[0],
        start = { x=cd[1], y=cd[2], z=cd[3] },
    }

    if (map.version < 7 or map.version > 9) then
        fh:close()
        return nil, "Invalid map version"
    end

    cd = doread(fh, "int16_t", 3)
    if (cd==nil) then
        return nil, "Couldn't read header (2)"
    end

    map.start.ang = cd[0]
    map.start.sectnum = cd[1]

    -- sectors
    map.numsectors = cd[2]
    if (map.numsectors == nil) then
        return nil, "Couldn't read number of sectors"
    end
    if (map.numsectors <= 0 or map.numsectors > MAX.SECTORS[map.version]) then
        fh:close()
        return nil, "Invalid number of sectors"
    end

    map.sector = doread(fh, "sectortype", map.numsectors)
    if (map.sector == nil) then
        return nil, "Couldn't read sectors"
    end

    -- walls
    cd = doread(fh, "int16_t", 1)
    if (cd == nil) then
        return nil, "Couldn't read number of walls"
    end
    map.numwalls = cd[0]
    if (map.numwalls <= 0 or map.numwalls > MAX.WALLS[map.version]) then
        fh:close()
        return nil, "Invalid number of walls"
    end

    map.wall = doread(fh, "walltype", map.numwalls)
    if (map.wall == nil) then
        return nil, "Couldn't read walls"
    end

    -- sprites
    cd = doread(fh, "int16_t", 1)
    if (cd == nil) then
        return nil, "Couldn't read number of sprites"
    end
    map.numsprites = cd[0]
    if (map.numsprites < 0 or map.numsprites > MAX.SPRITES[map.version]) then
        fh:close()
        return nil, "Invalid number of sprites"
    end

    if (map.numsprites ~= 0) then
        map.sprite = doread(fh, "spritetype", map.numsprites)
        if (map.sprite == nil) then
            return nil, "Couldn't read sprites"
        end
    end
    fh:close()

    if (do_canonicalize_sprite) then
        -- must do this before setting metatable
        canonicalize_sprite_order(map)
    end

    map.sector = set_secwalspr_mt(map.sector, map.numsectors)
    map.wall = set_secwalspr_mt(map.wall, map.numwalls)
    map.sprite = set_secwalspr_mt(map.sprite, map.numsprites)

    get_numyaxbunches(map)

    -- done
    return map
end

local picanm_t = ffi.typeof[[
struct {
    uint8_t num;  // animate number
    int8_t xofs, yofs;
    uint8_t sf;  // anim. speed and flags
}
]]

local artfile_mt = {
    __index = {
        -- Global -> local tile index (-1 if gtile is not in this ART file)
        toltile = function(self, gtile)
            if (self.tbeg <= gtile and gtile <= self.tend) then
                return gtile - self.tbeg
            end
            return -1
        end,

        _check_ltile = function(self, ltile)
            if (not (ltile >= 0 and ltile < self.numtiles)) then
                error("Invalid local tile number "..ltile, 3)
            end
        end,

        _getofs = function(self, ltile)
            return self.tiledataofs + self.offs[ltile]
        end,

        dims = function(self, ltile)
            self:_check_ltile(ltile)
            return self.sizx[ltile], self.sizy[ltile]
        end,

        getpic = function(self, ltile)
            local sx, sy = self:dims(ltile)
            if (sx == 0 or sy == 0) then
                -- Tile nonexistent/empty in this ART file
                return nil, "Empty tile"
            end

            assert(self.fh:seek("set", self:_getofs(ltile)))
            return doread(self.fh, "uint8_t", sx*sy, self.grpfh ~= false)  -- GRPFH_FALSE
        end,
    },

    __metatable = true,
}

-- af, errmsg = artfile(filename [, grpfh, grpofs])
--
-- <filename>: File name of the file to get data from, expect if <grpfh>
--  passed. Always closed on error. Kept open on success.
-- <grpfh>: io.open() file handle to grouping file containing ART
--  uncompressed. Never closed, even on error.
-- <grpofs>: offset of the ART file in file given by <grpfh>
--
-- Returns:
--  * on error: nil, <errmsg>
--  * on success:
function artfile(filename, grpfh, grpofs)
    local ogrpofs
    local fh

    if (grpfh) then
        ogrpofs = grpfh:seek()
        assert(grpfh:seek("set", grpofs))
        fh = grpfh
    else
        local errmsg
        fh, errmsg = io.open(filename, "rb")
        if (fh == nil) then
            return nil, errmsg
        end
    end

    -- Close file on error?
    local dontclose = (grpfh ~= nil)

    -- Maybe close file handle and return error message <msg>
    local function err(msg, ...)
        if (not dontclose) then
            fh:close()
        end
        return nil, string.format(msg, ...)
    end

    local hdr = doread(fh, "int32_t", 4, dontclose)
    -- artversion, numtiles, localtilestart, localtileend
    if (hdr == nil) then
        return err("Couldn't read header")
    end

    local af = {
        filename = filename,
        fh = fh,
        grpfh = grpfh or false,  -- GRPFH_FALSE

        tbeg = hdr[2],
        tend = hdr[3],
        numtiles = hdr[3]-hdr[2]+1,

        -- Members inserted later:
        -- sizx, sizy: picanm: arrays of length af.numtiles
        -- tiledataofs: byte offset in `fh' to beginning of tile data
        -- offs: local byte offsets of each tile, relative of af.tiledataofs
        --
        -- Thus,
        --  af.tiledataofs + af.offs[localtilenum]
        -- is the byte offset of global tile
        --  af.tbeg + localtilenum
        -- in `fh'.
    }

    if (af.numtiles <= 0) then
        return err("Invalid tile start/end or empty ART file")
    end

    local lasttile = af.tbeg + af.numtiles
    if (lasttile >= MAX.TILES) then
        return err("Last tile %d beyond MAXTILES-1 (%d)", lasttile, MAX.TILES-1)
    end

    af.sizx = doread(fh, "int16_t", af.numtiles, dontclose)
    af.sizy = doread(fh, "int16_t", af.numtiles, dontclose)

    if (af.sizx==nil or af.sizy==nil) then
        return err("Couldn't read sizx or sizy arrays")
    end

    af.picanm = doread(fh, picanm_t, af.numtiles, dontclose)

    if (af.picanm == nil) then
        return err("Couldn't read picanm array")
    end

    af.tiledataofs = assert(fh:seek())
    af.offs = ffi.new("uint32_t [?]", af.numtiles)

    local curofs = 0

    for i=0,af.numtiles-1 do
        local sx, sy = af.sizx[i], af.sizy[i]

        if (sx > 0 and sy > 0) then
            af.offs[i] = curofs
            curofs = curofs + sx*sy
        elseif (sx < 0 or sy < 0) then
            -- Let's sanity-check stuff a little
            return err("Local tile %d has negative x or y size", i)
        else
            af.offs[i] = -1
        end
    end

    return setmetatable(af, artfile_mt)
end

local function set_sizarray_mt(sizar)
    local mt = {
        __index = function(tab, idx)
            if (not (idx >= 0 and idx < MAX.TILES)) then
                error("Invalid tile size array read access", 2)
            end
            return sizar[idx]
        end,

        __newindex = function(tab, idx, newval)
            if (not (idx >= 0 and idx < MAX.TILES)) then
                error("Invalid tile size array write access", 2)
            end
            sizar[idx] = newval
        end,
    }

    return setmetatable({}, mt)
end

--== LOADARTS (currently tilesizx and tilesizy only) ==--
-- filenames: a table with ART file names
-- returns:
--  on failure, nil, errmsg
--  on success, a table
--    {
--      sizx = <cdata (array of length MAXTILES)>
--      sizy = <cdata (array of length MAXTILES)>
--    }
function loadarts(filenames)
    local tile = {
        sizx = ffi.new("int16_t [?]", MAX.TILES),
        sizy = ffi.new("int16_t [?]", MAX.TILES),
    }

    for fni=1,#filenames do
        local fn = filenames[fni]
        local fh, errmsg = io.open(fn, "rb")

        if (fh==nil) then
            return nil, errmsg
        end

        local cd = doread(fh, "int32_t", 4)
        -- artversion, numtiles, localtilestart, localtileend
        if (cd==nil) then
            fh:close()
            return nil, fn..": Couldn't read header"
        end

        local localtilestart = cd[2]
        local numtileshere = cd[3]-localtilestart+1
--        print(fn.. ": "..cd[2].. ", "..cd[3])

        if (numtileshere < 0 or localtilestart+numtileshere >= MAX.TILES) then
            fh:close()
            return nil, fn..": Invalid tile start/end"
        end

        if (numtileshere==0) then
            fh:close()
        else
            -- X sizes
            cd = doread(fh, "int16_t", numtileshere)
            if (cd == nil) then
                fh:close()
                return nil, fn..": Couldn't read tilesizx array"
            end

            ffi.copy(tile.sizx+localtilestart, cd, numtileshere*2)

            -- Y sizes
            cd = doread(fh, "int16_t", numtileshere)
            if (cd == nil) then
                fh:close()
                return nil, fn..": Couldn't read tilesizy array"
            end

            ffi.copy(tile.sizy+localtilestart, cd, numtileshere*2)
        end
    end

    tile.sizx = set_sizarray_mt(tile.sizx)
    tile.sizy = set_sizarray_mt(tile.sizy)

    return tile
end

-- defs [, rdefs] = readdefs(fn [, alsoreverse])
function readdefs(fn, alsoreverse)
    local fh, errmsg = io.open(fn)

    if (fh==nil) then
        return nil, errmsg
    end

    local defs, rdefs = {}, nil

    for line in fh:lines() do
        local defname, numstr = string.match(line, "#?%s*define%s+([%a_][%w_]+)%s+([0-9]+)")
        if (defname) then
            defs[defname] = tonumber(numstr)
        end
    end

    if (alsoreverse) then
        rdefs = {}
        for defname, num in pairs(defs) do
            rdefs[num] = defname
        end
    end

    fh:close()
    return defs, rdefs
end


code:
#!/usr/bin/env luajit

local io = require("io")
local os = require("os")
local string = require("string")
local format = string.format
local table = require("table")

local B = require("build")

local ffi = require("ffi")
local C = ffi.C

ffi.cdef[[
int memcmp(const void *s1, const void *s2, size_t n);
]]

local fn1, fn2 = arg[1], arg[2]
local noOnlyLeft = (arg[3]=="-l")  -- don't show diff lines where left has tile, right hasn't

local function err(str)
    io.stderr:write(str, "\n")
    os.exit(1)
end

local function printf(fmt, ...)
    io.stdout:write(string.format(fmt, ...), "\n")
end

if (not fn1 or not fn2) then
    err("Usage: "..arg[0].." </path/to/tilesXXX.ART> </path/to/tilesYYY.ART> [-l]")
end

local af1, errmsg = B.artfile(fn1)
if (errmsg) then
    err(errmsg)
end

local af2, errmsg = B.artfile(fn2)
if (errmsg) then
    err(errmsg)
end

-- Check for same start/end

if (af1.tbeg ~= af2.tbeg) then
    err("tile starts differ: "..af1.tbeg.." "..af2.tbeg)
end

if (af1.tend ~= af2.tend) then
    err("tile ends differ: "..af1.tend.." "..af2.tend)
end

-- Compare the two ART files

local SIZEOF_PICANM = ffi.sizeof(af1.picanm[0])

for i=0,af1.numtiles-1 do
    local sx1, sx2 = af1.sizx[i], af2.sizx[i]
    local sxdif = (sx1 ~= sx2)

    local sy1, sy2 = af1.sizy[i], af2.sizy[i]
    local sydif = (sy1 ~= sy2)

    local sizedif = (sxdif or sydif)
    local picanmdif = (C.memcmp(af1.picanm[i], af2.picanm[i], SIZEOF_PICANM) ~= 0)

    local datadif = false
    -- compare data
    if (not sizedif) then
        local pic1 = af1:getpic(i)
        local pic2 = af2:getpic(i)

        if (pic1 and pic2 and C.memcmp(pic1, pic2, sx1*sy1) ~= 0) then
            datadif = true
        end
    end

    if (sizedif or picanmdif or datadif) then
        local strbuf = {}

        local s1str = sx1*sy1==0 and "none" or format("(%d,%d)", sx1, sy1)
        local s2str = sx2*sy2==0 and "none" or format("(%d,%d)", sx2, sy2)
        if ((sxdif or sydif) and (not noOnlyLeft or s2str ~= "none")) then
            strbuf[#strbuf+1] = format("size %s %s", s1str, s2str)
        end

        if (picanmdif) then
            strbuf[#strbuf+1] = "picanm"
        end

        if (not sizedif and datadif) then
            strbuf[#strbuf+1] = "data"
        end

        if (#strbuf > 0) then
            io.stdout:write(i..": "..table.concat(strbuf, ", "), "\n")
        end
    end
end


code:
-- Search for enemies with nonzero pal.
-- foreachmap module.

local build = require("build")
local D = build.readdefs("../../names.h") or error("Need ../../names.h")

local assert = assert
local print = print
local string = require("string")

local io = require("io")

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

module(...)

local ENEMY_NAME = {
--[[
    [D.BOSS1] = "BOSS1",
    [D.BOSS1STAYPUT] = "BOSS1STAYPUT",
--]]
    [D.BOSS2] = "BOSS2",
    [D.BOSS3] = "BOSS3",
    [D.BOSS4] = "BOSS4",
    [D.BOSS4STAYPUT] = "BOSS4STAYPUT",
    [D.COMMANDER] = "COMMANDER",
    [D.COMMANDERSTAYPUT] = "COMMANDERSTAYPUT",
    [D.DRONE] = "DRONE",
    [D.GREENSLIME] = "GREENSLIME",
    [D.LIZMAN] = "LIZMAN",
    [D.LIZMANFEEDING] = "LIZMANFEEDING",
    [D.LIZMANJUMP] = "LIZMANJUMP",
    [D.LIZMANSPITTING] = "LIZMANSPITTING",
    [D.LIZMANSTAYPUT] = "LIZMANSTAYPUT",
    [D.LIZTROOP] = "LIZTROOP",
    [D.LIZTROOPDUCKING] = "LIZTROOPDUCKING",
    [D.LIZTROOPJETPACK] = "LIZTROOPJETPACK",
    [D.LIZTROOPJUSTSIT] = "LIZTROOPJUSTSIT",
    [D.LIZTROOPONTOILET] = "LIZTROOPONTOILET",
    [D.LIZTROOPRUNNING] = "LIZTROOPRUNNING",
    [D.LIZTROOPSHOOT] = "LIZTROOPSHOOT",
    [D.LIZTROOPSTAYPUT] = "LIZTROOPSTAYPUT",
    [D.OCTABRAIN] = "OCTABRAIN",
    [D.OCTABRAINSTAYPUT] = "OCTABRAINSTAYPUT",
    [D.ORGANTIC] = "ORGANTIC",
    [D.PIGCOP] = "PIGCOP",
    [D.PIGCOPDIVE] = "PIGCOPDIVE",
    [D.PIGCOPSTAYPUT] = "PIGCOPSTAYPUT",
    [D.RAT] = "RAT",
    [D.ROTATEGUN] = "ROTATEGUN",
    [D.SHARK] = "SHARK",
}

local uniq = false

function init(args)
    if (#args == 1) then
        local wr = function(s) io.stderr:write(s) end
        wr("Usage: "..args[0].." "..args[1].." [-u] *.map ...\n")
        wr("  -u: print only one pal-x-tilenum combination\n")
    end

    if (args[2]:sub(1,1)=="-") then
        assert(args[2]=="-u", "Unknown option "..args[2])
        uniq = true
        return 3
    end
end

function success(map, fn)
    -- Have one at all?
    local haveone = false

    -- Have pal-x-tile combination?
    -- [tile*256 + pal] = true
    local havept = {}

    for i=0,map.numsprites-1 do
        local spr = map.sprite[i]
        local picnum, pal = spr.picnum, spr.pal
        local name = ENEMY_NAME[picnum]

        if (name and pal~=0) then
            if (not (name:match("^LIZTROOP") and pal==21)) then  -- those are handled by CON
                if (not uniq or not havept[picnum*256 + pal]) then
                    if (not haveone) then
                        printf("%s:", fn)
                        haveone = true
                    end
                    printf("%5d %3d %s", i, pal, name)
                    havept[picnum*256 + pal] = true
                end
            end
        end
    end
    if (haveone) then
        print("")
    end
end


code:
#!/bin/bash

ok=yes
if [ -z "$1" ]; then
    ok=
fi
if [ -z "$2" ]; then
    ok=
fi

if [ -z "$ok" ]; then
    echo "Usage: $0 <dir> <some_foreachmap_module.lua | code for foreachmap.lua -e>"
    exit 1
fi

LOPT=-L
idx=$(expr match `uname -s` '[mM][iI][nN][gG][wW]')
if [ "$idx" != 0 ]; then
    LOPT=
fi

FN="$1"
ARG="$2"

idx=$(expr match "$ARG" '.*lua$')
if [ "$idx" == 0 ]; then
    ARG="-e$ARG"
    find $LOPT "$FN" -iname '*.map' -print0 | xargs -0 ./foreachmap.lua "$ARG"
else
    shift
    # So that you can e.g. do
    # ./findmaps.sh ~/.eduke32 ./colenemy.lua -u
    find $LOPT "$FN" -iname '*.map' -print0 | xargs -0 ./foreachmap.lua "$@"
fi


code:
#!/usr/bin/env luajit

-- Generic map iterator.

-- The first cmdline arg is a name of a lua file (may be sans .lua) which must
-- be a module and is `require'd into this script, e.g. "stats" or "stats.lua".
-- First, a key named .init is looked up in the loaded module and if it exists,
-- it is run like .init(arg)  (thus allowing it to parse the command-line
-- arguments and then potentially remove the ones it used).
-- If .init returns non-nil, this script aborts.
-- Otherwise, for each 2nd and following argument, if map loading succeeds,
--  .success(map, filename) is run, otherwise
--  .failure(filename, errmsg) is run if that key exists, or a standard error
--  message is printed to stderr.
-- Finally, if there is a .finish field in the module, it is run with no args.

-- forxcode example: print sprite numbers with lotag < -1 (lotag is signed for us),
-- and for each matching sprite also print its lotag and picnum:
--  $ ./findmaps.sh ~/.eduke32/ "sprite: .lotag < -1 :: io.write(', '.. .lotag .. ' ' .. .picnum)"

-- The local 'd' provides defs loaded from ../../names.h, example:
--  $ ./findmaps.sh ~/.eduke32/ "sprite: .picnum>=d.CRACK1 and .picnum<=d.CRACK4"
-- (Now: no space between "d." and "CRACK" is necessary ".xxx" is translated to
-- "sprite[<current>].xxx" only if it's the name of a sprite member.)
-- The local 'rdefs' provides the reverse mapping: from picnums to tile names
-- (or nil).

-- Print all V9 maps along with their number of bunches and max(ceilings of a bunch)
--  $ prog='if (map.version==9) then print(map.numbunches.." ".. math.max(unpack(map.sectsperbunch[0],0)) .." "..fn) end'
--  $ ./findmaps.sh ~/.eduke32 "$prog" |sort -n -k 2

-- Print all MUSICANDSFX sprites that play sounds with bit 1 set.
-- ./findmaps.sh /g/Games/Eduke32c/grp 'sprite: .picnum==5 and eq(.lotag, {170, 186, 187, 279, 382, 347}) :: io.write(" ".. tostring(.lotag))'

-- Print all maps that have floor-aligned blockable sprites in underwater sectors.
-- ./findmaps.sh ~/.eduke32 'sprite:: bit.band(.cstat,49)==33 and .sectnum>=0 and .sectnum < map.numsectors and sector[.sectnum].lotag==2

local B = require "build"
local string = require "string"
local io = require "io"
local os = require "os"

if (#arg < 1) then
    local wr = function(s) io.stdout:write(s) end
    wr("Usage: ./foreachmap.lua <module[.lua]> [init args...] <filename.map> ...\n")
    wr("       ./foreachmap.lua -e\"some_lua_code ...\" <filename.map> ...\n")
    wr("       ./foreachmap.lua -e\"[sector|wall|sprite]: <condition on .<field>>\" <fn.map> ...\n\n")
    wr("In the second form, the code is run as body of a function(map, fn)\n")
    wr("and num{sectors,walls,sprites} and {sector,wall,sprite} do not\n")
    wr("need to be qualified with the \"map.\" prefix.\n")
    wr("The third form is a shortcut for quickly finding sectors/walls/sprites\n")
    wr("satisfying a certain condition (see example below)\n\n")
    wr("Examples: ./foreachmap.lua -e\"if map.numbunches==1 then print(fn) end\" ~/.eduke32/*.map\n")
    wr("          ./foreachmap.lua -e\"sprite: .picnum==10 and .lotag==2563\" *.map\n")
    wr("          ./foreachmap.lua -e\"sprite:: ...  -- (only prints the file names)\n")
    wr("(See foreachmap.lua for an example of the \"forxcode\" feature.)\n\n")
    return
end

local modname = string.gsub(arg[1], "%.lua$", "")

function sum(tab, initidx)
    local s = 0
    for i=(initidx or 1),#tab do
        s = s + tab[i]
    end
    return s
end

local g_what
-- Maybe replace e.g. .nextwall --> wall[i].nextwall.
-- <what>: one of "sector", "wall" or "sprite"
-- <maybechar>: the char before <maybememb>, or ""
-- <maybememb>: a potential member name prefixed by "."
local function maybe_complete_member(maybechar, maybememb)
    if (maybechar~="]" and B.ismember(g_what, maybememb:sub(2))) then
        return maybechar..g_what.."[i]"..maybememb
    else
        return maybechar..maybememb
    end
end

local mod
if (modname:sub(1,2) == "-e") then
    local body = modname:sub(3)

    -- sector/wall/sprite finder shortcut
    local b, e, what = body:find("^([a-z]+)::?")
    if (what) then
        g_what = what
        local onlyfiles = (body:sub(e-1,e)=="::")  -- "::" means "only list files" (like grep -l)
        body = body:sub(e+1)  -- clip off "bla::"
        body = body:gsub("(.?)(%.[a-z][a-z0-9]*)", maybe_complete_member)  -- e.g. .lotag --> sprite[i].lotag

        local perxcode
        -- look for additional "print" code to be executed for each match
        b, e, perxcode = body:find("::(.*)")
        if (perxcode) then
            body = body:sub(1,b-1)
            if (onlyfiles) then
                error("Per-x code can only be used with the ':' syntax (list each match)")
            end
        else
            perxcode = ""
        end

        assert(what=="sector" or what=="wall" or what=="sprite")

        body =
            "for i=0,num"..what.."s-1 do\n"..
            "  if ("..body..") then\n"..
            (onlyfiles and "io.write(fn); return\n" or "io.write(fn..': '..i)\n") ..
            perxcode .. "io.write('\\n')\n"..
            "  end\n"..
            "end\n"
    end

    local successfunc, errmsg = loadstring([[
        local function eq(x, tab)
            for i=1,#tab do
                if (x==tab[i]) then return true end
            end
        end
        local d,rdefs = require('build').readdefs('../../names.h',true)
        if (not d) then error('Need ../../names.h') end -- XXX
        local numsectors, numwalls, numsprites, sector, wall, sprite
        return function (map, fn)
            numsectors, numwalls, numsprites = map.numsectors, map.numwalls, map.numsprites
            sector, wall, sprite = map.sector, map.wall, map.sprite
        ]]..body
    .." end")

    if (successfunc==nil) then
        io.stderr:write("Error loading string: "..errmsg.."\n")
        io.stderr:write("Function body:\n", body)
        os.exit(1)
    end

    mod = { success=successfunc() }
else
    mod = require(modname)
end

-- The return value from the module's .init().
local initret

if (mod.init) then
    initret = mod.init(arg)
    if (type(initret)=="number" and initret < 0) then
        -- A negative return value from .init() is taken as a request to exit,
        -- e.g. because an error occurred.
        os.exit(1)
    end
end

-- A positive return value from .init() is taken to start counting map names
-- from that 'arg' index.
local startargi = (type(initret)=="number" and initret > 0 and initret) or 2

for i=startargi,#arg do
    local fn = arg[i]
    local map, errmsg = B.loadboard(fn)

    if (map ~= nil) then
        mod.success(map, fn)
    else
        if (mod.failure) then
            mod.failure(fn, errmsg)
        else
            io.stderr:write(string.format("--- %s: %s\n", fn, errmsg))
        end
    end
end

if (mod.finish) then
    mod.finish()
end