GameTests.BuildingButtons = nil config.EditorsToTest = false and { "AutoAttachPreset", "TextStyle","XTemplate", "ClassDef", "ParticleSystemPreset", "MapDataPreset", "Achievement", "FXPreset","SoundPreset", "LootDef", "ConstDef", "Camera", "BanterDef", "CampaignPreset", "Conversation", "EnemySquads", "PopupNotification", "QuestsDef", "ConflictDescription", "SectorOperation", "SetpiecePrg", "AppearancePreset", "InventoryItemCompositeDef", "UnitDataCompositeDef", "AIArchetype", "ActionCameraDef", "ChanceToHitModifier", "CharacterEffectCompositeDef", } g_UIGetBuildingsList = function() return {} end GameTestsNightly.NonInferedShaders = nil GameTestsNightly.ReferenceImages = nil GameTestsNightly.RenderingBenchmark = nil --[[ function GameTestsNightly.CopyDailyTests(time_start_up) GameTestsPrintf("Night Watch:Zulu daily tests for now...") --g_UIAutoTestButtonsMap = "_alt1" for test, func in sorted_pairs(GameTests) do func(time_start_up) end end ]] function GameTests_LoadTestSaves() local err, savegames = AsyncListFiles("svnAssets/Source/TestSaves/", "*.savegame.sav") if err then GameTestsPrint("Error while getting test savegames: %s", err) end table.sort(savegames) local test_time = 1500 local max_time = 0 for _, savegame in ipairs(savegames) do local _, file, ext = SplitPath(savegame) if not string.match(file, "^%[TS 4%]") then GameTestsPrintf("Loading savegame: %s", savegame) local start_time = GetPreciseTicks() local err = LoadGame(file .. ext) if not err then GameTestsPrintf("Testing savegame '%s' for %.1fs", savegame, test_time * 0.001) WaitLoadingScreenClose() local end_time = GetPreciseTicks() + test_time while GetPreciseTicks() <= end_time do Sleep(1) end local time = GetPreciseTicks() - start_time max_time = max_time <= time and time or max_time else GameTestsErrorf("Error loading savegame: '%s'", savegame) end end end GameTestsPrintf("Longest load save took: %.1fs", max_time * 0.001) end function GetMapRevision(map_id) local err, list = AsyncListFiles("Maps/" .. map_id) if err then return 0 end local last_rev = 0 for _, file in ipairs(list) do local rev = GetAssetFileRevision(file) if rev > last_rev and rev ~= 999999999 then last_rev = rev end end return last_rev end ChangeGameplayMaps_LastChangedToTest = 6 function ViewRandomMapPositions(duration) local border_areas = MapGetMarkers("BorderArea") local bam = border_areas and border_areas[1] if bam then local end_time = GetPreciseTicks() + (duration or 1500) for _, pos in random_ipairs(bam:GetAreaPositions(), 0) do ViewPos(point(point_unpack(pos))) WaitNextFrame() if GetPreciseTicks() > end_time then break end end end end ChangeVideoSettings_ViewPositions = ViewRandomMapPositions function GetMapsToTest(game_tests_name) local campaign_maps = {} for _, map in pairs(MapData) do if map.Status ~= "Not started" then campaign_maps[#campaign_maps+1] = { map.id, GetMapRevision(map.id) } end end table.sortby_field_descending(campaign_maps, 2) local maps_to_test = (game_tests_name == "GameTestsNightly") and #campaign_maps or ChangeGameplayMaps_LastChangedToTest while #campaign_maps > maps_to_test and campaign_maps[#campaign_maps][2] ~= campaign_maps[1][2] do table.remove(campaign_maps) end return campaign_maps end function GameTests.ChangeGameplayMaps(_, game_tests_name) GameTestMapLoadRandom = xxhash("GameTestMapLoadRandomSeed") MapLoadRandom = InitMapLoadRandom() ResetInteractionRand(0) -- same reset at map game time 0 to get control values for interaction rand results local maps_to_test = GetMapsToTest(game_tests_name) for _, map_descr in ipairs(maps_to_test) do local map = MapData[map_descr[1]] ClearErrorSource() GameTestsPrint(map.id) local time = GetPreciseTicks() ChangeMap(map.id) -- PostNewMapLoaded will emit ValidateMap and show VMEs when in dev mode GameTestsPrint("... changing map took " .. (GetPreciseTicks() - time) .. " ms") ValidateMapObjects({ validate_properties = true }) WaitLoadingScreenClose() GameTestsPrint("... changing map and validation took " .. (GetPreciseTicks() - time) .. " ms") ViewRandomMapPositions() GameTestsFlushErrors() end end GameTestsNightly.ChangeGameplayMaps = GameTests.ChangeGameplayMaps GameTestsNightly.TestMapEntranceMarkersAL = GameTests.TestMapEntranceMarkersAL function GameTests.Ladders() ChangeMap("__CombatTest") if not HasGameSession() then NewGameSession() end local tf = GetTimeFactor() SetTimeFactor(10000) InitTestCombat(false, { TestTeamDef:new{ mercs = { "Barry" }, team_color = RGB(0, 0, 200), spawn_marker_group = "TestTeamA", side = "player1" }, TestTeamDef:new{ mercs = { "Grizzly" }, team_color = RGB(200, 0, 0), spawn_marker_group = "TestTeamB", side = "enemy1" }, }) g_Combat:Start() while GameTime() == 0 do Sleep(10) end local function exec_action(action_id, unit, ...) if g_Combat then CombatActions[action_id]:Execute({unit}, ...) --Sleep(100) while #SyncEventsQueue > 0 do WaitMsg("SyncEventsProcessed", 50) end while IsValidTarget(unit) and not unit:IsIdleCommand() do WaitMsg("Idle", 200) end end end local ladder = MapGetFirst(true, "Ladder", function(ladder) return ladder.LadderParts > 0 end) if not ladder then GameTestsErrorf("Ladders not present on '%s' map", GetMapName()) return end local x1, y1, z1, x2, y2, z2 = ladder:GetTunnelPositions() local pos1 = point(x1, y1, z1) local pos2 = point(x2, y2, z2) local Barry = g_Units.Barry local Grizzly = g_Units.Grizzly while not Barry:CanBeControlled() do Sleep(10) end Barry:ForEachItemInSlot("Handheld A", false, function(item) Barry:RemoveItem("Handheld A", item) end) local maxPoints = Barry:GetMaxActionPoints() Barry.GetMaxActionPoints = function () return maxPoints * 3 end Barry.ActionPoints = Barry:GetMaxActionPoints() Barry:SetPos(pos1) SnapCameraToObj(Barry) Grizzly:SetPos(pos1 + point(const.SlabSizeX, const.SlabSizeY)) exec_action("Move", Barry, { goto_pos = pos2 }) if pos2:Dist(Barry:GetPos()) > 2 * guim / 10 then GameTestsErrorf("Climbing ladder up error") end exec_action("Move", Barry, { goto_pos = pos1 }) if pos1:Dist(Barry:GetPos()) > 2 * guim / 10 then GameTestsErrorf("Climbing ladder down error") end SetTimeFactor(tf) end local mat_props_names = {"Body", "Head", "Hat", "Pants", "Shirt"} function TestEntityMaterialsUnitLighting() for _, group in ipairs(Presets.AppearancePreset) do for _, appearance in ipairs(group) do for _, mat_name in ipairs(mat_props_names) do local mat_name_value = appearance[mat_name] if mat_name_value and mat_name_value ~= "" then local filename = string.format("%s_mesh.mtl", mat_name_value) if io.exists("Materials/" .. filename) then local mat_props = GetMaterialProperties(filename) if mat_props.UnitLighting == 0 then local err_text = string.format("Unit Appearance material '%s' should have UnitLighting flag", filename) GameTestsErrorf(err_text) print(err_text) end end end end end end for _, group in ipairs(Presets.WeaponComponent) do for _, weapon_component in ipairs(group) do for _, visual in ipairs(weapon_component.Visuals) do local filename = string.format("%s_mesh.mtl", visual.Entity) if io.exists("Materials/" .. filename) then local mat_props = GetMaterialProperties(filename) if mat_props.UnitLighting == 0 then local err_text = string.format("Weapon material '%s' should have UnitLighting flag", filename) GameTestsErrorf(err_text) print(err_text) end end end end end end function GameTests.EntityMaterials() GameTests_LoadAnyMap() local all_entities = GetAllEntities() local materials = Presets.ObjMaterial.Default for entity_name in pairs(all_entities) do local entity_data = EntityData[entity_name] if entity_data and entity_data.entity then local material_type = entity_data.entity.material_type local material_preset = materials[material_type] if material_preset and entity_data.editor_category ~= "Decal" then if material_preset.impenetrable and HasAnySurfaces(entity_name, ~0) then local has_collision = HasCollisions(entity_name) local has_obstruction = HasMeshWithCollisionMask(entity_name, const.cmObstruction) if not (has_collision or has_obstruction) then local err_text = string.format("Entity '%s' has impenetrable material '%s' without S collision mask/surfaces", entity_name, material_type) GameTestsErrorf(err_text) end end end end end TestEntityMaterialsUnitLighting() end function GameTests.EntityMissingFiles() GameTests_LoadAnyMap() local all_entities = GetAllEntities() for entity in sorted_pairs(all_entities) do local existing, non_existing = EntitySpec.GetEntityFiles(nil, entity) for _, ne in ipairs(non_existing) do if not string.match(ne, ".json$") then GameTestsErrorf("%s: missing %s", entity, ne) end end end end function GameTests.Interactables() print("Entering interaction test...") NewGameSession() local interactionTest = Presets.TestCombat.GameTest.InteractionTest TestCombatEnterSector(interactionTest, interactionTest.map) WaitLoadingScreenClose() wait_interface_mode("IModeExploration") local unitsToDeploy = GetAllPlayerUnitsOnMap() local unit = unitsToDeploy[1] local function lGetNearbyInteractable(obj) return MapGetFirst(obj:GetPos(), guim * 5, "Interactable") end local impassable = MapGetMarkers("Position", "InteractableOnImpassable")[1] local interactable = lGetNearbyInteractable(impassable) unit:SetPos(impassable:GetPos()) UIInteractWith(unit, interactable) Sleep(500) CloseDialog("FullscreenGameDialogs") -- all test interactables are lootables if not interactable.interaction_log or interactable.interaction_log == 0 then GameTestsError("Cant interact with interactable on impassable.") end local behindWall = MapGetMarkers("Position", "InteractableBehindWall")[1] interactable = lGetNearbyInteractable(behindWall) unit:SetPos(behindWall:GetPos()) UIInteractWith(unit, interactable) Sleep(500) while unit.goto_target do Sleep(100) end Sleep(1000) CloseDialog("FullscreenGameDialogs") if not interactable.interaction_log or interactable.interaction_log == 0 then GameTestsError("Cant interact with interactable behind wall.") end if unit:GetPos() == behindWall:GetPos() then GameTestsError("Interacted through a wall.") end end function GameTests.TestSaveLoadSystem() do return end GameTests_LoadAnyMap() print("Starting new game...") CreateRealTimeThread(QuickStartCampaign) WaitMsg("EnterSector", 5000) WaitLoadingScreenClose() local oldOpenPopup = OpenPopupNotification OpenPopupNotification = empty_func local intro = GetDialog("Intro") if intro then intro:Close() WaitMsg("Resume", 2000) end while IsSetpiecePlaying() do print("Waiting for setpiece...") WaitMsg("SetpieceEnded", 100) end Sleep(500) --Setpiece fade in happens after msg for some reason.. -- Pause the game to ensure unit properties dont change due to game logic Pause("g_TestingSaveLoadSystem") -- sat view won't open with PauseGame cuz it uses threads, but this pause does not persist through load g_TestingSaveLoadSystem = true local function finally(err) if err then GameTestsErrorf(err) end if OpenPopupNotification == empty_func then OpenPopupNotification = oldOpenPopup end g_TestingSaveLoadSystem = false table.restore(hr, "g_TestingSaveLoadSystem", "ignore_error") ResumeGame() Resume("g_TestingSaveLoadSystem") print("Done!") end print("Grabbing unit info...") local units = GetAllPlayerUnitsOnMap() local propertiesPreSave = {} local unitPropertiesProperties = UnitProperties:GetProperties() for i, u in ipairs(units) do local unitTable = {} for i = 1, #unitPropertiesProperties do local prop_meta = unitPropertiesProperties[i] if prop_meta.dont_save then goto continue end local prop_id = prop_meta.id local value = u:GetProperty(prop_id) local default = u:GetDefaultPropertyValue(prop_id, prop_meta) local is_default = true if type(value) == "table" and type(default) == "table" then is_default = value == default or table.hash(value) == table.hash(default) else is_default = value == nil or value == default end if is_default then unitTable[prop_id] = "~~default~~" elseif type(value) == "table" then unitTable[prop_id] = ValueToLuaCode(value) else unitTable[prop_id] = value end ::continue:: end propertiesPreSave[u.session_id] = unitTable end local function lComparePropertiesWithPreSave() local units = GetAllPlayerUnitsOnMap() for i, u in ipairs(units) do local unitTable = propertiesPreSave[u.session_id] -- What if a new unit appeared? if unitTable then for i = 1, #unitPropertiesProperties do local prop_meta = unitPropertiesProperties[i] if prop_meta.dont_save then goto continue end local prop_id = prop_meta.id local value = u:GetProperty(prop_id) local default = u:GetDefaultPropertyValue(prop_id, prop_meta) local is_default = true if type(value) == "table" and type(default) == "table" then is_default = value == default or table.hash(value) == table.hash(default) else is_default = value == nil or value == default end if is_default then value = "~~default~~" elseif type(value) == "table" then value = ValueToLuaCode(value) end local preSaveVal = unitTable[prop_id] if preSaveVal ~= nil and preSaveVal ~= value then GameTestsErrorf("Property %s of unit %s changed its value from %s to %s", prop_id, u.session_id, preSaveVal, value) end ::continue:: end end end end local function lReallyWaitForLSToClose() --SatelliteToggleActionRun will refuse to do work if ls is up local timeout = 5000 local ts = GetPreciseTicks() while GetLoadingScreenDialog() do if GetPreciseTicks() - ts > timeout then break end print("waiting for ls to close...") Sleep(100) end end print("Opening satellite view...") if SatelliteToggleActionState() ~= "enabled" then finally("Satellite view is disabled!") return end lReallyWaitForLSToClose() local err = SatelliteToggleActionRun() if not err then WaitMsg("OpenSatelliteView", 10000) end if not gv_SatelliteView then finally("Couldn't open satellite view, " .. (err or "")) return end print("Closing satellite view...") lReallyWaitForLSToClose() err = SatelliteToggleActionRun() if not err then WaitMsg("CloseSatelliteView", 10000) end if gv_SatelliteView then finally("Couldn't close satellite view, " .. (err or "")) return end print("Checking if satellite sync changed unit properties...") lComparePropertiesWithPreSave() OpenPopupNotification = oldOpenPopup local function lResetVariablesExpectedToChange() InteractionSeed = 0 InteractionSeeds = {} MapLoadRandom = 0 Game.loaded_from_id = false end -- Persists through load! PauseGame() --use this pause for save/load testing cuz the other one does not persist; table.change(hr, "g_TestingSaveLoadSystem", {CameraTacMouseEdgeScrolling = false, CameraTacClampToTerrain = false}) print("Saving game and loading it...") lResetVariablesExpectedToChange() local saveOne = GatherSessionData():str() LoadGameSessionData(saveOne) local filePath = "AppData/TestSave.lua" local err = AsyncStringToFile(filePath, saveOne) if err then printf("Failed to save to file %s: %s", filePath, err) end print("Checking if loading the save changed unit properties...") lComparePropertiesWithPreSave() print("Saving game again, and checking difff...") local filePathTwo = "AppData/TestSaveTwo.lua" lResetVariablesExpectedToChange() local saveTwo = GatherSessionData():str() err = AsyncStringToFile(filePathTwo, saveTwo) if err then printf("Failed to save to file %s: %s", filePathTwo, err) end local _, str = SVNDiffTwoUnrelatedFiles(filePath, filePathTwo) local diff = {} for s in str:gmatch("[^\r\n]+") do diff[#diff+1] = s if #diff == 20 then break end end if #diff > 0 then GameTestsError("Second save is different from the first?!") GameTestsPrint(table.concat(diff, "\n")) end finally() print("Success!") end function GameTests_LandmineLOF() NewGameSession() -- combat test will try to keep the existing squads, prevent this local retreatTest = Presets.TestCombat.GameTest.RetreatTest TestCombatEnterSector(retreatTest, retreatTest.map) WaitLoadingScreenClose() wait_interface_mode("IModeDeployment") local dlg = GetInGameInterfaceModeDlg() dlg:StartExploration() WaitMsg("ExplorationTick", 10000) local landmine = PlaceObject("Landmine") landmine:SetPos(point(198499, 117922, 6950)) landmine.discovered_by["player1"] = true local kalyna = g_Units.Kalyna local defaultAction = kalyna:GetDefaultAttackAction("ranged") local results = defaultAction:GetActionResults(kalyna, { target = landmine }) if not results.target_hit then GameTestsErrorf("LOF didn't hit landmine!") end end function GameTests.DeploymentLogic() print("Entering deployment combat test...") NewGameSession() local deploymentTest = Presets.TestCombat.GameTest.DeploymentTest TestCombatEnterSector(deploymentTest, deploymentTest.map) WaitLoadingScreenClose() wait_interface_mode("IModeDeployment") if not gv_Deployment then GameTestsErrorf("Deployment didn't start!") return end local allTheMarkers = MapGetMarkers() local cliff = table.find_value(allTheMarkers, "ID", "Cliff") local raised = table.find_value(allTheMarkers, "ID", "Raised") local flat = table.find_value(allTheMarkers, "ID", "Flat") local house = table.find_value(allTheMarkers, "ID", "House") local beach = table.find_value(allTheMarkers, "ID", "Beach") local raisedTerrain = table.find_value(allTheMarkers, "ID", "RaisedTerrain") local markerInHouse = table.find_value(allTheMarkers, "ID", "InBuilding") assert(cliff and raised and flat and house and beach and raisedTerrain) local unitsToDeploy = GetCurrentDeploymentSquadUnits() assert(#unitsToDeploy == 4) ResetInteractionRand(0) -- Deployment uses randomization, reset to ensure the test is deterministic. local function CheckDeploymentAllCool() local highestZ = -9999999999 local lowestZ = 9999999999 for i, u in ipairs(unitsToDeploy) do assert(u.visible, u.session_id) -- Did this unit get deployed? local _, _, vZ = WorldToVoxel(u) highestZ = Max(highestZ, vZ) lowestZ = Min(lowestZ, vZ) end if abs(highestZ - lowestZ) > 1 then GameTestsErrorf("Units were deployed very far apart on the z axis, possibly to an invalid pos.") return false end return true end -- Test cliff deployment. Half the cliff is on the terrain, the other is on a rock object. -- Common problems: -- 1. Units going outside of area because it is too thin -- 2. Units deploying inside the cliff face SnapCameraToObj(cliff, true) Sleep(300) local pointOnTerrainPart = point(172200, 132600) for i = 1, 5 do LocalDeployUnitsOnMarker(unitsToDeploy, cliff, "show", pointOnTerrainPart) if not CheckDeploymentAllCool() then return end end local pointOnObjectPart = point(168600, 131400, 15400) for i = 1, 5 do LocalDeployUnitsOnMarker(unitsToDeploy, cliff, "show", pointOnObjectPart) if not CheckDeploymentAllCool() then return end end -- Test raised deployment. The marker is high above the ground on a slab. A small part of the area is on a lower z. -- Common problems: -- 1. Portion of the area is unclickable. -- 2. Units deploying on the ground below -- 3. Units not being able to deploy on the stairs down the marker SnapCameraToObj(raised, true) Sleep(300) local OnArea = point(126600, 136200, 13300) for i = 1, 5 do LocalDeployUnitsOnMarker(unitsToDeploy, raised, "show", OnArea) if not CheckDeploymentAllCool() then return end end local downTheStairs = point(126600, 131400, 12600) for i = 1, 5 do LocalDeployUnitsOnMarker(unitsToDeploy, raised, "show", downTheStairs) if not CheckDeploymentAllCool() then return end assert(unitsToDeploy[1]:GetPos() == downTheStairs) end local inTheVoid = point(124200, 137400) if raised:IsInsideArea(inTheVoid) then GameTestsErrorf("The point below the marker shouldn't be considered inside it.") return end -- Test flat deployment. Nothing special here, just in case. SnapCameraToObj(flat, true) Sleep(300) local partOne = point(127800, 177000) for i = 1, 5 do LocalDeployUnitsOnMarker(unitsToDeploy, flat, "show", partOne) if not CheckDeploymentAllCool() then return end end local partTwo = point(124200, 163800) for i = 1, 5 do LocalDeployUnitsOnMarker(unitsToDeploy, flat, "show", partTwo) if not CheckDeploymentAllCool() then return end end -- Test house deployment. -- Deploying the mercs in the house should deploy the other mercs around it, rather than far away outside. SnapCameraToObj(house, true) Sleep(300) local insideHouse = point(166200, 177000, 7000) for i = 1, 5 do LocalDeployUnitsOnMarker(unitsToDeploy, house, "show", insideHouse) if not CheckDeploymentAllCool() then return end for i, u in ipairs(unitsToDeploy) do local unitPos = u:GetPos() assert(unitPos:IsValidZ()) -- Not on the terrain outside, but inside the house. end end local outsideHouse = point(175800, 178200) for i = 1, 5 do LocalDeployUnitsOnMarker(unitsToDeploy, house, "show", outsideHouse) if not CheckDeploymentAllCool() then return end end -- Beach deployment. -- This is used at the start of the game and is the only time you'll find deployment on a "Reachable Only" marker beach.Reachable = true SnapCameraToObj(beach, true) Sleep(300) local onTheBeach = point(178200, 157800) for i = 1, 5 do LocalDeployUnitsOnMarker(unitsToDeploy, beach, "show", onTheBeach) if not CheckDeploymentAllCool() then return end end -- Raised terrain deployment -- Voxels on different Z levels but both on the terrain. SnapCameraToObj(raisedTerrain, true) local lowArea = point(185400, 108600) for i = 1, 5 do LocalDeployUnitsOnMarker(unitsToDeploy, raisedTerrain, "show", lowArea) if not CheckDeploymentAllCool() then return end assert(unitsToDeploy[1]:GetPos() == lowArea) end local highArea = point(174600, 108600) for i = 1, 5 do LocalDeployUnitsOnMarker(unitsToDeploy, raisedTerrain, "show", highArea) if not CheckDeploymentAllCool() then return end assert(unitsToDeploy[1]:GetPos() == highArea) end -- Marker inside house, deploying outside on terrain. SnapCameraToObj(markerInHouse, true) local outsideArea = point(102600, 162600) for i = 1, 5 do LocalDeployUnitsOnMarker(unitsToDeploy, markerInHouse, "show", outsideArea) if not CheckDeploymentAllCool() then return end assert(unitsToDeploy[1]:GetPos() == outsideArea) end local insideArea = point(103800, 155400, 7700) for i = 1, 5 do LocalDeployUnitsOnMarker(unitsToDeploy, markerInHouse, "show", insideArea) if not CheckDeploymentAllCool() then return end assert(unitsToDeploy[1]:GetPos() == insideArea) end print("Entering retreat test...") local retreatTest = Presets.TestCombat.GameTest.RetreatTest TestCombatEnterSector(retreatTest, retreatTest.map) WaitLoadingScreenClose() wait_interface_mode("IModeDeployment") local dlg = GetInGameInterfaceModeDlg() dlg:StartExploration() WaitMsg("ExplorationStart", 5000) Sleep(100) gv_Sectors.I1.Side = "player1" -- Provides vision on the satellite map AllowRevealSectors({ "H2", "H3", "H4", "I1", "I2", "I3" }) unitsToDeploy = GetCurrentDeploymentSquadUnits() local marker = MapGetMarkers("ExitZoneInteractable", "East") marker[1]:UnitLeaveSector(unitsToDeploy[1]) WaitMsg("ZuluMessagePopup", 1000) local popup = g_ZuluMessagePopup[1] popup:Close(1) WaitMsg("InitSatelliteView", 3000) assert(gv_SatelliteView) end function WriteAnimSetsCSV() local anims, csv, columns = {}, {}, { "name" } for _, model in ipairs{ "Male", "Female" } do for _, prefix in ipairs{ "ar_", "civ_", "dw_", "gr_", "hg_", "hmg_", "hw_", "mk_", "nw_", "sg_", "inf_" } do local column = string.sub(model .. "_" .. prefix, 1, -2) columns[#columns+1] = column for _, anim in ipairs(table.map(EnumValidStates(model), GetStateName)) do if anim:starts_with(prefix) then local anim_name = anim:sub(#prefix+1) anims[anim_name] = anims[anim_name] or { name = anim_name } anims[anim_name][column] = GetAnimDuration(model, anim) end end end end for name, columns in sorted_pairs(anims) do csv[#csv+1] = columns end SaveCSV("AnimsBySet.csv", csv, columns, columns, ",") end function GameTests.AnimsCheckList() return end --[[ GameTests_LoadAnyMap() local model = "Male" local weapon_anims = GetModelWeaponAnims(model) local ar_anims = weapon_anims[1] local function check_model(weapon_anims, start_idx, model2) for k = start_idx, #weapon_anims do local weapon_prefix = weapon_sets[k] local anims = weapon_anims[k] if #ar_anims ~= #anims then GameTestsPrintf("%s:'%s' weapon set has %d animations, '%s:ar_' has %d", model2, weapon_prefix, #anims, model, #ar_anims) end -- check animations sets differencies local extra_anims = table.subtraction(anims, ar_anims) local missing_anims = table.subtraction(ar_anims, anims) if #extra_anims > 0 then GameTestsPrintf("'%s:%s' weapon set has %d extra animations compared to the '%s:_ar' set: %s", model2, weapon_prefix, #extra_anims, model, table.concat(extra_anims, ", ")) end if #missing_anims > 0 then GameTestsPrintf("'%s:%s' weapon set misses %d animations from the '%s:_ar' set: %s", model2, weapon_prefix, #missing_anims, model, table.concat(missing_anims, ", ")) end -- check matching anims for compensation and moments local matching_anims = table.intersection(ar_anims, anims) for _, anim in ipairs(matching_anims) do -- check compensation local ar_anim = "ar_" .. anim local weapon_anim = weapon_prefix .. anim local ar_compensatation = GetAnimCompensate(model, weapon_anim) local weapon_compensation = GetAnimCompensate(model, ar_anim) if ar_compensatation ~= weapon_compensation then GameTestsPrintf("'%s:%s' is %s compensated but '%s:%s' is %s compensated", model2, weapon_anim, ar_compensatation, model, ar_anim, weapon_compensation) end -- check moments local ar_moments = table.map(GetEntityAnimMoments(model, ar_anim), "Type") local weapon_moments = table.map(GetEntityAnimMoments(model, weapon_anim), "Type") if #ar_moments ~= #weapon_moments then GameTestsPrintf("'%s:%s' has %d moments, '%s:%s' has %d", model2, weapon_anim, #weapon_moments, model, ar_anim, #ar_moments) end local moment_diff = #ar_moments ~= #weapon_moments if not moment_diff then for m = 1, #ar_moments do if ar_moments[m] ~= weapon_moments[m] then moment_diff = m break end end end if moment_diff then GameTestsPrintf("'%s:%s' moments sequence mismatches '%s:%s", model2, weapon_anim, model, ar_anim) GameTestsPrintf("'%s:%s': %s", model2, weapon_anim, table.concat(weapon_moments, ", ")) GameTestsPrintf("'%s:%s': %s", model, ar_anim, table.concat(ar_moments, ", ")) local extra_moments = table.subtraction(weapon_moments, ar_moments) local missing_moments = table.subtraction(ar_moments, weapon_moments) if #extra_moments > 0 then GameTestsPrintf("'%s:%s' has %d extra moments compared to the '%s:%s' set: %s", model2, weapon_anim, #extra_moments, model, ar_anim, table.concat(extra_moments, ", ")) end if #missing_moments > 0 then GameTestsPrintf("'%s:%s' misses %d moments from the '%s:%s': %s", model2, weapon_anim, #missing_moments, model, ar_anim, table.concat(missing_moments, ", ")) end end end end end check_model(weapon_anims, 2, model) local model2 = "Female" local weapon_anims2 = GetModelWeaponAnims(model2) -- trunc first model animations variations if more than corresponding animations in model2 -- e.g. Female:ar_Combat_Walk has less variations exported than Male:ar_CombatWalk for k, anims in ipairs(weapon_anims2) do for _, anim_name in ipairs(anims) do local anim_base = string.match(anim_name, "([^%d]+)(%d+)") if anim_base then local model_vars = weapon_anims[k].vars[anim_base] local model2_vars = weapon_anims2[k].vars[anim_base] if model_vars and model2_vars and model_vars > model2_vars then for var = model2_vars + 1, model_vars do local anim_to_del = anim_base .. var table.remove_entry(weapon_anims[k], anim_to_del) end end end end end check_model(weapon_anims2, 1, model2) end local weapon_sets = {"ar_", "mk_", "dw_", "hg_", "nw_", "hw_"} local function GetWeaponPrefix(anim_name) for _, prefix in ipairs(weapon_sets) do local weapon_prefix = "^" .. prefix if string.match(anim_name, weapon_prefix) then return prefix end end end local s_WeaponAnim = false local function GetModelWeaponAnims(model, sets) local valid_states = EnumValidStates(model) local anims = {} for _, state in ipairs(valid_states) do table.insert(anims, GetStateName(state)) end local weapon_anims = {} for idx, prefix in ipairs(sets or weapon_sets) do weapon_anims[idx] = {vars = {}, exists = {}} local weapon_prefix = "^" .. prefix for _, anim in ipairs(anims) do if string.match(anim, weapon_prefix) and anim:sub(-1, -1) ~= "*" then local anim_name = string.match(anim, weapon_prefix .. "(.*)") table.insert(weapon_anims[idx], anim_name) local anim_base, anim_vars = string.match(anim_name, "([^%d]+)(%d+)") if anim_base then anim_vars = tonumber(anim_vars) weapon_anims[idx].vars[anim_base] = Max(weapon_anims[idx].vars[anim_base] or 0, anim_vars) end local anim_base = string.sub(anim, string.len(prefix) + 1, -1) weapon_anims[idx].exists[anim_base] = true end end end return weapon_anims end local function GetMissingSets(model, anim_base) if not s_WeaponAnim then s_WeaponAnim = { ["Male"] = GetModelWeaponAnims("Male"), ["Female"] = GetModelWeaponAnims("Female") } end local missing_sets = {} for k, weapon_prefix in ipairs(weapon_sets) do if not s_WeaponAnim[model][k].exists[anim_base] then table.insert(missing_sets, weapon_prefix) end end return #missing_sets > 0 and missing_sets end function AnimMetadata:GetWarning() local entity = self.group local anim = self.id local weapon_prefix = GetWeaponPrefix(anim) local matching_other_weapons = true if weapon_prefix then local anim_base = string.sub(anim, string.len(weapon_prefix) + 1, -1) for _, prefix in ipairs(weapon_sets) do if weapon_prefix ~= prefix then local missing_sets = GetMissingSets(entity, anim_base) if missing_sets then if #missing_sets > 1 then return string.format("'%s' missing in {%s} sets", anim, table.concat(missing_sets, ", ")) else return string.format("'%s' missing in '%s' set", anim, missing_sets[1]) end end end end end end ]] GameTestsNightly.TestDoesPrefabMapsSavingGenerateFakeDeltas = empty_func function GameTestsNightly.TestBantersUsage() GameTests_LoadAnyMap() ClearErrorSource() local undefinedBanters, unusedBanters, ignoredGroups = TestBantersUsage() for _, undefinedBanterMarkers in ipairs(undefinedBanters) do local id = undefinedBanterMarkers[1] local obj = undefinedBanterMarkers[2] local handle, map = obj.handle, obj.map StoreErrorSource(HandleToObject[handle], string.format("Did not find a banter id or group '%s' for marker with handle %d on map: %s", id, handle, map)) end for _, unusedBanter in ipairs(unusedBanters) do StoreWarningSource(unusedBanter, string.format("Banter not used anywhere: '%s'", unusedBanter.id)) end print(" These banter groups are ignored: " .. table.concat(ignoredGroups, ", ") .. "") end function GameTestsNightly_TestLootTablesUsage() GameTests_LoadAnyMap() ClearErrorSource() local undefinedLootTableIds, unusedLootTableIds, ignoredLootTableGroups = TestLootTablesUsage() for _, undefinedBanterMarkers in ipairs(undefinedLootTableIds) do StoreErrorSource(undefinedBanterMarkers[2], string.format("Did not find a loot table id or group '%s' on map: %s", undefinedBanterMarkers[1], undefinedBanterMarkers[2].map)) end for _, unusedLootTable in ipairs(unusedLootTableIds) do StoreWarningSource(unusedLootTable, string.format("Loot table not used anywhere: '%s'", unusedLootTable.id)) end print(" These loot table groups are ignored: " .. table.concat(ignoredLootTableGroups, ", ") .. "") end GameTests.CheckSpots = nil function GameTests.CheckNonReachableAnimFXMoment() local anim_moment = {} for entity, anims in pairs(Presets.AnimMetadata) do for _, anim in ipairs(anims) do for _, moment in ipairs(anim.Moments) do anim_moment[anim.id] = anim_moment[anim.id] or {} anim_moment[anim.id][moment.Type] = true end end end local missing_anims, missing_moments = 0, 0 for fx, moments in sorted_pairs(FXRules) do local anim = string.match(fx, "Anim:(.+)") if anim then if not anim_moment[anim] then GameTestsPrintf("No such anim in AME: %s, required for: %s[%s]", anim, fx, table.concat(table.keys(moments, "sorted"), ",")) missing_anims = missing_anims + 1 else for moment, actors in sorted_pairs(moments) do for actor in sorted_pairs(actors) do if not anim_moment[anim][moment] then GameTestsPrintf("Missing FX moment in AME for '%s': %s-%s-%s", anim, fx, moment, actor) missing_moments = missing_moments + 1 end end end end end end GameTestsPrintf("Missing FX anims: %d, Missing FX moments: %d", missing_anims, missing_moments) end