-- This Source Code Form is subject to the terms of the bCDDL, v. 1.1. -- If a copy of the bCDDL was not distributed with this -- file, You can obtain one at http://beamng.com/bCDDL-1.1.txt -- [[ STORE FREQUENTLY USED FUNCTIONS IN UPVALUES ]] -- local max = math.max local min = math.min local sin = math.sin local asin = math.asin local pi = math.pi local abs = math.abs local sqrt = math.sqrt local tableInsert = table.insert local tableRemove = table.remove local strFormat = string.format --------------------------------- local scriptai = nil local M = {} M.mode = 'disabled' -- this is the main mode M.manualTargetName = nil M.debugMode = 'off' M.speedMode = nil M.routeSpeed = nil M.extAggression = 5 M.cutOffDrivability = 0 -- [[ ENVIRONMENT VARIABLES ]] -- local g = abs(obj:getGravity()) local gravityDir = vec3(0, 0, -1) local gravityVec = gravityDir * g ---------------------------------- -- [[ PERFORMANCE RELATED ]] -- local aggression = 5 local aggressionMode -------------------------------------------------------------------------- -- [[ AI DATA: POSITION, CONTROL, STATE ]] -- local aiPos = vec3(obj:getFrontPosition()) --aiPos.z = BeamEngine:getSurfaceHeightBelow(aiPos) local aiDirVec = vec3(obj:getDirectionVector()) local prevDirVec = vec3(aiDirVec) local aiUpVec = vec3(obj:getDirectionVectorUp()) local aiRightVec = vec3(0, 0, 0) local aiVel = vec3(obj:getVelocity()) local aiSpeed = aiVel:length() local aiSpeedSmoother = newTemporalSmoothingNonLinear(math.huge, 0.2) local aiWidth -- this comes back zero at this point local aiLength -- this comes back zero at this point local aiDeviation = 0 local aiDeviationSmoother = newTemporalSmoothing(1) local aiCannotMoveTime = 0 local aiForceGoFrontTime = 0 local staticFrictionCoef = 1 local prevBrake = 0 local forces = {} local threewayturn = 0 local lastCommand = {steering = 0, throttle = 0, brake = 0, parkingbrake = 0} local targetLength local driveInLaneFlag = false local internalState = 'onroad' local validateInput = nop ------------------------------ -- [[ CRASH DETECTION ]] -- local crashTime = 0 local crashManeuver = 0 local crashDir = nil -- [[ OPPONENT DATA ]] -- local player local plPrevVel -- [[ SETTINGS, PARAMETERS, AUXILIARY DATA ]] -- local map local targetName local currentRoute local minPlanCount = 18 local wpList local speedList local routeSpeed local limitSpeed local race local noOfLaps local targetObjectSelectionMode local edgeDict ------------------------------ -- [[ TRAFFIC ]] -- local baseSpeed = 22 local damageOver = false ----------------------- -- [[ HEAVY DEBUG MODE ]] -- local speedRecordings = {} local trajecRec = {last = 0} local routeRec = {last = 0} local labelRenderDistance = 10 local targetSpeed local trafficTable = {} local trafficBlock = {timer = 0, coef = 0, limit = 6, horn = 0} local trafficSide = {timer = 5, cTimer = 0, side = 1} local trafficMinProjSpeed = math.huge local avoidCars = 'on' local avoidCarsOverride = nil local changePlanTimer = 0 ------------------------------ local function aistatus(status, category) guihooks.trigger("AIStatusChange", {status=status, category=category}) end local function getState() return M end local function stateChanged() if playerInfo.anyPlayerSeated then guihooks.trigger("AIStateChange", getState()) end end local function setSpeed(speed) -- both routeSpeed and limitSpeed have to be ~= nil for the set speed (either in limit mode or in set mode) to be effective if type(speed) ~= 'number' then routeSpeed = nil else routeSpeed = speed end baseSpeed = routeSpeed M.routeSpeed = routeSpeed end local function setSpeedMode(speedMode) if speedMode == 'set' then limitSpeed = false elseif speedMode == 'limit' then limitSpeed = true else limitSpeed = nil end M.speedMode = speedMode end local function resetSpeedModeAndValue() routeSpeed = nil limitSpeed = nil M.speedMode = nil -- maybe this should be 'off' M.routeSpeed = nil end local function setAggressionInternal(v) aggression = v and v or M.extAggression end local function setAggressionExternal(v) M.extAggression = v or M.extAggression setAggressionInternal() stateChanged() end local function setAggressionMode(aggrmode) if aggrmode == 'rubberBand' then aggressionMode = 'rubberBand' else aggressionMode = nil end end local function resetAggression() setAggressionInternal() end local function setTargetObjectID(id) M.targetObjectID = M.targetObjectID ~= objectId and id or -1 if M.targetObjectID ~= -1 then targetObjectSelectionMode = 'manual' end end local function updatePlayerData() if mapmgr.objects[M.targetObjectID] and targetObjectSelectionMode == 'manual' then player = mapmgr.objects[M.targetObjectID] player.id = M.targetObjectID elseif tableSize(mapmgr.objects) == 2 then if player ~= nil then player = mapmgr.objects[player.id] else for k, v in pairs(mapmgr.objects) do if k ~= objectId then M.targetObjectID = k player = v break end end targetObjectSelectionMode = 'auto' end else if player ~= nil and player.active == true then player = mapmgr.objects[player.id] else for k, v in pairs(mapmgr.objects) do if k ~= objectId and v.active == true then M.targetObjectID = k player = v break end end targetObjectSelectionMode = 'targetActive' end end mapmgr.objects[objectId] = mapmgr.objects[objectId] or {pos = aiPos, dirVec = aiDirVec} end local function driveCar(steering, throttle, brake, parkingbrake) input.event("steering", -steering, 1) input.event("throttle", throttle, 2) input.event("brake", brake, 2) input.event("parkingbrake", parkingbrake, 2) lastCommand.steering = steering lastCommand.throttle = throttle lastCommand.brake = brake lastCommand.parkingbrake = parkingbrake end local function driveToTarget(dt, targetPos, throttle, brake) if not targetPos then return end local brakeCoef = 1 local throttleCoef = 1 local targetVec = (targetPos - aiPos):normalized() local dirAngle = math.asin(aiRightVec:dot(targetVec)) local dirVel = aiVel:dot(aiDirVec) local absAiSpeed = abs(dirVel) local plan = currentRoute and currentRoute.plan -- oversteer if aiSpeed > 1 then local rightVel = aiRightVec:dot(aiVel) if rightVel * aiRightVec:dot(targetPos - aiPos) > 0 then local rotVel = min(1, (prevDirVec:projectToOriginPlane(aiUpVec):normalized() - aiDirVec):length() * dt * 10000) throttleCoef = throttleCoef * max(0, 1 - abs(rightVel * aiSpeed * 0.05) * min(1, dirAngle * dirAngle * aiSpeed * 6) * rotVel) end end if plan ~= nil and #plan >= 3 and dirVel > 3 then local p1, p2 = plan[1].pos, plan[2].pos local p2p1 = p2 - p1 local turnRight = p2p1:cross(aiUpVec):normalized() local tp2 if plan.targetSeg and plan.targetSeg > 1 then tp2 = (targetPos - p2):normalized():dot(turnRight) else tp2 = (plan[3].pos - p2):normalized():dot(turnRight) end local outDeviation = aiDeviationSmoother:value() - aiDeviation * sign(tp2) outDeviation = sign(outDeviation) * min(1, abs(outDeviation)) aiDeviationSmoother:set(outDeviation) aiDeviationSmoother:getUncapped(0, dt) if outDeviation > 0 and absAiSpeed > 3 then local steerCoef = outDeviation * absAiSpeed * absAiSpeed * min(1, dirAngle * dirAngle * 4) local understeerCoef = max(0, steerCoef) * min(1, abs(aiVel:dot(p2p1:normalized()) * 3)) local noUndersteerCoef = max(0, 1 - understeerCoef) throttleCoef = throttleCoef * noUndersteerCoef brakeCoef = min(brakeCoef, max(0, 1 - understeerCoef * understeerCoef)) end else aiDeviationSmoother:set(0) end -- wheel speed if absAiSpeed > 0.2 then local avgWheelSpeed = 0 local wheelCount = 0 local minAbsWheelSpeed = math.huge local maxAbsWheelSpeed = 0 local lwheels = wheels.wheels for i = 0, tableSizeC(lwheels) - 1 do local wd = lwheels[i] if not wd.isBroken then wheelCount = wheelCount + wd.isSpeedo local absWheelVel = abs(wd.angularVelocity * wd.radius) avgWheelSpeed = avgWheelSpeed + absWheelVel * wd.isSpeedo if wd.brakeTorque > 0 then minAbsWheelSpeed = min(minAbsWheelSpeed, absWheelVel) end if wd.isPropulsed then maxAbsWheelSpeed = max(maxAbsWheelSpeed, absWheelVel) end end end -- manual abs and tcs if wheelCount > 0 then local latVgrad = 0.5 * aiWidth * abs(sensors.gx2) / (aiSpeed + 1e-30) avgWheelSpeed = avgWheelSpeed / wheelCount -- abs local minGradSpeed = absAiSpeed - latVgrad if avgWheelSpeed < minGradSpeed and brake > 0 then brakeCoef = brakeCoef * minAbsWheelSpeed / minGradSpeed end -- tcs local maxGradSpeed = max(3, absAiSpeed + latVgrad) if maxAbsWheelSpeed > maxGradSpeed and throttle > 0 then local redcoef = maxGradSpeed / maxAbsWheelSpeed throttleCoef = throttleCoef * redcoef * redcoef end end end local dirTarget = aiDirVec:dot(targetVec) local dirDiff = -math.asin(aiDirVec:cross(aiUpVec):normalized():dot(targetVec)) if crashManeuver == 1 and dirTarget < aiDirVec:dot(crashDir) then driveCar(-fsign(dirDiff), brake * brakeCoef, throttle * throttleCoef, 0) return else crashManeuver = 0 end aiForceGoFrontTime = max(0, aiForceGoFrontTime - dt) if threewayturn == 1 and aiCannotMoveTime > 1 and aiForceGoFrontTime == 0 then threewayturn = 0 aiCannotMoveTime = 0 aiForceGoFrontTime = 2 end if aiForceGoFrontTime > 0 and dirTarget < 0 then dirTarget = -dirTarget dirDiff = -dirDiff end if dirTarget < 0 and currentRoute then local edgeDist = min((plan[2] or plan[1]).radiusOrig, plan[1].radiusOrig) - aiPos:z0():distanceToLine((plan[3] or plan[2]).posOrig:z0(), plan[2].posOrig:z0()) if edgeDist > aiWidth and threewayturn == 0 then driveCar(fsign(dirDiff), 0.5 * throttleCoef, 0, min(max(aiSpeed - 3, 0), 1)) else threewayturn = 1 driveCar(-fsign(dirDiff), 0, max(min(abs(dirTarget), 1), 0.6), 0) end else threewayturn = 0 local pbrake if aiVel:dot(aiDirVec) * max(aiVel:squaredLength() - 1e-2, 0) < 0 then if aiSpeed < 0.15 and targetSpeed <= 1e-5 then pbrake = 1 else pbrake = 0 end driveCar(dirDiff, 0.5 * throttleCoef, 0, pbrake) else if (aiSpeed > 4 and aiSpeed < 30 and abs(dirDiff) > 0.97 and brake == 0) or (aiSpeed < 0.15 and targetSpeed and targetSpeed <= 1e-5) then pbrake = 1 else pbrake = 0 end driveCar(dirDiff, throttle * throttleCoef, brake * brakeCoef, pbrake) end end end local function aiPosOnPlan(plan) local aiSeg = 1 local aiXnormOnSeg = 0 for i = 1, #plan-1 do local xnorm = aiPos:xnormOnLine(plan[i].pos, plan[i+1].pos) if xnorm < 1 then if i < #plan - 2 then local nextXnorm = aiPos:xnormOnLine(plan[i + 1].pos, plan[i + 2].pos) if nextXnorm >= -0.1 then aiXnormOnSeg = nextXnorm aiSeg = i + 1 break end end aiXnormOnSeg = xnorm aiSeg = i break end end for _ = 1, aiSeg-1 do tableRemove(plan, 1) end return aiXnormOnSeg end local function calculateTarget(plan) targetLength = max(aiSpeed * 0.65, 6.5) local remainder = targetLength local aiXnormOnSeg = aiPosOnPlan(plan) local segCount = #plan if #plan >= 3 then local xnorm = clamp(aiPos:xnormOnLine(plan[1].pos, plan[2].pos), 0 ,1) targetLength = max(targetLength, plan[1].pos:distance(plan[2].pos) * (1 - xnorm), xnorm * plan[2].pos:distance(plan[3].pos)) end local targetPos = plan[#plan].pos local targetSeg = max(1, segCount - 1) local prevPos = aiPos for i = 2, segCount do local pos = plan[i].pos local segVec = pos - prevPos local segLen = segVec:length() if remainder <= segLen then targetSeg = i - 1 targetPos = prevPos + segVec * (remainder / (segLen + 1e-25)) -- smooth target local n1pos, n2pos = prevPos, pos local xnorm = clamp(targetPos:xnormOnLine(n1pos, n2pos), 0 ,1) local lp_n1n2 = linePointFromXnorm(n1pos, n2pos, xnorm * 0.5 + 0.25) if xnorm <= 0.5 then if i >= 3 then targetPos = linePointFromXnorm(linePointFromXnorm(plan[i-2].pos, n1pos, xnorm * 0.5 + 0.75), lp_n1n2, xnorm + 0.5) end else if i <= segCount - 2 then targetPos = linePointFromXnorm(lp_n1n2, linePointFromXnorm(n2pos, plan[i+1].pos, xnorm * 0.5 - 0.25), xnorm - 0.5) end end break end prevPos = pos remainder = remainder - segLen end return targetPos, targetSeg, aiXnormOnSeg end local function targetsCompatible(baseRoute, newRoute) local baseTvec = baseRoute.plan.targetPos - aiPos local newTvec = newRoute.plan.targetPos - aiPos if aiSpeed < 2 then return true end if newTvec:dot(aiDirVec) * baseTvec:dot(aiDirVec) <= 0 then return false end local baseTargetRight = baseTvec:cross(aiUpVec):normalized() return abs(newTvec:normalized():dot(baseTargetRight)) * aiSpeed < 2 end local function getAiWp(route) if route then return route.path[route.plan[route.plan.aiSeg or 1].pathidx] end end local function getPlanLen() return min(550, max(150, aiSpeed * aiSpeed * 0.1 / aggression)) end local function pickAiWp(wp1, wp2, dirVec) dirVec = dirVec or aiDirVec local vec1 = map.nodes[wp1].pos - aiPos local vec2 = map.nodes[wp2].pos - aiPos local dot1 = vec1:dot(dirVec) local dot2 = vec2:dot(dirVec) if (dot1 * dot2) <= 0 then if dot1 < 0 then return wp2 end else if vec2:squaredLength() < vec1:squaredLength() then return wp2 end end return wp1 end local function planExtend(plan, path) if path == nil then return end local planPathLen = #plan.path if plan.path[planPathLen] ~= path[1] then return end local cplen = planPathLen - 1 for i = 1, #path do plan.path[i + cplen] = path[i] end end local function turnDeflection(plan, i, attrib) if i < 2 or i >= #plan then return vec3(0, 0, 0) end attrib = attrib or 'pos' local n1pos = plan[i][attrib] return (linePointFromXnorm(plan[i-1][attrib], n1pos, 0.75) + linePointFromXnorm(n1pos, plan[i+1][attrib], 0.25)) * 0.5 - n1pos end -- http://cnx.org/contents/--TzKjCB@8/Projectile-motion-on-an-inclin local function projectileSqSpeedToRangeRatio(pos1, pos2, pos3) local sinTheta = (pos2.z - pos1.z) / pos1:distance(pos2) local sinAlpha = (pos3.z - pos2.z) / pos2:distance(pos3) local cosAlphaSquared = max(1 - sinAlpha * sinAlpha, 0) local cosTheta = sqrt(max(1 - sinTheta * sinTheta, 0)) -- in the interval theta = {-pi/2, pi/2} cosTheta is always positive return 0.5 * g * cosAlphaSquared / max(cosTheta * (sinTheta*sqrt(cosAlphaSquared) - cosTheta*sinAlpha), 0) end local function buildNextRoute(plan, planCount, path) local nextPathId, n1 if planCount == 0 then -- TODO: maybe in this case it would be better to call findClosest road rather than just using a radius from the vehicle width nextPathId = 1 if not path[nextPathId] then return end n1 = {pos = vec3(aiPos), radius = aiWidth * 0.5} else nextPathId = plan[planCount].pathidx + 1 local prevPathId = nextPathId - 1 if race == true and noOfLaps and noOfLaps > 1 and nextPathId > #path then local loopPathId local lastWayPoint = path[#path] for i, wayPoint in ipairs(path) do if lastWayPoint == wayPoint then loopPathId = i break end end nextPathId = 1 + loopPathId -- nextPathId % #path noOfLaps = noOfLaps - 1 end if not path[nextPathId] then return end n1 = map.nodes[path[prevPathId]] -- this might be a bug, try this -> route.plan[planCount].posOrig end local n2 = map.nodes[path[nextPathId]] if not n2 then return end local nodeName = path[nextPathId] if driveInLaneFlag and n2.radius >= 2.5 then -- TODO: (and n1.radius >= 2.5) the second check might be problematic when n1.radius falls to the vehicle radius... i.e. planLen == 0 above local n2Pos = n2.pos local n1Pos = n1.pos local nVec1 = (n1Pos - n2Pos):z0():cross(gravityDir):normalized() -- vector normal to direction vector for current segment local prevNodeName = path[nextPathId - 1] local lane = 1 plan.lane = plan.lane or 0 if prevNodeName then local link = map.nodes[prevNodeName].links[nodeName] if link and link.oneWay then if plan.lane ~= 0 then lane = plan.lane else if #path >= 2 then local curPathIdx = max(2, plan[1].pathidx or 2) local p1Pos = map.nodes[path[curPathIdx - 1]].pos lane = sign((map.nodes[path[curPathIdx]].pos - p1Pos):z0():cross(gravityDir):dot(p1Pos - aiPos)) plan.lane = lane end end else plan.lane = 0 end end local n2HalfRadius = n2.radius * 0.5 if path[nextPathId + 1] then -- if this is not the last segment in the path local n3Pos = map.nodes[path[nextPathId+1]].pos local nVec2 = (n2Pos - n3Pos):z0():cross(gravityDir):normalized() -- vector normal to direction vector of lookAhead segment n2 = {pos = n2Pos + n2HalfRadius * lane * (1 - nVec1:dot(nVec2) * 0.5) * (nVec1 + nVec2), radius = n2HalfRadius} else n2 = {pos = n2Pos + n2HalfRadius * lane * nVec1, radius = n2HalfRadius} end local n1HalfRadius = n1.radius * 0.5 n1 = {pos = n1Pos + min(planCount, 1) * n1HalfRadius * nVec1, radius = n1HalfRadius} end local vec = (n1.pos - n2.pos):z0() local manSpeed = speedList and speedList[nodeName] return {pos = vec3(n2.pos), posOrig = vec3(n2.pos), radius = n2.radius, radiusOrig = n2.radius, posz0 = n2.pos:z0(), vec = vec, dirVec = vec:normalized(), turnDir = vec3(0,0,0), manSpeed = manSpeed, pathidx = nextPathId} end local function createPlan(route) local path = route.path local plan = route.plan local planCount = #plan local newPlan = {} if planCount == 0 then local pos1 = aiPos newPlan[1] = {posOrig = vec3(pos1), pos = vec3(pos1), posz0 = pos1:z0(), vec = 8 * vec3(-aiDirVec), dirVec = vec3(-aiDirVec), turnDir = vec3(0,0,0), radiusOrig = 2, radius = 2} else newPlan[1] = plan[1] end local n = plan[2] or buildNextRoute(plan, planCount, path) if not n then route.plan = newPlan return 1 end newPlan[2] = n local newPlanCount = 2 local newPlanLen = 0 local j = 1 local i = 2 local minPlanLen = getPlanLen() repeat local curDist = newPlan[j].posOrig:distance(newPlan[j+1].posOrig) local xSq = square(newPlanLen + curDist) if curDist > min(220, (1e-7*xSq + 1e-3)*xSq + 6) then local n1 = newPlan[j] local n2 = newPlan[j+1] local deflection = (turnDeflection(newPlan, j) + turnDeflection(newPlan, j + 1)):projectToOriginPlane((n1.pos - n2.pos):normalized()) local pos = (n1.pos + n2.pos - deflection)*0.5 local vec = (n1.pos - pos):z0() n2.vec = (pos - n2.pos):z0() n2.dirVec = n2.vec:normalized() local origDeflection = (turnDeflection(newPlan, j, 'posOrig') + turnDeflection(newPlan, j + 1, 'posOrig')):projectToOriginPlane((n1.posOrig - n2.posOrig):normalized()) tableInsert(newPlan, j+1, {posOrig = (n1.posOrig + n2.posOrig - origDeflection)*0.5, pos = pos, posz0 = pos:z0(), vec = vec, dirVec = vec:normalized(), turnDir = vec3(0, 0, 0), radiusOrig = (n1.radiusOrig + n2.radiusOrig)*0.5, radius = (n1.radius + n2.radius)*0.5, pathidx = n2.pathidx}) newPlanCount = newPlanCount + 1 else j = j + 1 if j >= newPlanCount then i = i + 1 local n = plan[i] or buildNextRoute(newPlan, newPlanCount, path) if not n then break end newPlanCount = newPlanCount + 1 newPlan[newPlanCount] = n end newPlanLen = newPlanLen + curDist end -- 'and i >= planCount' ensures all previous plan nodes have been processed -- and inserted into the newPlan. solves the issue of having one very long edge. until newPlanLen > minPlanLen and newPlanCount >= minPlanCount and i >= planCount newPlan[1].pathidx = newPlan[2].pathidx route.plan = newPlan return newPlanCount end local function mergePathPrefix(source, dest, srcStart) srcStart = srcStart or 1 local dict = table.new(0, #source) for i = srcStart, #source do dict[source[i]] = i end for i = #dest, 1, -1 do local srci = dict[dest[i]] if srci ~= nil then local res = table.new(#dest, 0) local resi = 1 for i1 = srcStart, srci - 1 do res[resi] = source[i1] resi = resi + 1 end for i1 = i, #dest do res[resi] = dest[i1] resi = resi + 1 end return res, srci end end return dest, 0 end local function planAhead(route, baseRoute) if route == nil then return end if route.path == nil then route.path = {} for i = 1, #route do route.path[i] = route[i] route[i] = nil end route.plan = {} end local plan = route.plan local planCount = #plan if baseRoute and planCount == 0 then -- merge from base plan local bsrPlan = baseRoute.plan for i = bsrPlan.aiSeg-1, 1, -1 do tableRemove(bsrPlan, 1) end bsrPlan.aiSeg = 1 local bsrPlanLen = #bsrPlan if bsrPlanLen > 1 then local commonPathEnd route.path, commonPathEnd = mergePathPrefix(baseRoute.path, route.path, bsrPlan[1].pathidx) if commonPathEnd > 1 then local i = 1 local refpathidx = bsrPlan[1].pathidx - 1 while i <= bsrPlanLen and bsrPlan[i].pathidx <= commonPathEnd do local n = bsrPlan[i] plan[i] = {pos = vec3(n.pos), posOrig = vec3(n.posOrig), posz0 = vec3(n.posz0), vec = vec3(n.vec), dirVec = vec3(n.dirVec), turnDir = vec3(n.turnDir), radius = n.radius, radiusOrig = n.radiusOrig, pathidx = n.pathidx - refpathidx} i = i + 1 end plan.aiSeg = bsrPlan.aiSeg planCount = i - 1 if planCount > bsrPlan.targetSeg then plan.targetSeg = bsrPlan.targetSeg end end end end if planCount < minPlanCount then planCount = createPlan(route) plan = route.plan end if plan.targetSeg == nil then local targetPos, targetSeg, _ = calculateTarget(plan) plan.targetPos = targetPos plan.targetSeg = targetSeg plan.aiSeg = 1 planCount = #plan end local aiSeg = plan.aiSeg or 1 for i = aiSeg-1, 1, -1 do planCount = planCount - 1 tableRemove(plan, 1) end local targetSeg = plan.targetSeg if planCount == 0 then return end for i = planCount, 0, -1 do if forces[i] then forces[i]:set(0,0,0) else forces[i] = vec3(0,0,0) end end -- apply springs forces for i = 1, planCount-1 do local n1 = plan[i] local n2 = plan[i+1] local v1 = n1.dirVec local v2 = -n2.dirVec local turnDir = (v1 + v2):normalized() local nforce = max(1 + v1:dot(v2), 0) * 2 * turnDir forces[i+1] = forces[i+1] - nforce forces[i-1] = forces[i-1] - nforce forces[i] = forces[i] + nforce + nforce n1.turnDir:set(turnDir) n1.speed = 0 end -- other vehicle awareness trafficMinProjSpeed = math.huge if avoidCars ~= 'off' and baseRoute == nil then table.clear(trafficTable) for plID, v in pairs(mapmgr.objects) do if not (plID == objectId or (M.mode == 'chase' and plID == player.id)) then v.length = obj:getObjectInitialLength(plID) + 0.3 v.width = obj:getObjectInitialWidth(plID) local posFront = obj:getObjectFrontPosition(plID) local dirVec = v.dirVec v.posFront = dirVec * 0.3 + posFront v.posRear = dirVec * (-v.length) + posFront table.insert(trafficTable, v) end end local trafficMinSpeed = math.huge local trafficTableLen = #trafficTable local distanceT = 0 local aiPathVel = aiVel:dot((plan[2].pos - plan[1].pos):normalized()) local aiPathVelInv = 1 / abs(aiPathVel + 1e-30) local minTrafficDir = 1 for i = 2, planCount-1 do local n1, n2 = plan[i], plan[i+1] local n1pos, n2pos = n1.pos, n2.pos local n1n2 = n2pos - n1pos local n1n2len = n1n2:length() local nDir = n1n2 / (n1n2len + 1e-30) n1.trafficSqVel = math.huge local arrivalT = distanceT * aiPathVelInv for i1 = trafficTableLen, 1, -1 do local v = trafficTable[i1] local plPosFront, plPosRear, plWidth = v.posFront, v.posRear, v.width local plAi = plPosFront - aiPos local plAiDir = plAi:dot(aiDirVec) if plAiDir > 0 then local velDisp = arrivalT * v.vel plPosFront = plPosFront + velDisp plPosRear = plPosRear + velDisp end local extVec = nDir * (max(aiWidth, plWidth) * 0.5) local n1ext, n2ext = n1pos - extVec, n2pos + extVec local rnorm, vnorm = closestLinePoints(n1ext,n2ext, plPosFront, plPosRear) local minSqDist = math.huge if rnorm > 0 and rnorm < 1 and vnorm > 0 and vnorm < 1 then minSqDist = 0 else local rlen = n1n2len + plWidth local v1 = plPosFront - n1ext local v1dot = v1:dot(nDir) if (v1dot > 0 and v1dot < rlen) then minSqDist = min(minSqDist, (v1 - v1dot * nDir):squaredLength()) end v1 = plPosRear - n1ext v1dot = v1:dot(nDir) if (v1dot > 0 and v1dot < rlen) then minSqDist = min(minSqDist, (v1 - v1dot * nDir):squaredLength()) end rlen = v.length + aiWidth v1 = n1ext - plPosRear v1dot = v1:dot(v.dirVec) if (v1dot > 0 and v1dot < rlen) then minSqDist = min(minSqDist, (v1 - v1dot * v.dirVec):squaredLength()) end v1 = n2ext - plPosRear v1dot = v1:dot(v.dirVec) if (v1dot > 0 and v1dot < rlen) then minSqDist = min(minSqDist, (v1 - v1dot * v.dirVec):squaredLength()) end end if minSqDist < square((aiWidth + plWidth) * 0.8) then local vel = max(0, v.vel:dot(nDir)) local middlePos = (plPosFront + plPosRear) * 0.5 local forceCoef = max(0, (aiSpeed - vel) * 0.5) * trafficSide.side / ((1 + minSqDist) * (1 + min(distanceT * 0.1, distanceT / max(0, aiPathVel - v.vel:dot(nDir)) * 2))) forces[i] = forces[i] - (sign(n1.turnDir:dot(middlePos - n1.posOrig)) * forceCoef) * n1.turnDir forces[i + 1] = forces[i + 1] - (sign(n2.turnDir:dot(middlePos - n2.posOrig)) * forceCoef) * n2.turnDir if avoidCars == 'on' and M.mode ~= 'flee' and M.mode ~= 'random' then if minSqDist < square((aiWidth + plWidth) * 0.51) then -- obj.debugDrawProxy:drawSphere(0.25, v.posFront:toFloat3(), color(0,0,255,255)) -- obj.debugDrawProxy:drawSphere(0.25, plPosFront:toFloat3(), color(0,0,255,255)) table.remove(trafficTable, i1) trafficTableLen = trafficTableLen - 1 trafficMinProjSpeed = min(trafficMinProjSpeed, vel) n1.trafficSqVel = min(n1.trafficSqVel, vel * vel) trafficMinSpeed = min(trafficMinSpeed, v.vel:length()) minTrafficDir = min(minTrafficDir, (plPosFront - plPosRear):normalized():dot(nDir)) end if i == 2 and minSqDist < square((aiWidth + plWidth) * 0.6) and plAiDir > 0 and v.vel:dot(aiRightVec) * plAi:dot(aiRightVec) < 0 then n1.trafficSqVel = max(0, n1.trafficSqVel - (v.vel - v.vel:dot(aiDirVec) * v.vel):length()) end end end end distanceT = distanceT + n1n2len end if max(trafficMinSpeed, aiSpeed) < 0.5 then trafficBlock.coef = clamp((1 - minTrafficDir) * 0.5, 0, 1) else trafficBlock.coef = 0 end plan[1].trafficSqVel = plan[2].trafficSqVel end -- spring force integrator local distFromEdge = aiWidth * min(1, 0.7 + 0.55 * square(max(0, 1 - aggression))) distFromEdge = distFromEdge / (1 + trafficSide.cTimer * 0.3) for i = 3, planCount do local n = plan[i] local k = n.turnDir:dot(forces[i]) local nodeDisplVec = n.pos + fsign(k) * min(abs(k), 0.5) * n.turnDir - n.posOrig local nodeDisplLen = nodeDisplVec:length() local nodeDisplLenLim = min(nodeDisplLen, max(n.radiusOrig - distFromEdge, 0)) --n.radius = max(n.radiusOrig - fsign(nodeDisplVec:dot(n.turnDir)) * nodeDisplLenLim - distFromEdge, distFromEdge) n.radius = max(n.radiusOrig - nodeDisplLenLim, aiWidth * 0.7) n.pos = n.posOrig + (nodeDisplLenLim / (nodeDisplLen + 1e-30)) * nodeDisplVec n.posz0:set(n.pos:z0()) n.vec:set(plan[i-1].posz0 - n.posz0) n.dirVec:set(n.vec:normalized()) end -- smoothly distribute error from planline onto the front segments if targetSeg ~= nil and #plan > targetSeg and plan.targetPos then local dTotal = 0 for i = 2, targetSeg - 1 do plan[i].sumLen = dTotal dTotal = dTotal + (plan[i + 1].pos - plan[i].pos):length() end dTotal = dTotal + (plan.targetPos - plan[targetSeg].pos):length() dTotal = max(1, dTotal) local p1, p2 = plan[1].pos, plan[2].pos local dispVec = aiPos - linePointFromXnorm(p1, p2, aiPos:xnormOnLine(p1, p2)) aiDeviation = dispVec:dot((p2 - p1):cross(aiUpVec):normalized()) local dispVecRatio = dispVec / dTotal for i = targetSeg - 1, 3, -1 do plan[i].pos = plan[i].pos + (dTotal - plan[i].sumLen) * dispVecRatio plan[i].posz0 = plan[i].pos:z0() plan[i+1].vec = plan[i].posz0 - plan[i + 1].posz0 plan[i+1].dirVec = plan[i+1].vec:normalized() end plan[1].pos = p1 + dispVec plan[1].posz0 = plan[1].pos:z0() plan[2].pos = p2 + dispVec plan[2].posz0 = plan[2].pos:z0() if plan[3] then plan[3].vec = plan[2].posz0 - plan[3].posz0 plan[3].dirVec = plan[3].vec:normalized() end end -- plan speeds local totalAccel = min(aggression, staticFrictionCoef) * g local rLast = plan[planCount] if rLast.pathidx ~= #route.path or (race and noOfLaps and noOfLaps > 1) then rLast.speed = rLast.manSpeed or sqrt(2 * 550 * totalAccel) elseif M.mode == 'chase' then rLast.speed = player.vel:length() else rLast.speed = rLast.manSpeed or 0 end -- speed planning for i = planCount-1, 1, -1 do local n1 = plan[i] local n2 = plan[i+1] -- inclination calculation local v12d = n2.pos - n1.pos local dist = v12d:length() + 1e-30 v12d = v12d / dist local Gf = gravityVec:dot(v12d) -- acceleration due to gravity parallel to road segment, positive when downhill local Gt = (gravityVec - v12d * Gf):length() / g -- gravity vec normal to road segment local n2SpeedSq = square(n2.speed) local curvature = 2 * sqrt(n1.vec:cross(n2.vec):squaredLength() / (n1.vec:squaredLength() * n2.vec:squaredLength() * (n1.vec + n2.vec):squaredLength())) + 1.6e-7 local turnSpeedSq = totalAccel * Gt / curvature -- available centripetal acceleration * radius -- https://physics.stackexchange.com/questions/312569/non-uniform-circular-motion-velocity-optimization --local deltaPhi = 2 * asin(0.5 * n2.vec:length() * curvature) -- phi = phi2 - phi1 = 2 * asin(halfcord / radius) local n1SpeedSq = turnSpeedSq * sin(min(asin(min(1, n2SpeedSq / turnSpeedSq)) + 2*curvature*dist, pi*0.5)) -- -- average tangential acceleration -- THIS IS PROBLEMATIC FOR VERY LOW TO ZERO CURVATURES -- local acct = max((n1SpeedSq - min(n2SpeedSq, turnSpeedSq)) * 0.5 / dist, 0) - Gf -- -- average centripetal acceleration -- local accn = square((sqrt(min(n2SpeedSq, turnSpeedSq)) + sqrt(n1SpeedSq)) * 0.5) * curvature -- local acc = sqrt(accn * accn + acct * acct) -- turnSpeedSq = acc / curvature -- n1SpeedSq = turnSpeedSq * sin(min(asin(min(n2SpeedSq/turnSpeedSq), 1) + 2*curvature*dist, pi*0.5)) -- if i > 1 then -- local n0 = plan[i-1] -- local bumpSqSpeedToRangeRatio = projectileSqSpeedToRangeRatio(n0.pos, n1.pos, n2.pos) -- if bumpSqSpeedToRangeRatio ~= math.huge then -- i.e. the projectile angle is greater than the incline angle so a jump is possible -- if n2.posz0:distanceToLine(n0.posz0, n1.posz0) < n2.radius then -- the next node is ~ alligned with the current segment -- -- the ratio times the range gives the square speed of the projectile lauch speed for which we will not ecceed the given range -- local bumpSpeedSq = bumpSqSpeedToRangeRatio * max(n2.vec:length(), n1.radiusOrig) -- if n1SpeedSq > bumpSpeedSq then -- if bumpSpeedSq > n2SpeedSq then -- n1SpeedSq = (n2SpeedSq + 2 * n2.vec:length() * acc ) / (1 + 2 * (1/bumpSqSpeedToRangeRatio) * acc) -- else -- if nextCos < -0.999 then -- -- segment starting at node3speed is almost aligned with segment ending at node3speed -- n1SpeedSq = n2SpeedSq -- else -- -- so as not to go beyond n3 and also reach there with less speed than n3.speed (no room to brake, no need to brake) -- n1SpeedSq = bumpSpeedSq -- end -- end -- else -- n1SpeedSq <= bumpSpeedSq (will jump but how far and how much distance will there be to brake) -- -- at this speed there will be some flight but it will not reach n3 -- if n1SpeedSq > n2SpeedSq then -- -- some braking will be required and some traction is lost because of the distance we travel while in flight (how much?) -- -- find range of flight calculate distance that is available for braking and recalculate speed -- n1SpeedSq = (n2SpeedSq + 2 * n2.vec:length() * acc) / (1 + 2 * (1/bumpSqSpeedToRangeRatio) * acc) -- end -- end -- else -- do not take off -- n1SpeedSq = min(n1SpeedSq, bumpSqSpeedToRangeRatio * min(n2.vec:length(), n1.radiusOrig)) -- end -- end -- end n1SpeedSq = min(n1SpeedSq, n1.trafficSqVel or math.huge) n1.trafficSqVel = math.huge n1SpeedSq = sqrt(n1SpeedSq) -- if manSpeed or routeSpeed are nil/false then fall back to the calculated n1SpeedSq. -- if routeSpeed is true (has a value) and limitSpeed == false then impose routeSpeed on entire route -- if routeSpeed is true (has a value) and limitSpeed == true then cap route speed (set max speed limit) to routeSpeed n1.speed = n1.manSpeed or (routeSpeed and limitSpeed) and min(routeSpeed, n1SpeedSq) or limitSpeed ~= nil and routeSpeed or n1SpeedSq end local targetPos, targetSeg, aiXnormOnSeg = calculateTarget(plan) plan.targetPos = targetPos plan.targetSeg = targetSeg plan.aiSeg = 1 plan.aiXnormOnSeg = aiXnormOnSeg if plan[2] then plan.targetSpeed = plan[1].speed + aiXnormOnSeg * (plan[2].speed - plan[1].speed) else plan.targetSpeed = 0 end return plan end local function trafficSpeedLimit() -- smart speed limit based on road type -- TODO: only update this when plan pathidx gets updated if not currentRoute.plan then return end local damage = mapmgr.objects[objectId] and mapmgr.objects[objectId].damage or 0 if damage >= 2000 then if not damageOver then if math.random(1, 30) == 1 then -- random delay for hazard lights electrics.set_warn_signal(1) routeSpeed = 0 damageOver = true end end else if damageOver then routeSpeed = baseSpeed damageOver = false end local path = currentRoute.path local plan = currentRoute.plan local n1, n2 = map.nodes[path[plan[1].pathidx]], map.nodes[path[plan[1].pathidx + 1]] if not n2 then return end local link = n1.links[n2] or n2.links[n1] if link then local radius = n1.radius local oneWay = link.oneWay and 1.5 or 1 -- highways local drivability = link.drivability routeSpeed = baseSpeed * clamp(oneWay * ((radius + 5) / 10) * drivability, 0.4, 1.4) end end end local function useTurnSignal() -- checks if a turn signal is needed local path = currentRoute.path local plan = currentRoute.plan local turnDecision = 0 local maxLinks = 0 for i = plan[1].pathidx, min(#path - 1, plan[1].pathidx + 3) do local n1, n2 = map.nodes[path[i]], map.nodes[path[i + 1]] maxLinks = max(maxLinks, tableSize(n1.links)) if n1 and n2 then local turnDir = (n2.pos - n1.pos):z0():normalized() if aiDirVec:z0():normalized():dot(turnDir) < 0.5 then if aiRightVec:dot(turnDir) > 0 then turnDecision = 1 break else turnDecision = -1 break end end end end if maxLinks > 2 then if turnDecision > 0 then if electrics.values.turnsignal <= 0 then electrics.toggle_right_signal() end elseif turnDecision < 0 then if electrics.values.turnsignal >= 0 then electrics.toggle_left_signal() end end end end local function useHorn() if trafficBlock.horn >= 0.25 and trafficBlock.horn <= 0.75 then if trafficBlock.timer >= trafficBlock.limit * 0.9 then electrics.horn(1) else electrics.horn(0) end end end local function resetMapAndRoute() map = nil currentRoute = nil --targetPos = nil targetLength = nil --targetSeg = nil --aiSeg = 1 race = nil noOfLaps = nil internalState = 'onroad' changePlanTimer = 0 resetAggression() end local function getMapEdges(cutOffDrivability, node) -- creates a table (edgeDict) with map edges with drivability > cutOffDrivability if map.nodes ~= nil then local allSCC = mapmgr.getSCC(node) -- An array of dicts containing all strongly connected components reachable from 'node'. local maxSccLen = 0 local sccIdx for i, scc in ipairs(allSCC) do -- finds the scc with the most nodes local sccLen = scc[0] -- position at which the number of nodes in currentSCC is stored if sccLen > maxSccLen then sccIdx = i maxSccLen = sccLen end scc[0] = nil end local currentSCC = allSCC[sccIdx] local keySet = {} local keySetLen = 0 edgeDict = {} for nid, n in pairs(map.nodes) do if currentSCC[nid] or not driveInLaneFlag then for lid, data in pairs(n.links) do if (currentSCC[lid] or not driveInLaneFlag) and (data.drivability > cutOffDrivability) then local inNode = data.inNode local outNode = inNode == nid and lid or nid keySetLen = keySetLen + 1 keySet[keySetLen] = {inNode, outNode} edgeDict[inNode..'\0'..outNode] = 1 if not data.oneWay or not driveInLaneFlag then edgeDict[outNode..'\0'..inNode] = 1 end end end end end if keySetLen == 0 then return end local edge = keySet[math.random(keySetLen)] return edge[1], edge[2] end end local function newManualPath() local newRoute, n1, n2, dist local offRoad = false if currentRoute and currentRoute.path then newRoute = {plan = currentRoute.plan, path = currentRoute.path} else n1, n2, dist = mapmgr.findClosestRoad(aiPos) if n1 == nil or n2 == nil then gui.message("Could not find a road network, or closest road is too far", 5, "AI debug") return end if dist > 2 * max(map.nodes[n1].radius, map.nodes[n2].radius) then offRoad = true local vec1 = map.nodes[n1].pos - aiPos local vec2 = map.nodes[n2].pos - aiPos if aiDirVec:dot(vec1) > 0 and aiDirVec:dot(vec2) > 0 then if vec1:length() > vec2:length() then n1, n2 = n2, n1 end elseif aiDirVec:dot(map.nodes[n2].pos - map.nodes[n1].pos) > 0 then n1, n2 = n2, n1 end elseif aiDirVec:dot(map.nodes[n2].pos - map.nodes[n1].pos) > 0 then n1, n2 = n2, n1 end newRoute = {plan = {}, path = {n1}} end for i = 0, #wpList-1 do local wp1 = wpList[i] or newRoute.path[#newRoute.path] local wp2 = wpList[i+1] local route = mapmgr.getPath(wp1, wp2, driveInLaneFlag and 10e7 or 1) local routeLen = #route if routeLen == 0 or (routeLen == 1 and wp2 ~= wp1) then gui.message("Path between waypoints '".. wp1 .."' - '".. wp2 .."' Not Found", 7, "AI debug") return end for j = 2, routeLen do tableInsert(newRoute.path, route[j]) end end wpList = nil if not offRoad and #newRoute.path >= 3 and newRoute.path[2] == n2 then tableRemove(newRoute.path, 1) end currentRoute = newRoute end local function validateUserInput() validateInput = nop local wpListLen = #wpList local isValid = wpListLen >= 1 for i = 1, wpListLen do if map.nodes[wpList[i]] == nil then local nodeAlias = map.nodeAliases[wpList[i]] if nodeAlias and map.nodes[nodeAlias] then wpList[i] = nodeAlias else if isValid then gui.message("One or more of the waypoints were not found on the map. Check the game console for more info.", 6, "AI debug") print('The waypoints with the following names could not be found on the Map') isValid = false end print(wpList[i]) end end end return isValid end local function fleePlan() if currentRoute and currentRoute.plan[2].pathidx > #currentRoute.path * 0.7 and #currentRoute.path > 2 and targetName == nil and internalState ~= 'offroad' and (aiPos - player.pos):dot(aiDirVec) >= 0 and trafficMinProjSpeed > 3 then local cr1 = currentRoute.path[#currentRoute.path - 1] local cr2 = currentRoute.path[#currentRoute.path] planExtend(currentRoute, mapmgr.getFleePath(cr2, (map.nodes[cr2].pos - map.nodes[cr1].pos):normalized(), player.pos, getPlanLen(), 0.01, 0.01)) else local newRoute if changePlanTimer == 0 then local wp1, wp2 = mapmgr.findClosestRoad(aiPos) if wp1 == nil or wp2 == nil then internalState = 'offroad' return else internalState = 'onroad' end local dirVec if trafficMinProjSpeed < 3 then changePlanTimer = 5 dirVec = -aiDirVec else dirVec = aiDirVec end local startnode = pickAiWp(wp1, wp2, dirVec) if not targetName then -- flee without target newRoute = mapmgr.getFleePath(startnode, dirVec, player.pos, getPlanLen(), 0.01, 0.01) else -- flee to target newRoute = mapmgr.getPathAwayFrom(startnode, targetName, aiPos, player.pos) if next(newRoute) == nil then targetName = nil end end local newRouteLen = #newRoute if newRouteLen == 0 then internalState = 'offroad' return else internalState = 'onroad' end local tempPlan = planAhead(newRoute, currentRoute) if tempPlan then if currentRoute == nil or changePlanTimer > 0 then currentRoute = newRoute else local ai2pl = player.pos - aiPos local aiSeg = tempPlan.aiSeg local targetSpeed = tempPlan.targetSpeed --local targetSpeed = tempPlan[aiSeg].speed + tempPlan.aiXnormOnSeg * (tempPlan[aiSeg+1].speed - tempPlan[aiSeg].speed) if targetSpeed >= aiSpeed and targetsCompatible(currentRoute, newRoute) then currentRoute = newRoute end end end end if currentRoute ~= newRoute then planAhead(currentRoute) else changePlanTimer = max(1, changePlanTimer) end end end local function chasePlan(dt) local mapNodes = map.nodes local wp1, wp2, dist1 = mapmgr.findClosestRoad(aiPos) if wp1 == nil or wp2 == nil then internalState = 'offroad' return end if aiDirVec:dot(mapNodes[wp2].pos - mapNodes[wp1].pos) > 0 then wp1, wp2 = wp2, wp1 end local plwp1, plwp2, dist2 = mapmgr.findClosestRoad(player.pos) if plwp1 == nil or plwp2 == nil then internalState = 'offroad' return end if dist1 > max(mapNodes[wp1].radius, mapNodes[wp2].radius) * 2 and dist2 > max(mapNodes[plwp1].radius, mapNodes[plwp2].radius) * 2 then internalState = 'offroad' return end internalState = 'offroad' if player.dirVec:dot(mapNodes[plwp2].pos - mapNodes[plwp1].pos) > 0 then plwp1, plwp2 = plwp2, plwp1 end if plPrevVel and wp1 == plwp1 then local playerNodePos1 = mapNodes[plwp1].pos local segDir = (playerNodePos1 - mapNodes[plwp2].pos) local targetLineDir = vec3(-segDir.y, segDir.x, 0) local l1xn = closestLinePoints(playerNodePos1, playerNodePos1 + targetLineDir, player.pos, player.pos + player.dirVec) local tarPos = playerNodePos1 + targetLineDir * l1xn local p2Target = (tarPos - player.pos):normalized() local plVel2Target = player.vel:dot(p2Target) local plAccel = (plVel2Target - plPrevVel:dot(p2Target)) / dt local plTimeToTarget = (sqrt(max(plVel2Target * plVel2Target + 2 * plAccel * (tarPos - player.pos):length(), 0)) - plVel2Target) / (plAccel + 1e-30) local aiVel2Target = aiVel:dot((tarPos - aiPos):normalized()) local aiTimeToTarget = (tarPos - aiPos):length() / (aiVel2Target + 1e-30) if aiTimeToTarget < plTimeToTarget then internalState = 'tail' return else local newRoute = {} newRoute.path = {} local segRadius = mapNodes[plwp1].radius local playerPos = player.pos local playerDirVec = player.dirVec local vec1 = (aiPos - playerPos):z0() local vec2 = -playerDirVec:z0() newRoute.plan = { {pos = vec3(aiPos), posOrig = vec3(aiPos), posz0 = aiPos:z0(), vec = 8 * vec3(-aiDirVec), dirVec = vec3(-aiDirVec), turnDir = vec3(0,0,0), radius = 2, radiusOrig = 2, speed = 5, pathidx = 1}, {radius = segRadius, radiusOrig = segRadius, pos = vec3(playerPos), posz0 = playerPos:z0(), posOrig = vec3(playerPos), vec = vec1, dirVec = vec1:normalized(), turnDir = vec3(0,0,0), pathidx = 1, manSpeed = player.vel:length()} --[[ {radius = segRadius, radiusOrig = segRadius, pos = playerPos + 15*playerDirVec, posz0 = (playerPos + 15*playerDirVec):z0(), posOrig = playerPos + 15 * playerDirVec, vec = vec2, dirVec = vec2:normalized(), turnDir = vec3(0,0,0), pathidx = 2} ]]-- } currentRoute = newRoute planAhead(currentRoute) return end end if currentRoute and currentRoute.path[1] == wp1 and currentRoute.path[#currentRoute.path] == plwp1 then planAhead(currentRoute) return end local newRoute = mapmgr.getPath(pickAiWp(wp1, wp2), plwp1, driveInLaneFlag and 10e7 or 1) -- new plan same as old return if #newRoute >= 3 and newRoute[2] == wp2 then tableRemove(newRoute, 1) end local tempPlan = planAhead(newRoute, currentRoute) if tempPlan then if currentRoute == nil then currentRoute = newRoute else local aiSeg = tempPlan.aiSeg local targetSpeed = tempPlan[aiSeg].speed + tempPlan.aiXnormOnSeg * (tempPlan[min(aiSeg+1, #tempPlan)].speed - tempPlan[aiSeg].speed) if targetSpeed >= aiSpeed and (tempPlan.targetPos - currentRoute.plan.targetPos):dot(aiDirVec) >= 0 then currentRoute = newRoute end end end if currentRoute ~= newRoute then planAhead(currentRoute) end end local function warningAIDisabled(message) gui.message(message, 5, "AI debug") M.mode = 'disabled' M.updateGFX = nop resetMapAndRoute() stateChanged() end M.updateGFX = nop local function updateGFX(dt) if map == nil then if mapmgr.map == nil then return end map = mapmgr.map end -- local cgPos = obj:calcCenterOfGravity() -- aiPos:set(cgPos) -- aiPos.z = obj:getSurfaceHeightBelow(cgPos) local tmpPos = obj:getFrontPosition() aiPos:set(tmpPos) aiPos.z = max(aiPos.z - 1, obj:getSurfaceHeightBelow(tmpPos)) prevDirVec:set(aiDirVec) aiDirVec:set(obj:getDirectionVector()) aiUpVec:set(obj:getDirectionVectorUp()) aiVel:set(obj:getVelocity()) aiSpeed = aiVel:length() aiWidth = aiWidth or obj:getInitialWidth() aiLength = aiLength or obj:getInitialLength() aiRightVec = aiDirVec:cross(aiUpVec):normalized() staticFrictionCoef = 0.95 * obj:getStaticFrictionCoef() if trafficBlock.coef > 0 then trafficBlock.timer = trafficBlock.timer + dt * trafficBlock.coef else trafficBlock.timer = trafficBlock.timer * 0.8 end if max(lastCommand.throttle, lastCommand.throttle) > 0.5 and aiSpeed < 1 then aiCannotMoveTime = aiCannotMoveTime + dt else aiCannotMoveTime = 0 end if aiSpeed < 3 then trafficSide.timer = trafficSide.timer - dt trafficSide.cTimer = trafficSide.cTimer + dt if trafficSide.timer < 0 then trafficSide.timer = 5 trafficSide.side = -trafficSide.side end else trafficSide.cTimer = max(0, trafficSide.timer - dt) trafficSide.side = 1 end changePlanTimer = max(0, changePlanTimer - dt) ------------------ RANDOM MODE ---------------- if M.mode == 'random' then local newRoute if currentRoute == nil or currentRoute.plan[2].pathidx > #currentRoute.path * 0.5 then local wp1, wp2 = mapmgr.findClosestRoad(aiPos) if wp1 == nil or wp2 == nil then warningAIDisabled("Could not find a road network, or closest road is too far") return end if internalState == 'offroad' then local vec1 = map.nodes[wp1].pos - aiPos local vec2 = map.nodes[wp2].pos - aiPos if aiDirVec:dot(vec1) > 0 and aiDirVec:dot(vec2) > 0 then if vec1:length() > vec2:length() then wp1, wp2 = wp2, wp1 end elseif aiDirVec:dot(map.nodes[wp2].pos - map.nodes[wp1].pos) > 0 then wp1, wp2 = wp2, wp1 end elseif aiDirVec:dot(map.nodes[wp2].pos - map.nodes[wp1].pos) > 0 then wp1, wp2 = wp2, wp1 end newRoute = mapmgr.getRandomPath(wp1, wp2, driveInLaneFlag and 10e7 or 1) if newRoute and #newRoute > 0 then local tempPlan = planAhead(newRoute, currentRoute) if tempPlan then if not currentRoute then currentRoute = newRoute else local curPlanIdx = currentRoute.plan[2].pathidx local targetDir = tempPlan.targetPos:dot(aiDirVec) if curPlanIdx >= #currentRoute.path * 0.9 or ((curPlanIdx >= #currentRoute.path * 0.8 and targetDir >= 0) or (tempPlan.targetSpeed >= aiSpeed and targetDir >= 0)) then currentRoute = newRoute end end end end end if currentRoute ~= newRoute then planAhead(currentRoute) end if not currentRoute then return end ------------------ TRAFFIC MODE ---------------- elseif M.mode == 'traffic' then local newRoute if currentRoute == nil or currentRoute.plan[2].pathidx > #currentRoute.path * 0.4 or trafficBlock.timer > trafficBlock.limit then if currentRoute and currentRoute.plan[2].pathidx > #currentRoute.path * 0.7 and #currentRoute.path > 2 and internalState ~= 'offroad' and trafficBlock.timer <= trafficBlock.limit then local cr1 = currentRoute.path[#currentRoute.path - 1] local cr2 = currentRoute.path[#currentRoute.path] planExtend(currentRoute, mapmgr.getRandomPathG(cr2, (map.nodes[cr2].pos - map.nodes[cr1].pos):normalized(), getPlanLen(), 0.5, math.huge)) else local wp1, wp2 = mapmgr.findClosestRoad(aiPos) if wp1 == nil or wp2 == nil then warningAIDisabled("Could not find a road network, or closest road is too far") return end local dirVec if trafficBlock.timer > trafficBlock.limit then dirVec = -aiDirVec else dirVec = aiDirVec end if internalState == 'offroad' then local vec1 = map.nodes[wp1].pos - aiPos local vec2 = map.nodes[wp2].pos - aiPos if vec1:squaredLength() > vec2:squaredLength() then wp1 = wp2 end else wp1 = pickAiWp(wp1, wp2, dirVec) end -- local newRoute = mapmgr.getRandomPathG(wp1, aiDirVec, getPlanLen(), 0.4, 1 / (aiSpeed + 1e-30)) newRoute = mapmgr.getRandomPathG(wp1, dirVec, getPlanLen(), 0.5, math.huge) if newRoute and #newRoute > 0 then local tempPlan = planAhead(newRoute, currentRoute) if tempPlan then -- trafficBlock.horn = (math.random() + math.random() + math.random()) / 3 trafficBlock.limit = 4 + math.random() * 4 if not currentRoute or trafficBlock.timer > trafficBlock.limit then trafficBlock.timer = 0 currentRoute = newRoute else if tempPlan.targetSpeed >= aiSpeed and targetsCompatible(currentRoute, newRoute) then currentRoute = newRoute end end end end end end if currentRoute ~= newRoute then planAhead(currentRoute) end trafficSpeedLimit() useTurnSignal() -- useHorn() if not currentRoute then return end ------------------ MANUAL MODE ---------------- elseif M.mode == 'manual' then if validateInput() then newManualPath() end if aggressionMode == 'rubberBand' then updatePlayerData() if player ~= nil then if (aiPos - player.pos):dot(aiDirVec) > 0 then setAggressionInternal(max(min(0.1 + max((150 - (player.pos - aiPos):length())/150, 0), 1), 0.5)) else setAggressionInternal(0.95) end end end planAhead(currentRoute) if not currentRoute then return end ------------------ SPAN MODE ---------------- elseif M.mode == 'span' then if currentRoute == nil then local mapNodes = map.nodes local wpAft, wpFore = mapmgr.findClosestRoad(aiPos) if not (wpAft and wpFore) then warningAIDisabled("Could not find a road network, or closest road is too far") return end if aiDirVec:dot(mapNodes[wpFore].pos - mapNodes[wpAft].pos) < 0 then wpAft, wpFore = wpFore, wpAft end local target, targetLink if not edgeDict then -- creates the edgeDict and returns a random edge target, targetLink = getMapEdges(M.cutOffDrivability or 0, wpFore) if not target then warningAIDisabled("No available target with selected characteristics") return end end local newRoute = {} while true do if not target then local maxDist = -math.huge local lim = 1 repeat -- get most distant non walked edge for k, v in pairs(edgeDict) do if v <= lim then if lim > 1 then edgeDict[k] = 1 end local i = string.find(k, '\0') local n1id = string.sub(k, 1, i-1) local sqDist = (mapNodes[n1id].pos - aiPos):squaredLength() if sqDist > maxDist then maxDist = sqDist target = n1id targetLink = string.sub(k, i+1, #k) end end end lim = math.huge -- if the first iteration does not produce a target until target end -- of if not target local nodeDegree = 1 for k, v in pairs(mapNodes[target].links) do -- we're looking for neihboor nodes other than the targetLink if v ~= targetLink then nodeDegree = nodeDegree + 1 end end if nodeDegree == 1 then local key = target..'\0'..targetLink edgeDict[key] = edgeDict[key] + 1 end newRoute = mapmgr.spanMap(wpFore, wpAft, target, edgeDict, driveInLaneFlag and 10e7 or 1) local newRouteLen = #newRoute if newRouteLen <= 1 and wpFore ~= target then -- remove edge from edgeDict list and get a new target (while loop will iterate again) edgeDict[target..'\0'..targetLink] = nil edgeDict[targetLink..'\0'..target] = nil target = nil if next(edgeDict) == nil then warningAIDisabled("Could not find a path to any of the possible targets") return end elseif newRouteLen == 0 then warningAIDisabled("No Route Found") return else -- insert the second edge node in newRoute if it is not already contained if newRoute[newRouteLen-1] ~= targetLink then newRoute[newRouteLen+1] = targetLink end break end end -- of while if planAhead(newRoute) == nil then return end currentRoute = newRoute else --> if currentRoute ~= nil planAhead(currentRoute) end -- of "if currentroute == nil" ------------------ FLEE MODE ---------------- elseif M.mode == 'flee' then updatePlayerData() if player then if validateInput() then targetName = wpList[1] wpList = nil end setAggressionInternal(min(1, max(0.3, 1 - 0.002 * (player.pos - aiPos):length()))) -- setAggressionInternal(1) fleePlan() if internalState == 'offroad' then local targetPos = aiPos + (aiPos - player.pos) * 100 targetSpeed = math.huge driveToTarget(dt, targetPos, 1, 0) return end else --gui.message("No vehicle to Flee from", 5, "AI debug") -- TODO: this freezes the up because it runs on the gfx step return end if not currentRoute then return end ------------------ CHASE MODE ---------------- elseif M.mode == 'chase' then updatePlayerData() if player then setAggressionInternal(1000) chasePlan(dt) plPrevVel = vec3(player.vel) if internalState == 'tail' then internalState = 'onroad' currentRoute = nil local plai = player.pos - aiPos local relvel = aiVel:dot(plai) - player.vel:dot(plai) driveToTarget(dt, player.pos, 1, 0) return elseif internalState == 'offroad' then targetSpeed = math.huge driveToTarget(dt, player.pos, 1, 0) return end else --gui.message("No vehicle to Chase", 5, "AI debug") return end if not currentRoute then return end ------------------ STOP MODE ---------------- elseif M.mode == 'stop' then if aiVel:dot(aiDirVec) > 0 then driveCar(0, 0, 0.5, 0) else driveCar(0, 1, 0, 0) end if aiSpeed < 0.08 then -- or aiVel:dot(aiDirVec) < 0 driveCar(0, 0, 0, 0) -- only parkingbrake M.mode = 'disabled' M.manualTargetName = nil M.updateGFX = nop resetMapAndRoute() stateChanged() return end end if currentRoute then local plan = currentRoute.plan local targetPos = plan.targetPos local aiSeg = plan.aiSeg targetSpeed = plan.targetSpeed if not targetPos or ((targetSpeed < 0.05 and aiSpeed < 0.1 or (targetPos - aiPos):dot(aiDirVec) < 0) and aiSeg == #plan-1) then if M.mode == 'span' then local path = currentRoute.path for i = 1, #path - 1 do local key = path[i]..'\0'..path[i+1] -- in case we have gone over an edge that is not in the edgeDict list edgeDict[key] = edgeDict[key] and (edgeDict[key] * 20) end end driveCar(0, 0, 0, 1) aistatus('route done', 'route') gui.message("Route done", 5, "AI debug") currentRoute = nil speedRecordings = {} return end -- Throttle and Brake control local speedDif = aiSpeedSmoother:get(targetSpeed, dt) - aiSpeed local lowSpeedDif = (speedDif - clamp((aiSpeed - 2) * 0.5, 0, 1)) * 0.5 local throttle = clamp(lowSpeedDif, 0, 1) local brake = 0 if -lowSpeedDif > 0 and (prevBrake > 0 or -speedDif >= 0) then brake = clamp(-lowSpeedDif, 0 ,1) end prevBrake = brake -- TODO: this still runs if there is no currentPlan, but raises error if there is no targetSpeed if not controller.isFrozen and aiSpeed < 0.1 and targetSpeed and targetSpeed > 0.5 and (lastCommand.throttle ~= 0 or lastCommand.brake ~= 0) then crashTime = crashTime + dt if crashTime > 1 then crashDir = vec3(aiDirVec) crashManeuver = 1 end else crashTime = 0 end if not targetPos then driveCar(0, 0, 0, 1) else driveToTarget(dt, targetPos, throttle, brake) end end end local function debugDraw(focusPos) local debugDrawer = obj.debugDrawProxy if M.mode == 'script' and scriptai ~= nil then scriptai.debugDraw() end if currentRoute then local targetPos = currentRoute.plan.targetPos local targetSpeed = currentRoute.plan.targetSpeed if targetPos then local plan = currentRoute.plan --debugDrawer:drawSphere(0.25, (aiPos - aiDirVec*aiLength):toFloat3(), color(0,255,0,255)) --local aiXnormOnSeg = max(0, aiPos:xnormOnLine(plan[aiSeg].pos, plan[aiSeg+1].pos)) local aiXnormOnSeg = plan.aiXnormOnSeg local aiSeg = plan.aiSeg debugDrawer:drawSphere(0.25, targetPos:toFloat3(), color(255,0,0,255)) local shadowPos = currentRoute.plan[aiSeg].pos + aiXnormOnSeg*(plan[aiSeg+1].pos - plan[aiSeg].pos) debugDrawer:drawSphere(0.25, shadowPos:toFloat3(), color(0,0,255,255)) for plID, v in pairs(mapmgr.objects) do if plID ~= objectId then local plPosFront = vec3(obj:getObjectFrontPosition(plID)) debugDrawer:drawSphere(0.25, plPosFront:toFloat3(), color(0,0,255,255)) end end end if M.debugMode == 'target' then if map ~= nil and map.nodes and currentRoute.path then local p = map.nodes[currentRoute.path[#currentRoute.path]].pos:toFloat3() --debugDrawer:drawSphere(4, p, color(255,0,0,100)) --debugDrawer:drawText(p + float3(0, 0, 4), color(0,0,0,255), 'Destination') end elseif M.debugMode == 'route' then if currentRoute.path and next(map) ~= nil then local p = map.nodes[currentRoute.path[#currentRoute.path]].pos:toFloat3() debugDrawer:drawSphere(4, p, color(255,0,0,100)) debugDrawer:drawText(p + float3(0, 0, 4), color(0,0,0,255), 'Destination') end local maxLen = 700 local last = routeRec.last local len = min(#routeRec, maxLen) if len == 0 or (routeRec[last] - aiPos:toFloat3()):length() > 7 then last = 1 + last % maxLen routeRec[last] = aiPos:toFloat3() len = min(len+1, maxLen) routeRec.last = last end local fl3 = float3(0.7, aiWidth, 0.7) local col = color(0,0,0,128) for i = 1, len-1 do debugDrawer:drawSquarePrism(routeRec[1+(last+i-1)%len], routeRec[1+(last+i)%len], fl3, fl3, col) end if currentRoute.plan[1].pathidx then local mapNodes = map.nodes local path = currentRoute.path fl3 = fl3 + float3(0, aiWidth, 0) local col = color(255,0,0,120) for i = currentRoute.plan[1].pathidx, #path - 1 do debugDrawer:drawSquarePrism(mapNodes[path[i]].pos:toFloat3(), mapNodes[path[i+1]].pos:toFloat3(), fl3, fl3, col) end end elseif M.debugMode == 'speeds' then local plan = currentRoute.plan if plan and plan[1] then local red = color(255,0,0,200) -- getContrastColor(objectId) local prevPoint = plan[1].pos:toFloat3() local prevSpeed = -1 local drawLen = 0 for i = 1, #plan do local n = plan[i] local p = n.pos:toFloat3() local v = (n.speed >= 0 and n.speed) or prevSpeed local p1 = p + float3(0, 0, v*0.2) --debugDrawer:drawLine(p + float3(0, 0, v*0.2), (n.pos + n.turnDir):toFloat3() + float3(0, 0, v*0.2), col) debugDrawer:drawCylinder(p, p1, 0.03, red) debugDrawer:drawCylinder(prevPoint, p1, 0.05, red) debugDrawer:drawText(p1, color(0,0,0,255), strFormat("%2.0f", v*3.6) .. " kph") prevPoint = p1 prevSpeed = v --drawLen = drawLen + n.vec:length() -- if traffic and traffic[i] then -- for _, data in ipairs(traffic[i]) do -- local plPosOnPlan = linePointFromXnorm(n.pos, plan[i+1].pos, data[2]) -- debugDrawer:drawSphere(0.25, plPosOnPlan:toFloat3(), color(0,255,0,100)) -- end -- end if drawLen > 150 then break end end local aiPosFlt = aiPos:toFloat3() local speedRecLen = #speedRecordings if speedRecLen == 0 or (speedRecordings[speedRecLen][1]-aiPosFlt):length() > 0.25 then tableInsert(speedRecordings, {aiPosFlt, aiSpeed, targetSpeed, lastCommand.brake, lastCommand.throttle}) speedRecLen = speedRecLen + 1 end local zOffSet = float3(0,0,0.5) local lastEntry local yellow = color(255,255,0,200) local blue = color(0,0,255,200) for i = 1, speedRecLen do local v = speedRecordings[i] if lastEntry then -- actuall speed debugDrawer:drawCylinder(lastEntry[1]+float3(0,0,lastEntry[2]*0.2), v[1]+float3(0, 0, v[2]*0.2), 0.02, yellow) -- target speed debugDrawer:drawCylinder(lastEntry[1]+float3(0,0,lastEntry[3]*0.2), v[1]+float3(0, 0, v[3]*0.2), 0.02, blue) end debugDrawer:drawCylinder(v[1], v[1]+float3(0, 0, v[3]*0.2), 0.01, blue) if (focusPos - v[1]):length() < labelRenderDistance then debugDrawer:drawText(v[1]+float3(0, 0, v[2]*0.2)+zOffSet, yellow, strFormat("%2.0f", v[2]*3.6) .. " kph") debugDrawer:drawText(v[1]+float3(0, 0, v[3]*0.2)+zOffSet, blue, strFormat("%2.0f", v[3]*3.6) .. " kph") end lastEntry = v end if speedRecLen > 175 then tableRemove(speedRecordings, 1) end end -- Debug Throttle brake application local maxLen = 175 local len = min(#trajecRec, maxLen) local last = trajecRec.last local aiPosFlt = aiPos:toFloat3() if len == 0 or (trajecRec[last][1]-aiPosFlt):length() > 0.25 then last = 1 + last % maxLen trajecRec[last] = {aiPosFlt, lastCommand.throttle, lastCommand.brake} len = min(len+1, maxLen) trajecRec.last = last end local fl3 = float3(0.7, aiWidth, 0.7) for i = 1, len-1 do local n = trajecRec[1+(last+i)%len] debugDrawer:drawSquarePrism(trajecRec[1+(last+i-1)%len][1], n[1], fl3, fl3, color(255*sqrt(abs(n[3])), 255*sqrt(n[2]), 0, 100)) end elseif M.debugMode == 'trajectory' then -- Debug Curvatures -- local plan = currentRoute.plan -- if plan ~= nil then -- local prevPoint = plan[1].pos:toFloat3() -- for i = 1, #plan do -- local p = plan[i].pos:toFloat3() -- local v = plan[i].curvature or 1e-10 -- local scaledV = abs(1000 * v) -- debugDrawer:drawCylinder(p, p + float3(0, 0, scaledV), 0.06, color(abs(min(fsign(v),0))*255,max(fsign(v),0)*255,0,200)) -- debugDrawer:drawText(p + float3(0, 0, scaledV), color(0,0,0,255), strFormat("%5.4e", v)) -- debugDrawer:drawCylinder(prevPoint, p + float3(0, 0, scaledV), 0.06, col) -- prevPoint = p + float3(0, 0, scaledV) -- end -- end -- Debug Planned Speeds local plan = currentRoute.plan if plan and plan[1] then local col = getContrastColor(objectId) local prevPoint = plan[1].pos:toFloat3() local prevSpeed = -1 local drawLen = 0 for i = 1, #plan do local n = plan[i] local p = n.pos:toFloat3() local v = (n.speed >= 0 and n.speed) or prevSpeed local p1 = p + float3(0, 0, v*0.2) --debugDrawer:drawLine(p + float3(0, 0, v*0.2), (n.pos + n.turnDir):toFloat3() + float3(0, 0, v*0.2), col) debugDrawer:drawCylinder(p, p1, 0.03, col) debugDrawer:drawCylinder(prevPoint, p1, 0.05, col) debugDrawer:drawText(p1, color(0,0,0,255), strFormat("%2.0f", v*3.6) .. " kph") prevPoint = p1 prevSpeed = v drawLen = drawLen + n.vec:length() if drawLen > 80 then break end end end -- Debug Throttle brake application local maxLen = 175 local len = min(#trajecRec, maxLen) local last = trajecRec.last if len == 0 or (trajecRec[last][1] - aiPos:toFloat3()):length() > 0.25 then last = 1 + last % maxLen trajecRec[last] = {aiPos:toFloat3(), lastCommand.throttle, lastCommand.brake} len = min(len+1, maxLen) trajecRec.last = last end local fl3 = float3(0.7, aiWidth, 0.7) for i = 1, len-1 do local n = trajecRec[1+(last+i)%len] debugDrawer:drawSquarePrism(trajecRec[1+(last+i-1)%len][1], n[1], fl3, fl3, color(255*sqrt(abs(n[3])), 255*sqrt(n[2]), 0, 100)) end end end end local function init() map = {} end local function setAvoidCars(v) avoidCarsOverride = v end local function setMode(mode) local previousMode = M.mode if avoidCarsOverride == nil then avoidCars = mode == 'manual' and 'off' or 'on' else avoidCars = avoidCarsOverride and 'on' or 'off' end if mode ~= nil then M.mode = mode end if M.mode ~= 'script' then if M.mode ~= 'disabled' then resetMapAndRoute() end if M.mode ~= 'disabled' then -- TODO: this should be more explicit maybe? mapmgr.requestMap() M.updateGFX = updateGFX end if M.mode == 'disabled' then if previousMode ~= M.mode then driveCar(0, 0, 0, 0) end M.updateGFX = nop currentRoute = nil elseif M.mode ~= 'stop' then if controller.mainController then controller.mainController.setGearboxMode("arcade") end end end speedRecordings = {} trajecRec = {last = 0} routeRec = {last = 0} end local function reset() -- called when the user pressed I M.manualTargetName = nil if M.mode ~= 'disabled' then driveCar(0, 0, 0, 0) end setMode() -- some scenarios don't work if this is changed to setMode('disabled') stateChanged() end local function resetLearning() end local function setVehicleDebugMode(newMode) tableMerge(M, newMode) if M.debugMode ~= 'trajectory' then trajecRec = {last = 0} end if M.debugMode ~= 'route' then routeRec = {last = 0} end if M.debugMode ~= 'speed' then speedRecordings = {} end if M.debugMode ~= 'off' then M.debugDraw = debugDraw else M.debugDraw = nop end end local function setState(newState) tableMerge(M, newState) setAggressionExternal(M.extAggression) setMode() setVehicleDebugMode(M) setTargetObjectID(M.targetObjectID) end local function setTarget(wp) M.manualTargetName = wp validateInput = validateUserInput wpList = {wp} end local function driveInLane(v) if v == 'on' then driveInLaneFlag = true else driveInLaneFlag = false end end -- e.g. ai.driveUsingPath{ wpTargetList = {'wp1', 'wp10'}, wpSpeeds = {wp1 = 10, wp2 = 20}, noOfLaps = 3, aggression = 0.9, driveInLane = false, avoidCars = 'off/on/ram' } -- all arguments except wpTargetList are optional local function driveUsingPath(arg) if (arg.wpTargetList == nil and arg.script == nil) or (type(arg.wpTargetList) ~= 'table' and type(arg.script) ~= 'table') or (arg.wpSpeeds ~= nil and type(arg.wpSpeeds) ~= 'table') or (arg.noOfLaps ~= nil and type(arg.noOfLaps) ~= 'number') or (arg.routeSpeed ~= nil and type(arg.routeSpeed) ~= 'number') or (arg.routeSpeedMode ~= nil and type(arg.routeSpeedMode) ~= 'string') or (arg.driveInLane ~= nil and type(arg.driveInLane) ~= 'string') or (arg.aggression ~= nil and type(arg.aggression) ~= 'number') then return end if arg.resetLearning then resetLearning() end setState({mode = 'manual'}) noOfLaps = arg.noOfLaps and max(arg.noOfLaps, 1) or 1 wpList = arg.wpTargetList validateInput = validateUserInput avoidCars = arg.avoidCars or 'off' if noOfLaps > 1 and #wpList > 1 and wpList[1] == wpList[#wpList] then race = true end speedList = arg.wpSpeeds or {} setSpeed(arg.routeSpeed) setSpeedMode(arg.routeSpeedMode) driveInLane(arg.driveInLane) setAggressionExternal(arg.aggression) stateChanged() end local function spanMap(cutOffDrivability) M.cutOffDrivability = cutOffDrivability or 0 setState({mode = 'span'}) stateChanged() end local function setCutOffDrivability(drivability) M.cutOffDrivability = drivability or 0 stateChanged() end local function onDeserialized(v) setState(v) stateChanged() end local function dumpCurrentRoute() dump(currentRoute) end local function startRecording() M.mode = 'script' scriptai = require("scriptai") scriptai.startRecording() M.updateGFX = scriptai.updateGFX end local function stopRecording() M.mode = 'disabled' scriptai = require("scriptai") local script = scriptai.stopRecording() M.updateGFX = scriptai.updateGFX return script end local function startFollowing(...) M.mode = 'script' scriptai = require("scriptai") scriptai.startFollowing(...) M.updateGFX = scriptai.updateGFX end local function scriptStop(...) M.mode = 'disabled' scriptai = require("scriptai") scriptai.scriptStop(...) M.updateGFX = scriptai.updateGFX end local function scriptState() scriptai = require("scriptai") return scriptai.scriptState() end local function setScriptDebugMode(mode) scriptai = require("scriptai") if mode == nil or mode == 'off' then M.debugMode = 'all' M.debugDraw = nop return end M.debugDraw = debugDraw scriptai.debugMode = mode end local function isDriving() return M.updateGFX == updateGFX or (scriptai ~= nil and scriptai.isDriving()) end -- public interface M.driveInLane = driveInLane M.stateChanged = stateChanged M.reset = reset M.init = init M.setMode = setMode M.setAvoidCars = setAvoidCars M.setTarget = setTarget M.setSpeed = setSpeed M.setSpeedMode = setSpeedMode M.setVehicleDebugMode = setVehicleDebugMode M.setState = setState M.getState = getState M.debugDraw = nop M.driveUsingPath = driveUsingPath M.setAggressionMode = setAggressionMode M.setAggression = setAggressionExternal M.onDeserialized = onDeserialized M.setTargetObjectID = setTargetObjectID M.dumpCurrentRoute = dumpCurrentRoute M.spanMap = spanMap M.setCutOffDrivability = setCutOffDrivability M.resetLearning = resetLearning M.isDriving = isDriving -- scriptai M.startRecording = startRecording M.stopRecording = stopRecording M.startFollowing = startFollowing M.stopFollowing = scriptStop M.scriptStop = scriptStop M.scriptState = scriptState M.setScriptDebugMode = setScriptDebugMode return M