--[[ Sample usage - in startup.lua: local smokeTest = require("data/smoketest") smokeTest.Run() ]]-- JSON = require("JSON") UUID = require("uuid4") local SmokeTest = {} ---------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------- -- Account to login with -- NOTE: this account must exist and have GM status (/gmon) SmokeTest.Username = engine.GetLoginUsername() -- set via the -username command line param SmokeTest.Password = engine.GetLoginPassword() -- set via the -password command line param -- List of all the playfields to go through SmokeTest.Playfields = { 1200, 2210, 2500, 3200, 3210, 4200, 4210, 4310, 5200, 5210, 6200 } SmokeTest.GUIModules = {} -- Delay between two consecutive teleports, in seconds SmokeTest.TeleportDelay = 1.5 -- Optional callback to invoke when SmokeTest finishes SmokeTest.FinishCallback = nil -- function() end -- Radius used to reduce the set of nav mesh points we should teleport to. The bigger the radius, the fewer points will be selected from the nav mesh. SmokeTest.TeleportPointRadius = 12.0 -- take perf snapshots at each teleport point? SmokeTest.CollectPerformanceMetrics = true -- Time we should spend measuring performances at each spot SmokeTest.PerformanceSamplingDelay = 1.5 -- Send perf snapshots to the metrics server? SmokeTest.SendPerformanceMetrics = true -- Endpoint for snapshots SmokeTest.MetricsUrl = engine.GetConfigValue("Universe/Client/GameplayMetricsAddr") --- http://dreambuilder01.funcom.com:1337/Metrics/Submit -- Shall we quit the client upon completion? SmokeTest.ShutdownClient = true -- Filter functions SmokeTest.PlayfieldPointsFilter = nil -- [new_points] function PlayfieldPointsFilter( playfield_id, points ) - allows caller to provided a modified point list ---------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------- local function Log( msg, level ) level = level or 'important' msg = " " .. msg debug.log( level, msg ) end local function LogWarning( msg ) Log( msg, 'warning' ) end local function LogError( msg ) Log( msg, 'error' ) end local function RunCmd( cmd ) return DWEngine.ExecuteConsoleCommand( "/" .. cmd ) end function TeleportToPlayfield( pfid ) -- already in this playfield? if pfid == DWEngine.GetPlayfieldId() then debug.log('warning', "TeleportToPlayfield: client is already in playfield " .. pfid ) return true end -- debug.log('info2', "Teleporting to playfield: " .. pfid ) local result = RunCmd( "teleport sp TP_Entrance " .. pfid ) return result end function TeleportToPosition( pos ) local x = pos.x local y = pos.y local z = pos.z local cmd = "teleport " .. x .. " " .. z .. " y " .. y -- debug.log('info2', "Teleporting to " .. tostring(pos) .. " using cmd: " .. cmd ) return RunCmd( cmd ) end function OpenGUIs() for i, name in ipairs(SmokeTest.GUIModules) do engine.setdistributedvalue(name, true) for j=0,100 do gui.sendtouchevent(0,math.random(),math.random()) gui.sendtouchevent(2,math.random(),math.random()) gui.sendtouchevent(1,math.random(),math.random()) end coroutine.yield(0.2) end end function CloseGUIs() for i, name in ipairs(SmokeTest.GUIModules) do engine.setdistributedvalue(name, false) end end function ActivateGodMode() RunCmd( "selectself" ) RunCmd( "set MinimumHealthCap 100" ) end local function GenerateSessionId() return UUID.getUUID() end -- round number to the given number of decimal places (optional) function Round( val, decimalPlaces ) local mult = 10 ^ ( decimalPlaces or 0) return math.floor( val * mult + 0.5 ) / mult end local function GetPerfCounterRounded( counterId, decimalPlaces ) return Round( profile.GetPerfCounterValue( counterId ), decimalPlaces ) end local function GetClientConfiguration() if engine.live then return "live" elseif engine.release then return "release" elseif engine.debug then return "debug" else return "unknown" end end local function GetClientVersion() local version = engine.version if version == "@_" then version = "" end return version end local function TakePerformanceSnapshot( sessionId, playfieldId, pos, fpsTracker ) local snapshot = {} snapshot.session = sessionId snapshot.client_version = GetClientVersion() snapshot.client_cfg = GetClientConfiguration() snapshot.platform = engine.GetOSName() snapshot.platform_version = engine.GetOSVersion() snapshot.device_model = engine.GetDeviceModelName() snapshot.device_name = engine.GetDeviceName() snapshot.playfield = playfieldId snapshot.pos_x = Round( pos.x, 3 ) snapshot.pos_y = Round( pos.y, 3 ) snapshot.pos_z = Round( pos.z, 3 ) snapshot.frametime_worst = Round( 0.001 * fpsTracker.worstFrameTime, 1 ) snapshot.frametime_avg = Round( 0.001 * fpsTracker.averageFrameTime, 1 ) snapshot.frametime_best = Round( 0.001 * fpsTracker.bestFrameTime, 1 ) snapshot.fps_worst = fpsTracker.worstFps snapshot.fps_avg = fpsTracker.averageFps snapshot.fps_best = fpsTracker.bestFps return snapshot end function url_encode(str) if (str) then str = string.gsub (str, "\n", "\r\n") str = string.gsub (str, "([^%w %-%_%.%~])", function (c) return string.format ("%%%02X", string.byte(c)) end) str = string.gsub (str, " ", "+") end return str end local function SendSnapshot( snapshot ) if SmokeTest.MetricsUrl == nil or SmokeTest.MetricsUrl == '' then LogError( "SmokeTest.MetricsUrl is not set. Make sure you are using a valid LocalConfig.xml with specified." ) return false end local url = SmokeTest.MetricsUrl .. "?collection=smoketest" local data = "" for key, value in pairs(snapshot) do data = data .. "&" .. key .. "=" .. url_encode( value ) end local query = url .. data Log( query ) return engine.HttpGet( query ) end local function DumpSnapshotsToFile( snapshots, filename ) local f, err = io.open( filename, "w" ) if not f then return print( err ) end f:write( "{\n" ); for i, snapshot in ipairs( snapshots ) do local json = JSON:encode_pretty( snapshot ) f:write( tostring( json ) ) f:write( ",\n" ) end f:write( "}\n" ); f:close() end local function DisableCameraSmoothing() RunCmd( "gametweaks set Camera_Iso_SmoothEnabled 0" ) end local function RequestStaticPoints( playfieldIDs ) for i, pfid in ipairs( playfieldIDs ) do Log( "Requesting static points for playfield #" .. pfid ) Playfield.RequestStaticPoints( pfid ) end end local function BuildPlayfieldSequence( playfieldIDs, modifierFunc ) local playfields = {} for i, pfid in ipairs(playfieldIDs) do local pf = {} pfid = pfid pf.points = Playfield.GetStaticPoints( pfid ) table.insert( playfields, pf ) end -- let the modifier function tweak the content of the playfields sequence if modifierFunc then modifierFunc( playfields ) end return playfields end local function CountTotalStaticPoints( playfields ) total = 0 for i, pf in ipairs( playfields ) do total = total + #pf.points end return total end -- Reduce list of points by projecting them on a regular 2D grid (cell_size) and keeping only one point per grid cell function ReducePoints( points, cell_size ) -- spatial hashing local selected_points_map = {} for i, p in ipairs( points ) do local hash = string.format( "%d|%d", math.floor( p.x / cell_size ), math.floor( p.z / cell_size ) ) selected_points_map[hash] = p -- overwrite whatever was already there end -- generate a flat list so we dont drag around the hashes local selected_points_list = {} for hash, p in pairs( selected_points_map ) do table.insert( selected_points_list, p ) end return selected_points_list end --[[ Attempt to teleport to each playfield in the playfields array OUTPUTS result.SessionId: the session Id used for this smoke test result.Errors: error count result.Disconnected: true if client lost connection during smoke test result.LastPlayfield: the last playfield we were on result.Snapshots: collection of snapshots generated at each point the character teleported to result.TotalLocations: total number of static points this smoketest should cover result.TotalPointsVisited: total number of static points visited so far result.CompletionPercent: progress % (0..1) result.ExitCode: code the program will return on shutdown. This can be modified by FinishCallback. --]] function SmokeTest.Run() DWEngine.StartCoroutine( function() local sessionId = GenerateSessionId() Log( "Starting session: " .. sessionId ) -- output data passed to FinishCallback result = {} result.SessionId = sessionId result.Snapshots = {} result.Errors = 0 result.Disconnected = false result.LastPlayfield = 0 result.Playfields = {} result.PlayfieldsVisited = {} result.TotalPoints = 0 result.TotalPointsVisited = 0 result.ExitCode = 0 flow.Login( SmokeTest.Username, SmokeTest.Password ) coroutine.yield( 1 ) -- wait until the client is in a valid state to issue the "enter game" command while not flow.CanEnterGame() do coroutine.yield(0) end flow.EnterGame() -- wait until we're done loading while not flow.IsInPlay() do coroutine.yield(0) end coroutine.yield( 0.5 ) local playfields = SmokeTest.Playfields for i, pfid in ipairs( playfields ) do Log( "-> Playfield #" .. pfid ) end result.Playfields = playfields local fpsTracker = FrameRateTracker() for i, pfid in ipairs(playfields) do -- check if we lost connection to server if flow.IsDisconnected() then LogError( "Connection to server was lost. Aborting smoke-test." ) debug.log('warning', "Connection to server was lost. Aborting smoke-test." ) result.Disconnected = true result.Errors = result.Errors + 1 break end Log( "Teleporting to playfield #" .. pfid ) if TeleportToPlayfield( pfid ) then -- temporise a bit so flow has a few frames to finish transitioning state coroutine.yield( 4.0 ) -- wait until we're done loading the new playfield while not flow.IsInPlay() do coroutine.yield( 0 ) end -- re-apply our tweaks/commands to make sure they haven't been overwriten after loading ActivateGodMode() DisableCameraSmoothing() coroutine.yield( 1.0 ) -- current playfield local curPF = DWEngine.GetPlayfieldId() -- verify we're on the correct playfield if curPF == pfid then debug.log('info2', "Successfully teleported to playfield #" .. pfid ) result.LastPlayfield = pfid -- generate the list of points we should teleport to local navmesh_points = player.playfield:GetNavMeshPoints() local points = ReducePoints( navmesh_points, SmokeTest.TeleportPointRadius ) if SmokeTest.PlayfieldPointsFilter then points = SmokeTest.PlayfieldPointsFilter( pfid, points ) end local points_count = #points Log( string.format( "Playfield #%d: selected %d points to visit (navmesh has %d points)", pfid, points_count, #navmesh_points ) ) result.TotalPoints = result.TotalPoints + points_count -- teleport to each point for j, pos in ipairs( points ) do -- check if we lost connection to server if flow.IsDisconnected() then LogError( "Connection to server was lost. Aborting SmokeTest." ) result.Disconnected = true result.Errors = result.Errors + 1 break end -- make sure we're ready before teleporting again while not flow.IsInPlay() do coroutine.yield( 0 ) end -- report progress local progress = ( j - 1 ) / points_count Log( string.format( "Teleporting to point [%s] (%d/%d) on playfield #%d (%f%% complete)", tostring( pos ), j, points_count, pfid, Round( 100 * progress, 2 ) ) ) if TeleportToPosition( pos ) then result.TotalPointsVisited = result.TotalPointsVisited + 1 coroutine.yield( SmokeTest.TeleportDelay ) if #SmokeTest.GUIModules > 0 then for j=0,10 do OpenGUIs() coroutine.yield(1) CloseGUIs() end end -- performance if SmokeTest.CollectPerformanceMetrics then -- measure performances for a short while fpsTracker:Start() coroutine.yield( SmokeTest.PerformanceSamplingDelay ) fpsTracker:Stop() -- build the perf report/snapshot local snapshot = TakePerformanceSnapshot( sessionId, pfid, pos, fpsTracker ) snapshotJson = JSON:encode_pretty( snapshot ) -- Log( snapshotJson ) table.insert( result.Snapshots, snapshot ) -- upload snapshot to backend? if SmokeTest.SendPerformanceMetrics then if not SendSnapshot( snapshot ) then LogWarning( "Failed to send snapshot:\n" .. snapshotJson ) end end end end end table.insert( result.PlayfieldsVisited, pfid ) else LogError( "Failed to teleport to playfield #" .. pfid .. ". Player is still in pf #" .. curPF ) result.Errors = result.Errors + 1 coroutine.yield( 0.5 ) end else LogError( "Teleport command failed on playfield #" .. pfid ) result.Errors = result.Errors + 1 end end Log( string.format( "Done! %d errors - Visited %d/%d points, %d/%d playfields (last: #%d)", result.Errors, result.TotalPointsVisited, result.TotalPoints, #result.PlayfieldsVisited, #result.Playfields, result.LastPlayfield ) ) if result.Errors > 0 then if result.Disconnected then result.ExitCode = 111 else result.ExitCode = 666 end else result.ExitCode = 0 -- no error end -- invoke optional callback function if SmokeTest.FinishCallback then SmokeTest.FinishCallback( result ) end if SmokeTest.ShutdownClient then Log( "Shutting down client with exit code: " .. result.ExitCode ) engine.QuitViolently( result.ExitCode ) end end ) end return SmokeTest