raze/polymer/eduke32/source/lunatic/test.elua
helixhorned 7f2175fcec Lunatic: retire per-sprite tsprite access for per-tile animation callback reg.
Don't yet make this official API though, since there are unresolved issues
with newly created tsprites potentially being fed back to the animation loop.
Add xmath.angvec(), xmath.bangvec(), tspr:set_sectnum(), tspr:setpos().

git-svn-id: https://svn.eduke32.com/eduke32@3937 1a8010ca-5511-0410-912e-c29ae57300e0
2013-07-07 20:59:10 +00:00

707 lines
21 KiB
Text

-- test script for ELua/Lunatic Interpreter
--do return end
-- error=nil -- must not affect "require"
local string = require("string")
local bit = require("bit")
local math = require("math")
local pcall = pcall
local DBG_ = DBG_
print('---=== ELua Test script ===---')
local function printf(fmt, ...)
print(string.format(fmt, ...))
end
local function checkfail(funcstr, expectedmsg)
local status, errmsg = pcall(DBG_.loadstring(funcstr))
if (status) then
print('^21ERROR:^O '..funcstr.." DIDN'T fail")
else
if (expectedmsg==nil or string.find(errmsg, expectedmsg, 1, true)) then
print("^11SUCCESS:^O "..funcstr.." failed: "..errmsg)
else
-- XXX: beginning with "^10" is counted as error in OSD_Printf()
print("^10ERROR*:^O "..funcstr.." failed: "..errmsg..
", but expected error message was: "..expectedmsg)
end
end
end
--[[
---- check serialization ----
-- test nans, infs, precision, subnorms, booleans
ourvar2 = { "asd"; 0/0, 1/0, -1/0, 0.12345678901234567, 1e-314, true }
ourvar = { ourvar2; 1, 2, 3, "qwe"; [true]=0, [false]=1 }
ourvar[#ourvar+1] = ourvar;
--]]
local vol, lev = gv.currentEpisode()+1, gv.currentLevel()+1
printf('volume=%d, level=%d', vol, lev)
---[[
if (vol ~= 4) then
-- Tweak some sector pals.
-- NOTE: you're not really supposed to modify game state from Lua file
-- scope! This is for testing only! E.g. it will fail if a savegame is
-- loaded from the menu when no other level is loaded.
print('tweaking sector pals')
print('numsectors: ' .. gv.numsectors .. ' of ' .. gv.MAXSECTORS)
local SF = sector.STAT
for i = 0, gv.numsectors/2 do
local sec = sector[i]
sec.floorpal = 1;
sector[i].floor.shade = sec.floor.shade + 4
sector[i].ceilingpal = 2;
local ceil = sec.ceiling
ceil.shade = sector[i].ceiling.shade + 8
ceil.statbits:flip(SF.SMOOSH)
sec.floorstatbits:flip(SF.SWAPXY)
end
end
if (vol==1 and lev==1) then -- E1L1
print('tweaking some sprites 2')
local i = 562
spriteext[i].alpha = 0.5;
sprite[i].cstat = bit.bor(sprite[i].cstat, 2+512);
spriteext[i].pitch = 128;
spriteext[i].roll = 256;
i = 107 -- pistol ammo at rooftop
spriteext[i].pitch = 128;
spriteext[i].roll = 256;
for spr in spritesofsect(307) do -- some fence sprites in E1L1
printf('spr %d', spr)
sprite[spr].pal = 6
end
--this is a problem
--actor = {}
actor[562].flags = bit.bor(actor[562].flags, 2); -- pal 6 with goggles on front SEENINE
end
--[[
-- TODO: better API for all TROR things?
if (vol==1 and lev==8) then
print('tweaking bunch 1');
-- trueror1.map
for i in sectorsofbunch(1, gv.CEILING) do
sector[i].ceilingz = sector[i].ceilingz - 3*1024;
end
for i in sectorsofbunch(1, gv.FLOOR) do
sector[i].floorz = sector[i].floorz - 3*1024;
end
end
--]]
local unsafe = pcall(function() string.UNSAFE=true; end)
--]]
--tostring = nil -- REMEMBER
--DBG_.printkv('_G in test.elua', _G)
-- direct gv array access forbidden
checkfail('gv.sprite[0].yrepeat = 100', "access forbidden")
checkfail('print(sprite[100000].ceilingpal)', "out-of-bounds sprite[] read access")
checkfail('print(gv.sprite[0])', "access forbidden")
-- set metatable forbidden
checkfail('setmetatable(sprite, {})', "attempt to read undeclared variable 'setmetatable'")
-- OOB write access.
-- Note that indexing ("reading") sector fails, even if the user wants to
-- assign to a sector member. Potentially confusing error message.
checkfail('sector[-1].ceilingpal = 4', "out-of-bounds sector[] read access")
-- wallnum member is read-only
checkfail('sector[0].wallnum = 0', "attempt to write to constant location") -- this comes from LJ/FFI
-- gv.numsectors is read-only
checkfail('gv.numsectors = 4', "attempt to write to constant location")
-- cannot create new fields in 'gv'
checkfail('gv.QWE = 4', "write access forbidden")
-- direct sector write access forbidden
checkfail('sector[4] = sector[6]', "cannot write directly to sector[]")
-- that would be horrible...
checkfail('sprite._nextspritesect[4] = -666', "cannot write directly to nextspritesect[]")
-- we're indexing a plain array!
checkfail('print(sprite._nextspritesect[4].whatfield)', "attempt to index a number value")
-- creating new keys forbidden... handled by LuaJIT
checkfail('wall[4].QWE = 123', "has no member named 'QWE'")
-- our 'require' has only safe stuff
--checkfail("require('os')")
-- we must declare globals with 'gamevar'
checkfail("new_global = 345", "attempt to write to undeclared variable 'new_global'")
-- can't redefine constants in 'gv'
checkfail('gv.CEILING = 3', "attempt to write to constant location")
-- string.dump is unavailable
checkfail('local s=require[[string]]; local tmp=s.dump(gameevent)',
"attempt to call field 'dump' (a nil value)")
if (not unsafe) then
-- changing base module tables is disallowed
checkfail('local s=require[[string]]; s.format=nil', "modifying base module table forbidden")
else
print('WARNING: RUNNING WITH UNPROTECTED BASE MODULES')
end
print('')
-- This is problematic, even though pretty much every access will yield a
-- "missing declaration" error.
-- See http://luajit.org/ext_ffi_api.html#ffi_C about what stuff ffi.C contains.
checkfail('gv.luaJIT_setmode(nil, 0, 0)', "missing declaration for symbol 'luaJIT_setmode'")
checkfail('gv.luaJIT_BC_con_lang', "attempt to call a nil value")
checkfail('gv.gethiticks = nil', "attempt to write to constant location")
-- actor[].t_data[] is not accessible for now
checkfail('local i = actor[0].t_data[15]', "has no member named 't_data'")
-- no pointer arithmetic!
checkfail('local spr = sprite[0]; local x=spr+1', "attempt to perform arithmetic on")
checkfail('gameactor{1680, 0}', "must provide a function with last numeric arg or .func")
checkfail("do local bt=require'test.test_bitar'; bt.QWE=1; end", "modifying module table forbidden")
-- the cdata returned by player[] can't be made into a pointer!
checkfail("do local pl=player[0]; i=pl[1]; end")
checkfail("do local ud=gv.ud.camerasprite; end", "access forbidden") -- test for proper decl()
checkfail("sprite[0]:set_picnum(-10)", "invalid tile number")
checkfail("gv.g_sizes_of=nil; print(gv.g_sizes_of[0])", "write access forbidden")
checkfail("gv.cam.sect=-1", "invalid sector number")
checkfail("local flag=gv.SFLAG_NULL", "missing declaration")
player[0].wackedbyactor = -1 -- should succeed
checkfail("player[0].curr_weapon = -1", "Invalid weapon ID")
player[0].curr_weapon = 1
printf('ceilingbunch of sector 0: %d', sector[0].ceilingbunch)
gameevent{gv.EVENT_JUMP,
function(actori, playeri, dist)
printf("jump i=%d p=%d d=%d", actori, playeri, dist)
error("greetings from EVENT_JUMP")
end
}
gameevent
{
"PROCESSINPUT",
-- Input test.
-- NOTE: I don't think that exposing g_player[].sync (aka "input") is a good idea...
func = function(actori, playeri, dist)
local IB = player._INPUT_BITS
local input = player[playeri]._input
if (bit.band(input.bits, IB.JUMP) ~= 0) then
print("JUMPED")
-- ... because for example this doesn't work
-- (P_HandleSharedKeys, where the JETPACK bit is tested, runs
-- before P_ProcessInput):
input.bits = bit.bor(input.bits, IB.JETPACK)
end
end
}
local D = require("CON.DEFS")
-- test event chaining
gameevent
{
"JUMP",
flags = actor.FLAGS.chain_beg,
function(actori, playeri, dist)
local ps = player[playeri]
print("\n--- I'm first!")
-- DBG_.oom()
local pistol = ps.weapon.PISTOL
if (pistol.shoots ~= D.RPG) then
pistol.shoots = D.RPG
else
pistol.shoots = D.SHOTSPARK1
end
ps.weapon[gv.PISTOL_WEAPON].firesound = D.LIGHTNING_SLAP
-- Set pipebomb and tripbomb to timed mode.
-- XXX: Provide either named constants or methods?
-- XXX: These are probably reset to default on new game.
ps.pipebombControl = 2
ps.tripbombControl = 2
-- Test of INTERNAL member _pals.
-- NOTE: setting colors partially is bad! E.g. after an item is
-- picked up, col[0] and col[1] remain and tint everything greenish.
ps._pals[2] = 20
ps._pals.f = 30
end
}
local xmath = require "xmath"
gameevent
{
gv.EVENT_ENTERLEVEL,
function()
if (gv._DEBUG_LUNATIC) then
DBG_.testmembread()
end
-- NOTE: times are for helixhorned (Core2Duo 3GHz)
local i
local N = 1e6
local t = gv.gethiticks()
for i=3,N do
gv.gethiticks()
end
t = gv.gethiticks()-t
-- x86_64: 35ns/call, x86: 280 ns/call
-- Windows 32-bit: about 1 us/call?
printf("%d gethiticks() calls took %.03f ms (%.03f us/call)",
N, t, (t*1000)/N)
local sum=0
t = gv.gethiticks()
for i=1,N do sum = sum+gv.ksqrt(i) end
t = gv.gethiticks()-t
-- x86_64: 14ns/call
printf("%d ksqrt() calls took %.03f ms (%.03f us/call) [sum=%f]",
N, t, (t*1000)/N, sum)
sum=0
t = gv.gethiticks()
for i=1,N do sum = sum+math.sqrt(i) end
t = gv.gethiticks()-t
-- x86_64: 7ns/call
printf("%d math.sqrt() calls took %.03f ms (%.03f us/call) [sum=%f]",
N, t, (t*1000)/N, sum)
printf("sqrt(0xffffffff) = %f(ksqrt) %f(math.sqrt)",
gv.ksqrt(0xffffffff), math.sqrt(0xffffffff))
local pl = player[0]
-- MAX < current is "allowed"
pl.max_ammo_amount[gv.RPG_WEAPON] = 17
pl:give_weapon(gv.RPG_WEAPON)
pl.ammo_amount[gv.RPG_WEAPON] = 54
pl:give_weapon(gv.SHRINKER_WEAPON)
-- This looks much prettier:
pl.ammo_amount.SHRINKER = 2
-- MORTER2 from test/weaponvars.con
player[0].weapon.SHOTGUN.shoots = 1653
local proj = projectile[1653]
proj.drop = 0
proj:set_trail(D.SMALLSMOKE)
t = gv.gethiticks()
local N=1
for n=1,N do
for i=0,gv.MAXSPRITES-1 do
sprite[i].filler = 1
end
for i=gv.MAXSPRITES-1,0,-1 do
sprite[i].shade = 1
end
for i=0,gv.MAXSPRITES-1 do
sprite[i].xoffset = 0
end
for i=gv.MAXSPRITES-1,0,-1 do
sprite[i].yoffset = 1
end
end
t = gv.gethiticks()-t
printf("%d x four 0..MAXSPRITES-1 iterations took %.03f us per outer iteration", N, (1000*t)/N)
-- Results on x86:
-- N=1: 480-1000 us (too large variance)
-- N=10: 190-210 us * 10 = 1.9-2.1 ms
-- N=100: about 160 us * 100 = about 16 ms
-- Make the DUKECAR in E1L1 into a zombie actor (temporarily)
-- NOTE: Use static value (not the one from 'D').
if (sprite[24].picnum==2491) then
sprite.changestat(24, actor.STAT.ZOMBIEACTOR)
end
checkfail("gameevent('GAME', function() print('qwe') end)",
"must be called from top level")
-- Test vec3 + wall. Pseudo wall member 'z' will be accessed.
local mpos = xmath.vec3()
for i=0,gv.numwalls-1 do
mpos = mpos + wall[i]
end
mpos = mpos/gv.numwalls
local impos = xmath.ivec3(mpos)^20 -- test ivec3 with dvec3 arg, test '^' op
assert(impos.z == -20)
printf("Map center point: (%d,%f)", mpos.x, impos.y)
end
}
gameevent{"LOADACTOR", function(i)
local spr = sprite[i]
if (i==614 and spr.picnum==930) then
-- "police line" ribbon in E1L1
-- clear the hitag so that it doesn't spawn as FALLER from premap
spr.hitag = 0
end
end}
gameactor
{
-- "police line" ribbon
930, nil, 1,
func = function(i)
local spr = sprite[i]
local r = math.random
local d = 20
-- NOTE: __add metamethod is called because of the RHS:
local v = spr + xmath.vec3(r(-d,d), r(-d,d))
spr:setpos(v)
-- Test vec3 constructor with cdata.
local tempvec = xmath.vec3(player[0].pos)
end
}
local stat = require("stat")
local hs = stat.new()
local con = require("con")
local AC, MV = con.AC, con.MV
local CAC, CMV, CAI = require("CON.ACTION"), require("CON.MOVE"), require("CON.AI")
assert(CAC); assert(CMV); assert(CAI)
local AC, MV = {}, {}
AC.TROOPSTAND = assert(CAC.ATROOPSTAND) -- or con.action{0,1,5,1,1}
AC.TROOPFLINTCH = con.action{50, 1, 1, 1, 6}
MV.SHRUNKVELS = con.move{hvel=32}
con.ai(AC.TROOPFLINTCH, MV.SHRUNKVELS, 0) -- unused; TODO: test
local TROOPSTRENGTH = 30
local AF = actor.FLAGS
local CS = sprite.CSTAT
-- Also test actor code chaining: strength is doubled.
gameactor
{
D.LIZTROOP, AF.chain_end+AF.enemy, 2*TROOPSTRENGTH,
action = AC.TROOPSTAND,
func = function(i, playeri, dist)
sprite[i].pal = math.random(32)
-- sprite[i].ang = bit.band(sprite[i].ang-20, 2047)
local spr = sprite[i]
local t = gv.gethiticks()
local hit = hitscan(spr, spr.sectnum, 10, 10, 0, gv.CLIPMASK0)
hs:add(1000*(gv.gethiticks()-t))
if (hs.n == 300) then
printf("hitscan: %s", hs:getstatstr())
hs:reset()
error("greetings from LIZTROOP actor")
end
local actr = actor[i]
if (actr:get_count() % 30 == 0) then
spr.cstatbits:flip(CS.YFLIP)
end
-- Test of bitint's ":test()" for actor[].flags.
actr.flagsbits:test(AF.NVG)
if (dist < 4096) then
-- Duke Vader / Anakin Nukewalker?
actor[i]:set_action(AC.TROOPFLINTCH)
actor[i]:set_move(MV.SHRUNKVELS)
if (dist < 1024) then
con.killit()
end
end
if (actr:has_action(CAC.ATROOPWALKING)) then
if (actr:get_count() % 50 == 0) then
actr.movflagsbits:flip(actor.MOVFLAGS.spin)
end
end
end,
-- NOTE: the animate callback is not yet documented and thus not official API!
animate = function(tspr)
local tspr2 = tspr:dup()
if (tspr2) then
tspr2.x = tspr2.x + 512*math.cos(gv.totalclock/60)
tspr2.y = tspr2.y + 512*math.sin(gv.totalclock/60)
tspr2.cstatbits:set(CS.TRANS_BITMASK)
end
-- XXX: inserted tsprites have floor shadow in classic! (r_shadow)
-- G_DoSpriteAnimations() is passed as callback to the engine on occasion,
-- in other words, created tsprites may be fed back to G_DoSpriteAnimations()!
-- classic: shows shadow for both "ghost" liztroop and aim "reticle"
-- Polymost: only for "ghost"
-- Polymer: none
local aimv = 256*xmath.bangvec(tspr.ang)
local hit = hitscan(tspr^(16*256), tspr.sectnum, aimv.x, aimv.y, 0, gv.CLIPMASK1)
if (hit.wall >= 0) then
local aimtspr = tspr:dup()
if (aimtspr) then
aimtspr.pal = 2
aimtspr:set_picnum(555)
aimtspr:setpos(hit.pos)
aimtspr:set_sectnum(hit.sect)
end
end
end,
}
gameactor
{
1275, -- newspaper, E4L6 sprite #513
action = con.action{0, 4, delay=20}, -- Same as {0, 4, 1, 1, 20}
move = 1, -- so that the ID is nonzero and it will move
func = function(aci)
local a = actor[aci]
local delay = math.sin(0.1 * 2*math.pi*gv.totalclock/120)
a:set_action_delay(20 + 10*delay)
if (sprite[aci].pal ~= 0) then
a:set_hvel(1024/30)
a:set_vvel(-1024/30)
else
a:set_hvel(50*delay)
end
a.movflags = actor.MOVFLAGS.geth + actor.MOVFLAGS.getv
end,
}
gameevent
{
"DISPLAYROOMS",
function()
local cam = gv.cam
cam.pos.z = cam.pos.z + 64*16*math.sin(gv.totalclock/30)
local ps = player[0]
if (ps.cursectnum >= 0) then
local hit = sector[ps.cursectnum]:zrangeat(cam.pos)
if (gv.totalclock%200==0) then
printf("hit %s %d at z=%d, %s %d at z=%d",
hit.c.spritep and "sprite" or "sector", hit.c.num, hit.c.z,
hit.f.spritep and "sprite" or "sector", hit.f.num, hit.f.z)
end
end
end
}
gameevent
{
"DISPLAYREST",
function()
for i=0,10 do
for j=1,100 do
-- XXX: This is slower in the Polymodes than with classic!
-- con._gametext(2822, j, i, 12, 0, 0, 16, 0,0,gv.xdim,gv.ydim,8192)
end
end
-- Clear showing every sector with the pavement floor tile. (Unless we're
-- in such a sector or an adjoining one.)
-- XXX: We need a better place to do this, maybe an event in
-- G_DisplayRest() where show2dsector[] is tweaked?
local show2dsector = sector.showbitmap
for i=0,gv.numsectors-1 do
if (sector[i].floorpicnum==815) then
show2dsector:set0(i)
end
end
end
}
gameactor
{
D.APLAYER, actor.FLAGS.chain_beg,
function(aci, pli)
if (con._squished(aci, pli)) then
printf("Squished LunaCON test")
end
end
}
local function testbit(num, b)
return bit.band(num,b)~=0 and 1 or 0
end
local FADE_SPEED = {
[gv.KNEE_WEAPON] = 1/2.5,
1/128,
1/5,
1/3,
1/2,
1, -- pipebomb: ~1 sec
2,
3,
5,
127, -- freezer; such a fast fade is not visible, but it clears any
-- existing one (if of higher priority)
[gv.GROW_WEAPON] = 9.9, -- test banker's rouding -- should be like 10
}
-- Test player[]:fadecol(), a better palfrom.
gameevent
{
"CHANGEWEAPON",
function (aci, pli)
local ps = player[pli]
local neww, curw = gv.g_RETURN, ps.curr_weapon
local r, g, b = testbit(neww, 1), testbit(neww, 2), testbit(neww, 4)
local speed = FADE_SPEED[neww] or 1
player[pli]:fadecol(0.5, r, g, b, speed, neww-5)
end
}
-- Time the above p:fadecol() test for verification of the <speed> argument.
local last_f, last_t = 0, 0
local last_secs = nil
gameevent
{
"DISPLAYREST",
function(aci, pli)
local ps = player[pli]
-- WARNING: This function uses INTERNAL interfaces.
local curf = ps._pals.f
if (curf > last_f) then
-- Starting a tint
last_secs = nil
last_f = curf
last_t = gv.getticks()
elseif (last_t > 0 and curf==0) then
-- Fade has ended
last_secs = (gv.getticks()-last_t)/1000
last_f, last_t = 0, 0
end
if (last_secs ~= nil) then
con.minitext(10, 10, string.format("Last fade time: %.03f s (%.03f gametics)",
last_secs, last_secs*30))
end
end,
}
printf("EVENT_INIT = %d", gv.EVENT_INIT) -- tests default defines
local bittest = require "test.test_bitar"
bittest.sieve()
require("test.test_geom", 123123)
require("test.test_rotspr")
do
-- Test ksin vs. sinb
local xmath = require "xmath"
local sum = 0
local N = 1000
local t = gv.gethiticks()
for i=0,N*2048-1 do
sum = sum+xmath.ksin(i)
end
t = gv.gethiticks()-t
sum = sum*1e12
printf("ksin: sum*1e12=%.03f, %.03fus per 0-2047 cycle", sum, t)
sum = 0
local t = gv.gethiticks()
for i=0,N*2048-1 do
sum = sum+xmath.sinb(i)
end
t = gv.gethiticks()-t
sum = sum*1e12
printf("sinb: sum*1e12=%.03f, %.03fus per 0-2047 cycle", sum, t)
-- Results (helixhorned x86):
-- ksin: sum*1e12=0.000, 3.801us per 0-2047 cycle
-- sinb: sum*1e12=0.052, 2.886us per 0-2047 cycle
end
do
-- Test getflorzofslopeptr()/sector[]:floorzat()
local N = 100
local t = gv.gethiticks()
for n=1,N do
for i=0,gv.numsectors-1 do
local sec = sector[i]
local w1 = sec.wallptr
sec:floorzat(wall[w1])
end
end
printf("%d x %d floorzat: %.03f us", N, gv.numsectors, (gv.gethiticks()-t)*1000)
-- Results for 100 x 325 floorzat (helixhorned x86):
-- cdecl getflorzofslope(): 572.165 us
-- __fastcall getflorzofslope(): 830.147 us (sic, but tested only once!)
end
print('---=== END TEST SCRIPT ===---')
--[[
function check_sector_idx()
error("bla")
end
spritesofsect(0)
--]]
--DBG_.oom()
-- This will complain about wrong usage of 'error'. In particular,
-- the nil must not propagate to C!
checkfail('error(nil)', "error using 'error': error message must be a string")
local D = require("CON.DEFS")
checkfail('require("CON.DEFS").APLAYER=123', "modifying base module table forbidden")
-- Test with lunatic/test/rotfixed_actor.con.
print("DUKECAR="..tostring(D.DUKECAR))