diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e4fbcb7248e..fdc9c19c07a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -461,6 +461,7 @@ Ubuntu_Clang_tests_Debug: stage: test variables: PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" + EXAMPLE_SUITE_REVISION: f51b832e033429a7cdc520e0e48d7dfdb9141caa cache: paths: - .cache/pip diff --git a/CI/run_integration_tests.sh b/CI/run_integration_tests.sh index d7b025df522..e79408926a1 100755 --- a/CI/run_integration_tests.sh +++ b/CI/run_integration_tests.sh @@ -1,6 +1,12 @@ #!/bin/bash -ex -git clone --depth=1 https://gitlab.com/OpenMW/example-suite.git +mkdir example-suite +cd example-suite +git init +git remote add origin https://gitlab.com/OpenMW/example-suite.git +git fetch --depth=1 origin ${EXAMPLE_SUITE_REVISION} +git checkout FETCH_HEAD +cd .. xvfb-run --auto-servernum --server-args='-screen 0 640x480x24x60' \ scripts/integration_tests.py --omw build/install/bin/openmw --workdir integration_tests_output example-suite/ diff --git a/scripts/data/integration_tests/test_lua_api/player.lua b/scripts/data/integration_tests/test_lua_api/player.lua index 1da817e4958..a9c2002263f 100644 --- a/scripts/data/integration_tests/test_lua_api/player.lua +++ b/scripts/data/integration_tests/test_lua_api/player.lua @@ -5,7 +5,9 @@ local core = require('openmw.core') local input = require('openmw.input') local types = require('openmw.types') local nearby = require('openmw.nearby') +local camera = require('openmw.camera') +types.Player.setControlSwitch(self, types.Player.CONTROL_SWITCH.Controls, false) types.Player.setControlSwitch(self, types.Player.CONTROL_SWITCH.Fighting, false) types.Player.setControlSwitch(self, types.Player.CONTROL_SWITCH.Jumping, false) types.Player.setControlSwitch(self, types.Player.CONTROL_SWITCH.Looking, false) @@ -159,8 +161,8 @@ testing.registerLocalTest('playerDiagonalWalking', testing.registerLocalTest('findPath', function() - local src = util.vector3(4096, 4096, 867.237) - local dst = util.vector3(4500, 4500, 700.216) + local src = util.vector3(4096, 4096, 1745) + local dst = util.vector3(4500, 4500, 1745.95263671875) local options = { agentBounds = types.Actor.getPathfindingAgentBounds(self), includeFlags = nearby.NAVIGATOR_FLAGS.Walk + nearby.NAVIGATOR_FLAGS.Swim, @@ -180,7 +182,7 @@ testing.registerLocalTest('findPath', testing.registerLocalTest('findRandomPointAroundCircle', function() - local position = util.vector3(4096, 4096, 867.237) + local position = util.vector3(4096, 4096, 1745.95263671875) local maxRadius = 100 local options = { agentBounds = types.Actor.getPathfindingAgentBounds(self), @@ -193,8 +195,8 @@ testing.registerLocalTest('findRandomPointAroundCircle', testing.registerLocalTest('castNavigationRay', function() - local src = util.vector3(4096, 4096, 867.237) - local dst = util.vector3(4500, 4500, 700.216) + local src = util.vector3(4096, 4096, 1745) + local dst = util.vector3(4500, 4500, 1745.95263671875) local options = { agentBounds = types.Actor.getPathfindingAgentBounds(self), includeFlags = nearby.NAVIGATOR_FLAGS.Walk + nearby.NAVIGATOR_FLAGS.Swim, @@ -206,14 +208,14 @@ testing.registerLocalTest('castNavigationRay', testing.registerLocalTest('findNearestNavMeshPosition', function() - local position = util.vector3(4096, 4096, 1000) + local position = util.vector3(4096, 4096, 2000) local options = { agentBounds = types.Actor.getPathfindingAgentBounds(self), includeFlags = nearby.NAVIGATOR_FLAGS.Walk + nearby.NAVIGATOR_FLAGS.Swim, searchAreaHalfExtents = util.vector3(1000, 1000, 1000), } local result = nearby.findNearestNavMeshPosition(position, options) - local expected = util.vector3(4096, 4096, 872.674) + local expected = util.vector3(4096, 4096, 1746.27099609375) testing.expectLessOrEqual((result - expected):length(), 1, 'Navigation mesh position ' .. testing.formatActualExpected(result, expected)) end) @@ -230,6 +232,115 @@ testing.registerLocalTest('playerMemoryLimit', testing.expectEqual(err, 'not enough memory') end) +testing.registerLocalTest('playerWeaponAttack', + function() + camera.setMode(camera.MODE.ThirdPerson) + + local options = { + agentBounds = types.Actor.getPathfindingAgentBounds(self), + } + local duration = 10 + local endTime = core.getSimulationTime() + duration + local nextTime = 0 + local use = self.ATTACK_TYPE.NoAttack + + local attributes = {} + for k, v in pairs(types.Actor.stats.attributes) do + attributes[k] = v(self).base + end + + types.Actor.stats.attributes.speed(self).base = 100 + types.Actor.stats.attributes.strength(self).base = 1000 + types.Actor.stats.attributes.agility(self).base = 1000 + + local weaponId = 'basic_dagger1h' + local weapon = types.Actor.inventory(self):find(weaponId) + + local isWeapon = function(actual) + if actual == nil then + return weaponId .. ' is not found' + end + if actual.recordId ~= weaponId then + return 'found weapon recordId does not match expected: actual=' .. tostring(actual.id) + .. ', expected=' .. weaponId + end + return '' + end + testing.expectThat(weapon, isWeapon) + + types.Actor.setEquipment(self, {[types.Actor.EQUIPMENT_SLOT.CarriedRight] = weapon}) + + coroutine.yield() + + testing.expectThat(types.Actor.getEquipment(self, types.Actor.EQUIPMENT_SLOT.CarriedRight), isWeapon) + + types.Actor.setStance(self, types.Actor.STANCE.Weapon) + + local previousHealth = nil + local targetActor = nil + + while true do + local time = core.getSimulationTime() + testing.expectLessOrEqual(time, endTime, 'Did not damage any targets in ' .. duration .. ' seconds') + + if targetActor ~= nil and types.Actor.stats.dynamic.health(targetActor).current < previousHealth then + print('Dealt ' .. (previousHealth - types.Actor.stats.dynamic.health(targetActor).current) .. ' damage to ' .. tostring(targetActor)) + break + end + + local minDistance = nil + for i, actor in ipairs(nearby.actors) do + if actor.id ~= self.id then + local distance = (actor.position - self.position):length() + if minDistance == nil or minDistance > distance then + minDistance = distance + targetActor = actor + end + end + end + testing.expectNotEqual(targetActor, nil, 'No attack targets found') + + previousHealth = types.Actor.stats.dynamic.health(targetActor).current + + local destination = nil + if minDistance > 100 then + local status, path = nearby.findPath(self.position, targetActor.position, options) + + testing.expectEqual(status, nearby.FIND_PATH_STATUS.Success, + 'Failed to find path from ' .. tostring(self.position) .. ' to ' .. tostring(targetActor.position)) + + destination = path[2] + use = self.ATTACK_TYPE.NoAttack + + self.controls.run = true + self.controls.movement = 1 + else + destination = targetActor.position + + if nextTime < time then + if use == 0 then + use = self.ATTACK_TYPE.Any + nextTime = time + 0.5 + else + use = self.ATTACK_TYPE.NoAttack + end + end + end + self.controls.use = use + + local direction = destination - self.position + direction = direction:normalize() + self.controls.yawChange = util.normalizeAngle(math.atan2(direction.x, direction.y) - self.rotation:getYaw()) + self.controls.pitchChange = util.normalizeAngle(math.asin(util.clamp(-direction.z, -1, 1)) - self.rotation:getPitch()) + + coroutine.yield() + end + + for k, v in pairs(types.Actor.stats.attributes) do + v(self).base = attributes[k] + end + end) + return { engineHandlers = { onFrame = testing.updateLocal, diff --git a/scripts/data/integration_tests/test_lua_api/test.lua b/scripts/data/integration_tests/test_lua_api/test.lua index 86fb8e904c3..fe4cd1d24f3 100644 --- a/scripts/data/integration_tests/test_lua_api/test.lua +++ b/scripts/data/integration_tests/test_lua_api/test.lua @@ -220,7 +220,7 @@ local function testMemoryLimit() end local function initPlayer() - player:teleport('', util.vector3(4096, 4096, 867.237), util.transform.identity) + player:teleport('', util.vector3(4096, 4096, 1745), util.transform.identity) coroutine.yield() end @@ -277,7 +277,12 @@ tests = { {'playerMemoryLimit', function() initPlayer() testing.runLocalTest(player, 'playerMemoryLimit') - end} + end}, + {'player with equipped weapon on attack should damage health of other actors', function() + initPlayer() + world.createObject('basic_dagger1h', 1):moveInto(player) + testing.runLocalTest(player, 'playerWeaponAttack') + end}, } return { diff --git a/scripts/data/integration_tests/testing_util/testing_util.lua b/scripts/data/integration_tests/testing_util/testing_util.lua index 2fa18920379..7b886636ed8 100644 --- a/scripts/data/integration_tests/testing_util/testing_util.lua +++ b/scripts/data/integration_tests/testing_util/testing_util.lua @@ -81,6 +81,12 @@ function M.expectEqual(v1, v2, msg) end end +function M.expectNotEqual(v1, v2, msg) + if v1 == v2 then + error(string.format('%s: %s == %s', msg or '', v1, v2), 2) + end +end + function M.closeToVector(expected, maxDistance) return function(actual) local distance = (expected - actual):length() diff --git a/scripts/integration_tests.py b/scripts/integration_tests.py index 4d63c0f2dd6..17f89fbe2f2 100755 --- a/scripts/integration_tests.py +++ b/scripts/integration_tests.py @@ -17,11 +17,15 @@ args = parser.parse_args() example_suite_dir = Path(args.example_suite).resolve() -example_suite_content = example_suite_dir / "game_template" / "data" / "template.omwgame" -if not example_suite_content.is_file(): - sys.exit( - f"{example_suite_content} not found, use 'git clone https://gitlab.com/OpenMW/example-suite/' to get it" - ) + +content_paths = ( + example_suite_dir / "game_template" / "data" / "template.omwgame", + example_suite_dir / "example_animated_creature" / "data" / "landracer.omwaddon", + example_suite_dir / "the_hub" / "data" / "the_hub.omwaddon", +) +for path in content_paths: + if not path.is_file(): + sys.exit(f"{path} is not found, use 'git clone https://gitlab.com/OpenMW/example-suite/' to get it") openmw_binary = Path(args.omw).resolve() if not openmw_binary.is_file(): @@ -43,15 +47,17 @@ def runTest(name): shutil.copyfile(example_suite_dir / "settings.cfg", config_dir / "settings.cfg") test_dir = tests_dir / name with open(config_dir / "openmw.cfg", "w", encoding="utf-8") as omw_cfg: + for path in content_paths: + omw_cfg.write(f'data="{path.parent}"\n') omw_cfg.writelines( ( - f'data="{example_suite_dir}{os.sep}game_template{os.sep}data"\n', f'data="{testing_util_dir}"\n', f'data-local="{test_dir}"\n', f'user-data="{userdata_dir}"\n', - "content=template.omwgame\n", ) ) + for path in content_paths: + omw_cfg.write(f'content={path.name}\n') if (test_dir / "openmw.cfg").exists(): omw_cfg.write(open(test_dir / "openmw.cfg").read()) elif (test_dir / "test.omwscripts").exists(): @@ -124,6 +130,7 @@ def runTest(name): if entry.is_dir(): if not runTest(entry.name): status = -1 -shutil.rmtree(config_dir, ignore_errors=True) -shutil.rmtree(userdata_dir, ignore_errors=True) +if status == 0: + shutil.rmtree(config_dir, ignore_errors=True) + shutil.rmtree(userdata_dir, ignore_errors=True) exit(status)