local ManeuverBase = require('ManeuverBase') local TrafficContext = require('TrafficContext') local LaneChangeManeuver = require('LaneChangeManeuver') local TrafficConfig = require('TrafficConfig') local DistanceTags = require('DistanceTags') ---@class TrafficGuide ---@field path TrafficPath ---@field driver TrafficDriver ---@field _curCursor LaneCursor ---@field _curManeuver ManeuverBase ---@field _meta EdgeMeta local TrafficGuide = class('TrafficGuide', class.NoInitialize) ---@param path TrafficPath ---@param driver TrafficDriver ---@return TrafficGuide function TrafficGuide.allocate(path, driver) local nextLane, nextLanePos = path:next() if nextLane == nil then error('Invalid state of the path') end local cursor = nextLane:startCursor(driver, nextLanePos) return { path = path, driver = driver, maneuvering = ManeuverBase.ManeuverNone, _curCursor = cursor, _curManeuver = nil, _nextLane = nil, _nextLanePos = 0, _swapTimer = 0 } end -- Full detach, destroying all lane cursors ---@param reason string|nil|boolean @False if it’s an expected detachment which does not require to be checked if it happened too close to camera function TrafficGuide:detach(reason) if reason ~= false and self.driver:getPosRef():closerToThan(ac.getSim().cameraPosition, 200) then if not reason then reason = '?' end ac.warn('Early detachment nearby: reason='..tostring(reason)..', lane='..(self._curCursor and self._curCursor.lane.name or '?')) if DebugShapes then DebugShapes['Early detachment ('..(self._curCursor and self._curCursor.lane.name or '?')..')'] = self.driver:getPosRef():clone() end end if self._curManeuver ~= nil then self._curManeuver:detach() end if self._curCursor ~= nil then self._curCursor:detach() end return true end local _tmpVec = vec3() -- Advancing along path (returns true if path is finished and driver has arrived to their destination point) function TrafficGuide:advance(speedKmh, dt) local car = self:getDriver():getCar() if TrafficConfig.debugSpawnAround then if car and car:distanceToCameraSquared() > 400*400 then return self:detach('Too far') end elseif car and car:distanceToCameraSquared() > (car:crashed() and TrafficConfig.despawnCrashedDistance * TrafficConfig.despawnCrashedDistance or TrafficConfig.despawnDistance * TrafficConfig.despawnDistance) then return self:detach('Too far') end -- First, as we drive towards an intersection, let’s find it if nothing is set yet local _curCursor = self._curCursor if _curCursor ~= nil then if _curCursor:advance(speedKmh, dt) then _curCursor = nil self._curCursor = nil if self._curManeuver == nil then return self:detach('Lane is ended') else self._curManeuver:ensureActive() end else self._meta = _curCursor.edgeMeta end -- Calculate distance to it local distanceToNextLink = self._meta.distanceToNextLink -- If nearby, let’s engage with it if distanceToNextLink > 0 and distanceToNextLink < 24 and self._curManeuver == nil then -- self._nextLane, self._nextLanePos = self.path:next(self._meta.dir) self._nextLane, self._nextLanePos = self.path:next(self.driver:getDirRef() or self._meta.dir) if self._nextLane == nil then return self:detach('No lane to use after maneuver') end local nextLink = self._meta.nextLink self._curManeuver = nextLink.intersection:engage(self, _curCursor.lane, nextLink.from, self._nextLane, self._nextLanePos) end end -- If engaged, let’s check if we can proceed if self._curManeuver ~= nil then if self._curManeuver:advance(speedKmh, dt) then if _curCursor ~= nil then _curCursor:detach() _curCursor = nil end if self._nextCursor ~= nil then -- some maneuvers can end up calling :adoptCurrentCursor() so next cursor would already be set _curCursor = self._nextCursor self._nextCursor = nil self._curCursor = _curCursor self.path:changeCurrentTo(_curCursor.lane, _curCursor.distance) if _curCursor.lane == nil then error('Invalid state: adopted cursor is detached') end elseif self._nextLane == nil then return self:detach() else self._curManeuver:calculateCurrentPosInto(_tmpVec) _curCursor = self._nextLane:startCursor(self.driver, self._nextLanePos, _tmpVec) self._curCursor = _curCursor end self._curManeuver = nil self.maneuvering = ManeuverBase.ManeuverNone -- self.driver.pauseFor = 5e9 self._swapTimer = -10 elseif _curCursor ~= nil and self._curManeuver:shouldDetachFromLane() then _curCursor:detach() self._curCursor = nil end elseif speedKmh < 1 and self._meta.distanceToNextLink > 40 then local _swapTimer = self._swapTimer + dt if _swapTimer > 1 then _swapTimer = 0 self:_tryToChangeLane() end self._swapTimer = _swapTimer else self._swapTimer = 0 end return false end function TrafficGuide:_tryToChangeLane() local path = self.path if not path:canChange() or not self.driver.car or not self._curCursor or math.random() < 0.3 then return end local d, n = self:distanceToNextCar() if n and self.driver.car:freeDistanceTo(n, self.driver.car._transform.side) < 2 then return end local newLane = LaneChangeManeuver.findAlternativeLane(self.driver.car, self._curCursor.lane, self._meta) if newLane ~= nil then self._curManeuver = LaneChangeManeuver.tryCreate(self, newLane, self._meta) self.driver.car.stationaryFor = 0 end end ---@param car1 CarBase ---@param car2 CarBase local function checkBlocking(car1, car2) return car2:freeDistanceTo(car1) end local tmpVec = vec3() ---@return number, CarBase|nil, DistanceTag function TrafficGuide:distanceToNextCar() -- if self.index == 2 then return 0, nil end local rd, rc, rt local maneuver = self._curManeuver if maneuver ~= nil and maneuver:handlesDistanceToNext() then rd, rc, rt = maneuver:distanceToNextCar() elseif self._curCursor == nil then rd, rc, rt = 0, nil, DistanceTags.ErrorCursorless else rd, rc, rt = self._curCursor:distanceToNextCar() if maneuver ~= nil then local md, mc, mt = maneuver:distanceToNextCar() if md < rd then rd, rc, rt = md, mc, mt end end end if maneuver == nil or not maneuver:handlesDistanceToBlocking() then local ownCar = self.driver:getCar() if ownCar == nil then return 0, nil, DistanceTags.ErrorNoCar end local bd, bc = TrafficContext.trackerBlocking:findNearest(self.driver.pos, checkBlocking, ownCar) if bc ~= nil and bc:getDirRef():dot(self.driver:getDirRef()) < 0.5 and tmpVec:set(bc:getPosRef()):sub(self.driver:getPosRef()):normalize():dot(self.driver:getDirRef()) > 0.5 then bd = bd - 4 end if bd < rd then return bd, bc, DistanceTags.Blocking end end return rd, rc, rt end function TrafficGuide:canChange() return self.path:canChange() end ---@param newLane TrafficLane ---@param newLanePos number function TrafficGuide:changeNextTo(newLane, newLanePos) self.path:changeNextTo(newLane, newLanePos) self._nextLane = newLane self._nextLanePos = newLanePos end ---@param newCursor LaneCursor function TrafficGuide:adoptCurrentCursor(newCursor) if self._nextCursor ~= nil then self._nextCursor:detach() end self._nextCursor = newCursor end function TrafficGuide:getDistance() return self._curCursor.distance end function TrafficGuide:getDriver() return self.driver end ---@return LaneCursor function TrafficGuide:stealCurrentLaneCursor() local r = self._curCursor self._curCursor = nil return r end function TrafficGuide:calculateCurrentPosInto(v, estimate) local maneuver = self._curManeuver if not maneuver and not self._curCursor then error('Cursorless guide') end if maneuver and maneuver:calculateCurrentPosInto(v, estimate) then self.maneuvering = maneuver:getManeuverType() return v end return self._curCursor:calculateCurrentPosInto(v, estimate) end function TrafficGuide:getMeta() return self._meta end return class.emmy(TrafficGuide, TrafficGuide.allocate)