Something went wrong. Try again.
OpenMW is an open implementation of the Morrowind engine
Something went wrong. Try again.
21 kB · 473 lines
Lua
at master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474local testing = require('testing_util')local core = require('openmw.core')local async = require('openmw.async')local util = require('openmw.util')local types = require('openmw.types')local vfs = require('openmw.vfs')local world = require('openmw.world')local I = require('openmw.interfaces')
local function initPlayer(player) player = player or world.players[1] player:teleport('', util.vector3(4096, 4096, 1745), util.transform.identity) coroutine.yield() return playerend
testing.setSetupLocalTest(function(player) initPlayer(player)end)
testing.registerGlobalTest('crash in lua coroutine when accessing type (#8757)', function() local co = coroutine.wrap(function() testing.expectEqual(tostring(world.players[1].type), 'Player') end) co()end)
testing.registerGlobalTest('timers', function() testing.expectAlmostEqual(core.getGameTimeScale(), 30, 'incorrect getGameTimeScale() result') testing.expectAlmostEqual(core.getSimulationTimeScale(), 1, 'incorrect getSimulationTimeScale result')
local startGameTime = core.getGameTime() local startSimulationTime = core.getSimulationTime()
local ts1, ts2, th1, th2 local cb = async:registerTimerCallback("tfunc", function(arg) if arg == 'g' then th1 = core.getGameTime() - startGameTime else ts1 = core.getSimulationTime() - startSimulationTime end end) async:newGameTimer(36, cb, 'g') async:newSimulationTimer(0.5, cb, 's') async:newUnsavableGameTimer(72, function() th2 = core.getGameTime() - startGameTime end) async:newUnsavableSimulationTimer(1, function() ts2 = core.getSimulationTime() - startSimulationTime end)
while not (ts1 and ts2 and th1 and th2) do coroutine.yield() end
testing.expectGreaterOrEqual(th1, 36, 'async:newGameTimer failed') testing.expectGreaterOrEqual(ts1, 0.5, 'async:newSimulationTimer failed') testing.expectGreaterOrEqual(th2, 72, 'async:newUnsavableGameTimer failed') testing.expectGreaterOrEqual(ts2, 1, 'async:newUnsavableSimulationTimer failed')end)
testing.registerGlobalTest('teleport', function() local player = world.players[1] player:teleport('', util.vector3(100, 50, 500), util.transform.rotateZ(math.rad(90))) coroutine.yield() testing.expect(player.cell.isExterior, 'teleport to exterior failed') testing.expectEqualWithDelta(player.position.x, 100, 1, 'incorrect position after teleporting') testing.expectEqualWithDelta(player.position.y, 50, 1, 'incorrect position after teleporting') testing.expectEqualWithDelta(player.position.z, 500, 1, 'incorrect position after teleporting') testing.expectEqualWithDelta(player.rotation:getYaw(), math.rad(90), 0.05, 'incorrect yaw rotation after teleporting') testing.expectEqualWithDelta(player.rotation:getPitch(), math.rad(0), 0.05, 'incorrect pitch rotation after teleporting')
local rotationX1, rotationZ1 = player.rotation:getAnglesXZ() testing.expectEqualWithDelta(rotationX1, math.rad(0), 0.05, 'incorrect x rotation from getAnglesXZ after teleporting') testing.expectEqualWithDelta(rotationZ1, math.rad(90), 0.05, 'incorrect z rotation from getAnglesXZ after teleporting')
local rotationZ2, rotationY2, rotationX2 = player.rotation:getAnglesZYX() testing.expectEqualWithDelta(rotationZ2, math.rad(90), 0.05, 'incorrect z rotation from getAnglesZYX after teleporting') testing.expectEqualWithDelta(rotationY2, math.rad(0), 0.05, 'incorrect y rotation from getAnglesZYX after teleporting') testing.expectEqualWithDelta(rotationX2, math.rad(0), 0.05, 'incorrect x rotation from getAnglesZYX after teleporting')
player:teleport('', player.position, {rotation=util.transform.rotateZ(math.rad(-90)), onGround=true}) coroutine.yield() testing.expectEqualWithDelta(player.rotation:getYaw(), math.rad(-90), 0.05, 'options.rotation is not working') testing.expectLessOrEqual(player.position.z, 400, 'options.onGround is not working')
player:teleport('', util.vector3(50, -100, 0)) coroutine.yield() testing.expect(player.cell.isExterior, 'teleport to exterior failed') testing.expectEqualWithDelta(player.position.x, 50, 1, 'incorrect position after teleporting') testing.expectEqualWithDelta(player.position.y, -100, 1, 'incorrect position after teleporting') testing.expectEqualWithDelta(player.rotation:getYaw(), math.rad(-90), 0.05, 'teleporting changes rotation')end)
testing.registerGlobalTest('getGMST', function() testing.expectEqual(core.getGMST('non-existed gmst'), nil) testing.expectEqual(core.getGMST('Water_RippleFrameCount'), 4) testing.expectEqual(core.getGMST('Inventory_DirectionalDiffuseR'), 0.5) testing.expectEqual(core.getGMST('Level_Up_Level2'), 'something')end)
testing.registerGlobalTest('MWScript', function() local variableStoreCount = 19 local variableStore = world.mwscript.getGlobalVariables(player) testing.expectEqual(variableStoreCount, #variableStore)
variableStore.year = 5 testing.expectEqual(5, variableStore.year) variableStore.year = 1 local indexCheck = 0 for index, value in ipairs(variableStore) do testing.expectEqual(variableStore[index], value) indexCheck = indexCheck + 1 end testing.expectEqual(variableStoreCount, indexCheck) indexCheck = 0 for index, value in pairs(variableStore) do testing.expectEqual(variableStore[index], value) indexCheck = indexCheck + 1 end testing.expectEqual(variableStoreCount, indexCheck)end)
local function testRecordStore(store, storeName, skipPairs) testing.expect(store.records) local firstRecord = store.records[1] if not firstRecord then return end testing.expectEqual(firstRecord.id, store.records[firstRecord.id].id) local status, _ = pcall(function() for index, value in ipairs(store.records) do if value.id == firstRecord.id then testing.expectEqual(index, 1, storeName) break end end end)
testing.expectEqual(status, true, storeName)end
testing.registerGlobalTest('record stores', function() for key, type in pairs(types) do if type.records then testRecordStore(type, key) end end testRecordStore(core.magic.enchantments, "enchantments") testRecordStore(core.magic.effects, "effects", true) testRecordStore(core.magic.spells, "spells")
testRecordStore(core.stats.Attribute, "Attribute") testRecordStore(core.stats.Skill, "Skill")
testRecordStore(core.sound, "sound") testRecordStore(core.factions, "factions") testRecordStore(core.regions, "regions") testRecordStore(core.dialogue.greeting, "dialogue greeting") testRecordStore(core.dialogue.topic, "dialogue topic") testRecordStore(core.dialogue.journal, "dialogue journal") testRecordStore(core.dialogue.persuasion, "dialogue persuasion") testRecordStore(core.dialogue.voice, "dialogue voice")
testRecordStore(types.NPC.classes, "classes") testRecordStore(types.NPC.races, "races") testRecordStore(types.Player.birthSigns, "birthSigns")end)
testing.registerGlobalTest('record creation', function() local newLight = { isCarriable = true, isDynamic = true, isFire =false, isFlicker = false, isFlickerSlow = false, isNegative = false, isOffByDefault = false, isPulse = false, weight = 1, value = 10, duration = 12, radius = 30, color = util.color.hex('123456'), name = "TestLight", model = "meshes/marker_door.dae" } local draft = types.Light.createRecordDraft(newLight) local record = world.createRecord(draft) for key, value in pairs(newLight) do testing.expectEqual(record[key], value) endend)
testing.registerGlobalTest('UTF-8 characters', function() testing.expectEqual(utf8.codepoint("😀"), 0x1F600)
local chars = {}
for codepoint = 0, 0x10FFFF do local char = utf8.char(codepoint) local charSize = string.len(char)
testing.expect(not chars[char], nil, "Duplicate UTF-8 character: " .. char) chars[char] = true
if codepoint <= 0x7F then testing.expectEqual(charSize, 1) elseif codepoint <= 0x7FF then testing.expectEqual(charSize, 2) elseif codepoint <= 0xFFFF then testing.expectEqual(charSize, 3) elseif codepoint <= 0x10FFFF then testing.expectEqual(charSize, 4) end
testing.expectEqual(utf8.codepoint(char), codepoint) testing.expectEqual(utf8.len(char), 1) endend)
testing.registerGlobalTest('UTF-8 strings', function() local utf8str = "Hello, 你好, 🌎!"
local str = "" for utf_char in utf8str:gmatch(utf8.charpattern) do str = str .. utf_char end testing.expectEqual(str, utf8str)
testing.expectEqual(utf8.len(utf8str), 13) testing.expectEqual(utf8.offset(utf8str, 9), 11)end)
testing.registerGlobalTest('memory limit', function() local ok, err = pcall(function() local t = {} local n = 1 while true do t[n] = n n = n + 1 end end) testing.expectEqual(ok, false, 'Script reaching memory limit should fail') testing.expectEqual(err, 'not enough memory')end)
testing.registerGlobalTest('vfs', function() local file = 'test_vfs_dir/lines.txt' local nosuchfile = 'test_vfs_dir/nosuchfile' testing.expectEqual(vfs.fileExists(file), true, 'lines.txt should exist') testing.expectEqual(vfs.fileExists(nosuchfile), false, 'nosuchfile should not exist')
local expectedLines = { '1', '2', '', '4' } local getLine = vfs.lines(file) for _,v in pairs(expectedLines) do testing.expectEqual(getLine(), v) end testing.expectEqual(getLine(), nil, 'All lines should have been read') local ok = pcall(function() vfs.lines(nosuchfile) end) testing.expectEqual(ok, false, 'Should not be able to read lines from nonexistent file')
local getPath = vfs.pathsWithPrefix('test_vfs_dir/') testing.expectEqual(getPath(), file) testing.expectEqual(getPath(), nil, 'All paths should have been read')
local handle = vfs.open(file) testing.expectEqual(vfs.type(handle), 'file', 'File should be open') testing.expectEqual(handle.fileName, file)
local n1, n2, _, l3, l4 = handle:read("*n", "*number", "*l", "*line", "*l") testing.expectEqual(n1, 1) testing.expectEqual(n2, 2) testing.expectEqual(l3, '') testing.expectEqual(l4, '4')
testing.expectEqual(handle:seek('set', 0), 0, 'Reading should happen from the start of the file') testing.expectEqual(handle:read("*a"), '1\n2\n\n4')
testing.expectEqual(handle:close(), true, 'File should be closeable') testing.expectEqual(vfs.type(handle), 'closed file', 'File should be closed')
handle = vfs.open(nosuchfile) testing.expectEqual(handle, nil, 'vfs.open should return nil on nonexistent files')
getLine = vfs.open(file):lines() for _,v in pairs(expectedLines) do testing.expectEqual(getLine(), v) endend)
testing.registerGlobalTest('commit crime', function() local player = initPlayer() testing.expectEqual(player == nil, false, 'A viable player reference should exist to run `commit crime`') testing.expectEqual(I.Crimes == nil, false, 'Crimes interface should be available in global contexts')
-- Reset crime level to have a clean slate types.Player.setCrimeLevel(player, 0) testing.expectEqual(I.Crimes.commitCrime(player, { type = types.Player.OFFENSE_TYPE.Theft, victim = player, arg = 100}).wasCrimeSeen, false, "Running the crime with the player as the victim should not result in a seen crime") testing.expectEqual(I.Crimes.commitCrime(player, { type = types.Player.OFFENSE_TYPE.Theft, arg = 50 }).wasCrimeSeen, false, "Running the crime with no victim and a type shouldn't raise errors") testing.expectEqual(I.Crimes.commitCrime(player, { type = types.Player.OFFENSE_TYPE.Murder }).wasCrimeSeen, false, "Running a murder crime should work even without a victim")
-- Create a mockup target for crimes local victim = world.createObject(types.NPC.record(player).id) victim:teleport(player.cell, player.position + util.vector3(0, 300, 0)) coroutine.yield()
-- Reset crime level for testing with a valid victim types.Player.setCrimeLevel(player, 0) testing.expectEqual(I.Crimes.commitCrime(player, { victim = victim, type = types.Player.OFFENSE_TYPE.Theft, arg = 50 }).wasCrimeSeen, true, "Running a crime with a valid victim should notify them when the player is not sneaking, even if it's not explicitly passed in") testing.expectEqual(types.Player.getCrimeLevel(player), 0, "Crime level should not change if the victim's alarm value is low and there's no other witnesses")end)
testing.registerGlobalTest('record model property', function() local player = world.players[1] testing.expectEqual(types.NPC.record(player).model, 'meshes/basicplayer.dae')end)
testing.registerGlobalTest('player approaches and attacks a creature', function() local player = initPlayer() world.createObject('basic_dagger1h', 1):moveInto(player) testing.runLocalTest(player, 'player approaches and attacks a creature')end)
testing.registerGlobalTest('equipped weapon damages a stationary target', function() local player = initPlayer() local target = world.createObject('landracer') target:addScript('idle.lua') target:teleport(player.cell, player.position + util.vector3(0, 70, 0)) world.createObject('basic_dagger1h', 1):moveInto(player) testing.runLocalTest(player, 'equipped weapon damages a stationary target', target) target:remove()end)
testing.registerGlobalTest('camera.getFocusRay should report the object under the crosshair', function() local player = initPlayer() local target = world.createObject(types.NPC.record(player).id) target:teleport(player.cell, player.position + util.vector3(0, 150, 0), util.transform.rotateZ(math.rad(180))) coroutine.yield() coroutine.yield() -- The spawned NPC dies with the test. local ok, err = pcall(testing.runLocalTest, player, 'camera.getFocusRay should report the object under the crosshair') target:remove() if not ok then error(err, 0) endend)
testing.registerGlobalTestStep('load while teleporting - init player', function() local player = world.players[1] player:teleport('Museum of Wonders', util.vector3(0, -1500, 111), util.transform.rotateZ(math.rad(180)))end)
testing.registerGlobalTestStep('load while teleporting - teleport', function() local player = world.players[1] local landracer = world.createObject('landracer') landracer:teleport(player.cell, player.position + util.vector3(0, 500, 0)) coroutine.yield()
local door = world.getObjectByFormId(core.getFormId('the_hub.omwaddon', 26)) door:activateBy(player) coroutine.yield()
landracer:teleport(player.cell, player.position)end)
testing.registerGlobalTest('world.setGameTimeScale should not accept nan', function() local nan = 0.0 / 0.0 local ok, err = pcall(function() world.setGameTimeScale(nan) end) testing.expectEqual(ok, false) testing.expectEqual(err, 'Value must be a finite number')end)
testing.registerGlobalTest('weather.stormDirection should not accept a vector with a nan component', function() local nan = 0.0 / 0.0 local ok, err = pcall(function() core.weather.records[1].stormDirection = util.vector3(nan, nan, nan) end) testing.expectEqual(ok, false) testing.expectEqual(err, 'Vector must contain finite numbers')end)
testing.registerGlobalTest('mwscript magic interactions', function() local player = world.players[1] local script = world.mwscript.getGlobalScript('OpenMW_Tests', player) testing.expectEqual(script == nil, false, 'Expected mwscript OpenMW_Tests to be active') script.variables.state = 1 local globals = world.mwscript.getGlobalVariables(player); globals.OpenMW_Tests_Failed = 0 while script.variables.state ~= 0 and globals.OpenMW_Tests_Failed == 0 and script.isRunning do coroutine.yield() end testing.expectEqual(script.isRunning, true, 'OpenMW_Tests should not crash') testing.expectEqual(globals.OpenMW_Tests_Failed, 0, 'OpenMW_Tests should run without issue')end)
testing.registerGlobalTest('openmw.util math conventions', function() -- Pins the semantics openmw.util exposes to mods, so any change to the -- underlying C++ math implementation or to the utilpackage bindings -- that alters observable Lua behaviour is caught here rather than -- silently breaking existing mods. See components/lua/utilpackage.cpp.
-- v1 * v2 returns the DOT PRODUCT (a number). Not component-wise. local a = util.vector3(1, 2, 3) local b = util.vector3(4, 5, 6) testing.expectEqual(a * b, 32, 'util.vector3 * util.vector3 must be the dot product') testing.expectEqual(a:dot(b), 32, 'util.vector3:dot must equal *')
-- v:length() returns the MAGNITUDE, not the component count. testing.expectEqualWithDelta(util.vector3(3, 4, 0):length(), 5, 1e-5, 'util.vector3:length is magnitude') testing.expectEqual(util.vector3(3, 4, 0):length2(), 25, 'util.vector3:length2 is magnitude squared')
-- Cross product is exposed as :cross and is right-handed. local c = util.vector3(1, 0, 0):cross(util.vector3(0, 1, 0)) testing.expectEqualWithDelta(c.x, 0, 1e-5, 'cross x') testing.expectEqualWithDelta(c.y, 0, 1e-5, 'cross y') testing.expectEqualWithDelta(c.z, 1, 1e-5, 'cross z (right-handed)')
-- Vec2 * Vec2 is also the dot product. testing.expectEqual(util.vector2(1, 2) * util.vector2(3, 4), 11, 'util.vector2 * util.vector2 must be the dot product')
-- Transform composition applies in REVERSE order (right-to-left). -- Documented at files/lua_api/openmw/util.lua:608-609. local rotPi = util.transform.rotateZ(math.pi) local move10x = util.transform.move(util.vector3(10, 0, 0)) local v = util.vector3(1, 0, 0) -- (rotPi * move10x) applied to v: move10x first (v -> (11,0,0)), then rotPi (x flips -> -11). local moveThenRotate = (rotPi * move10x) * v testing.expectEqualWithDelta(moveThenRotate.x, -11, 1e-5, 'composition applies right operand first') -- (move10x * rotPi) applied to v: rotPi first ((1,0,0) -> (-1,0,0)), then move10x (+10 -> 9). local rotateThenMove = (move10x * rotPi) * v testing.expectEqualWithDelta(rotateThenMove.x, 9, 1e-5, 'composition applies right operand first (reverse case)')
-- Transform:inverse round-trip. local roundTrip = rotPi:inverse() * (rotPi * v) testing.expectEqualWithDelta(roundTrip.x, 1, 1e-5, 'inverse round-trip x') testing.expectEqualWithDelta(roundTrip.y, 0, 1e-5, 'inverse round-trip y')end)
testing.registerGlobalTest('load script generated static', function() local record = types.Static.records.OMW_Generated_Static testing.expectNotEqual(record, nil, 'OMW_Generated_Static should have been generated') testing.expectEqual(record.model, 'meshes/generatedonload.nif')end)
testing.registerGlobalTest('load script generated apparatuses', function() local apparatus = types.Apparatus.records.OMW_Generated_Apparatus testing.expectNotEqual(apparatus, nil) testing.expectEqual(apparatus.name, 'Modified apparatus') testing.expectEqual(apparatus.model, 'meshes/a/modified.nif') testing.expectEqual(apparatus.icon, 'icons/a/modified.dds') testing.expectEqual(apparatus.mwscript, nil) testing.expectEqual(apparatus.type, types.Apparatus.TYPE.Calcinator) testing.expectEqual(apparatus.quality, 10) testing.expectEqual(apparatus.weight, 5.5) testing.expectEqual(apparatus.value, 450)
local templated = types.Apparatus.records.OMW_Templated_Apparatus testing.expectEqual(templated.name, 'Templated apparatus') testing.expectEqual(templated.quality, apparatus.quality) local copied = types.Apparatus.records.OMW_Copied_Apparatus testing.expectEqual(copied.name, apparatus.name) testing.expectEqual(copied.type, apparatus.type) testing.expectEqual(types.Apparatus.records.OMW_Temporary_Apparatus, nil) testing.expectEqual(types.Apparatus.records.OMW_Invalid_Apparatus, nil)end)
return { engineHandlers = { onUpdate = testing.updateGlobal, }, eventHandlers = testing.globalEventHandlers,}