diff --git a/modDesc.xml b/modDesc.xml
index 8bebc44..7052167 100644
--- a/modDesc.xml
+++ b/modDesc.xml
@@ -217,6 +217,7 @@ Version 2.0.26.0 :
+
diff --git a/scripts/HP_AutoDriveContinuity.lua b/scripts/HP_AutoDriveContinuity.lua
new file mode 100644
index 0000000..961f0e8
--- /dev/null
+++ b/scripts/HP_AutoDriveContinuity.lua
@@ -0,0 +1,387 @@
+-- HP_AutoDriveContinuity.lua (FS25_HelperProfiles)
+-- AutoDrive helper continuity bridge.
+--
+-- V5 uses HelperProfiles' proven worker-appearance assignment hook as the
+-- authoritative vehicle<->helper relationship. HP_WorkerAppearance already sees
+-- Enterable.setRandomVehicleCharacter(vehicle, helper) in the live game and stores
+-- that exact pair in vehicleAssignments. We retain that assignment across
+-- AutoDrive's internal release/reacquire cycle without depending on AutoDrive's
+-- private event globals.
+
+print("[FS25_HelperProfiles/AutoDriveV5] Source loaded (worker-assignment continuity build)")
+
+-- Disable the original polling prototype in HP_Compatibility.lua. This module owns
+-- AutoDrive continuity on this branch.
+if HP_AutoDriveContinuity ~= nil then
+ HP_AutoDriveContinuity.update = function() end
+end
+
+HP_AutoDriveContinuityV5 = HP_AutoDriveContinuityV5 or {
+ installed = false,
+ reservations = setmetatable({}, {__mode = "k"}),
+ pendingByVehicle = setmetatable({}, {__mode = "k"}),
+ originalGetRandomHelper = nil,
+ originalReleaseHelper = nil,
+ originalIsHelperActive = nil,
+ runtimeManager = nil,
+ _lastWaitReason = nil,
+ _lastWaitLogMs = -100000
+}
+
+local LOG = "[FS25_HelperProfiles/AutoDriveV5] "
+
+local function log(message, ...)
+ print(LOG .. string.format(tostring(message), ...))
+end
+
+local function nowMs()
+ return tonumber(g_time) or 0
+end
+
+local function vehicleName(vehicle)
+ if vehicle ~= nil and type(vehicle.getFullName) == "function" then
+ local ok, value = pcall(vehicle.getFullName, vehicle)
+ if ok and value ~= nil and tostring(value) ~= "" then
+ return tostring(value)
+ end
+ end
+ if vehicle ~= nil and type(vehicle.getName) == "function" then
+ local ok, value = pcall(vehicle.getName, vehicle)
+ if ok and value ~= nil and tostring(value) ~= "" then
+ return tostring(value)
+ end
+ end
+ return tostring(vehicle or "unknown-vehicle")
+end
+
+local function helperName(helper)
+ return tostring(helper ~= nil and helper.name or "?")
+end
+
+local function isAutoDriveVehicle(vehicle)
+ return vehicle ~= nil and vehicle.ad ~= nil and vehicle.ad.stateModule ~= nil
+end
+
+local function isAutoDriveActive(vehicle)
+ if not isAutoDriveVehicle(vehicle) then
+ return false
+ end
+
+ local stateModule = vehicle.ad.stateModule
+ if type(stateModule.isActive) ~= "function" then
+ return false
+ end
+
+ local ok, value = pcall(stateModule.isActive, stateModule)
+ return ok and value == true
+end
+
+local function helperIsFree(helper)
+ if helper == nil then
+ return false
+ end
+
+ -- releaseHelper() has already completed before AutoDrive synchronously calls
+ -- getRandomHelper() again. Do not require membership in availableHelpers here:
+ -- HelperProfiles/roster filtering may proxy that table, while helper.inUse is
+ -- the direct ownership state we need for this tiny transition window.
+ return helper.inUse ~= true
+end
+
+function HP_AutoDriveContinuityV5:_logInstallWait(reason)
+ local now = nowMs()
+ reason = tostring(reason or "unknown")
+ if self._lastWaitReason ~= reason or (now - (tonumber(self._lastWaitLogMs) or 0)) >= 10000 then
+ self._lastWaitReason = reason
+ self._lastWaitLogMs = now
+ log("Waiting to install: %s", reason)
+ end
+end
+
+function HP_AutoDriveContinuityV5:_reserve(vehicle, helper, reason)
+ if vehicle == nil or helper == nil then
+ return false
+ end
+
+ local previous = self.reservations[vehicle]
+ if previous ~= nil and previous.helper == helper then
+ previous.helperIndex = tonumber(helper.index) or previous.helperIndex or 0
+ return true
+ end
+
+ self.reservations[vehicle] = {
+ helper = helper,
+ helperIndex = tonumber(helper.index) or 0,
+ observedAt = nowMs()
+ }
+
+ log(
+ "Driver session reserved: vehicle='%s' helper='%s' index=%d reason=%s",
+ vehicleName(vehicle),
+ helperName(helper),
+ tonumber(helper.index) or 0,
+ tostring(reason or "unknown")
+ )
+ return true
+end
+
+function HP_AutoDriveContinuityV5:_clear(vehicle, reason)
+ local reservation = vehicle ~= nil and self.reservations[vehicle] or nil
+ if reservation == nil then
+ self.pendingByVehicle[vehicle] = nil
+ return false
+ end
+
+ log(
+ "Driver session cleared: vehicle='%s' helper='%s' reason=%s",
+ vehicleName(vehicle),
+ helperName(reservation.helper),
+ tostring(reason or "unknown")
+ )
+
+ self.reservations[vehicle] = nil
+ self.pendingByVehicle[vehicle] = nil
+ return true
+end
+
+function HP_AutoDriveContinuityV5:isReserved(helper)
+ if helper == nil then
+ return false
+ end
+
+ for _, reservation in pairs(self.reservations or {}) do
+ if reservation ~= nil and reservation.helper == helper then
+ return true
+ end
+ end
+ return false
+end
+
+function HP_AutoDriveContinuityV5:_syncWorkerAssignments()
+ if HP_WorkerAppearance == nil or type(HP_WorkerAppearance.vehicleAssignments) ~= "table" then
+ return
+ end
+
+ for vehicle, assignment in pairs(HP_WorkerAppearance.vehicleAssignments) do
+ local helper = assignment ~= nil and assignment.helper or nil
+ if vehicle ~= nil and helper ~= nil and isAutoDriveVehicle(vehicle) and isAutoDriveActive(vehicle) then
+ local existing = self.reservations[vehicle]
+ local pending = self.pendingByVehicle[vehicle]
+
+ -- During a continuity transition, never let a later appearance update
+ -- replace the reserved owner before getRandomHelper has had a chance to
+ -- return that owner. In the normal path this branch is never needed,
+ -- because getRandomHelper is intercepted first.
+ if existing ~= nil and pending ~= nil and existing.helper ~= helper then
+ log(
+ "Ignoring replacement assignment while continuity is pending: vehicle='%s' reserved='%s' observed='%s'",
+ vehicleName(vehicle),
+ helperName(existing.helper),
+ helperName(helper)
+ )
+ else
+ self:_reserve(vehicle, helper, "worker-appearance-assignment")
+ end
+ end
+ end
+end
+
+function HP_AutoDriveContinuityV5:_findReservedVehicleForHelper(helper)
+ if helper == nil then
+ return nil
+ end
+
+ local match = nil
+ local matches = 0
+ for vehicle, reservation in pairs(self.reservations or {}) do
+ if reservation ~= nil and reservation.helper == helper then
+ match = vehicle
+ matches = matches + 1
+ end
+ end
+
+ if matches == 1 then
+ return match
+ end
+ if matches > 1 then
+ log("Release mapping ambiguous: helper='%s' has %d reserved AutoDrive vehicles", helperName(helper), matches)
+ end
+ return nil
+end
+
+function HP_AutoDriveContinuityV5:_observeRelease(helper)
+ local vehicle = self:_findReservedVehicleForHelper(helper)
+ if vehicle == nil then
+ return false
+ end
+
+ self.pendingByVehicle[vehicle] = {
+ helper = helper,
+ releasedAt = nowMs()
+ }
+
+ log(
+ "Driver release captured: vehicle='%s' helper='%s' adActive=%s; retaining reservation for synchronous restart",
+ vehicleName(vehicle),
+ helperName(helper),
+ tostring(isAutoDriveActive(vehicle))
+ )
+ return true
+end
+
+function HP_AutoDriveContinuityV5:_getPendingReacquire()
+ local matchedVehicle = nil
+ local matchedHelper = nil
+ local matches = 0
+
+ for vehicle, pending in pairs(self.pendingByVehicle or {}) do
+ local reservation = self.reservations[vehicle]
+ local helper = pending ~= nil and pending.helper or nil
+
+ if reservation ~= nil and helper ~= nil and reservation.helper == helper and isAutoDriveActive(vehicle) then
+ if helperIsFree(helper) then
+ matchedVehicle = vehicle
+ matchedHelper = helper
+ matches = matches + 1
+ else
+ log(
+ "Pending restart found but helper still in use: vehicle='%s' helper='%s'",
+ vehicleName(vehicle),
+ helperName(helper)
+ )
+ end
+ end
+ end
+
+ if matches == 1 then
+ self.pendingByVehicle[matchedVehicle] = nil
+ log(
+ "Driver continuity reacquire: vehicle='%s' helper='%s' index=%d reason=release-restart",
+ vehicleName(matchedVehicle),
+ helperName(matchedHelper),
+ tonumber(matchedHelper.index) or 0
+ )
+ return matchedHelper, matchedVehicle
+ end
+
+ if matches > 1 then
+ log("Continuity skipped: %d released AutoDrive vehicles are simultaneously requesting helpers", matches)
+ end
+ return nil, nil
+end
+
+function HP_AutoDriveContinuityV5:_expireStoppedPending()
+ for vehicle, pending in pairs(self.pendingByVehicle or {}) do
+ if pending ~= nil and not isAutoDriveActive(vehicle) then
+ -- AutoDrive's internal RestartADTask restarts synchronously. If we have
+ -- reached a later update frame and the vehicle is still inactive, this
+ -- was a genuine stop rather than the temporary release/reacquire cycle.
+ self:_clear(vehicle, "autodrive-stopped-no-synchronous-restart")
+ end
+ end
+end
+
+function HP_AutoDriveContinuityV5:install()
+ if self.installed then
+ return true
+ end
+
+ if HelperProfiles == nil then
+ self:_logInstallWait("HelperProfiles global unavailable")
+ return false
+ end
+ if HelperProfiles._hooksDone ~= true then
+ self:_logInstallWait("HelperProfiles getRandomHelper hook not ready")
+ return false
+ end
+ if HP_WorkerAppearance == nil or type(HP_WorkerAppearance.vehicleAssignments) ~= "table" then
+ self:_logInstallWait("HP_WorkerAppearance.vehicleAssignments unavailable")
+ return false
+ end
+
+ local runtimeManager = g_helperManager
+ if runtimeManager == nil then
+ self:_logInstallWait("g_helperManager unavailable")
+ return false
+ end
+ if type(runtimeManager.getRandomHelper) ~= "function" then
+ self:_logInstallWait("g_helperManager.getRandomHelper unavailable (type=" .. tostring(type(runtimeManager.getRandomHelper)) .. ")")
+ return false
+ end
+ if type(runtimeManager.releaseHelper) ~= "function" then
+ self:_logInstallWait("g_helperManager.releaseHelper unavailable (type=" .. tostring(type(runtimeManager.releaseHelper)) .. ")")
+ return false
+ end
+
+ self.runtimeManager = runtimeManager
+
+ self.originalGetRandomHelper = runtimeManager.getRandomHelper
+ runtimeManager.getRandomHelper = function(manager, ...)
+ local helper = HP_AutoDriveContinuityV5:_getPendingReacquire()
+ if helper ~= nil then
+ print(("[FS25_HelperProfiles] getRandomHelper -> '%s' (autodrive-worker-continuity)"):format(helperName(helper)))
+ return helper
+ end
+ return HP_AutoDriveContinuityV5.originalGetRandomHelper(manager, ...)
+ end
+
+ self.originalReleaseHelper = runtimeManager.releaseHelper
+ runtimeManager.releaseHelper = function(manager, helper, ...)
+ HP_AutoDriveContinuityV5:_observeRelease(helper)
+ return HP_AutoDriveContinuityV5.originalReleaseHelper(manager, helper, ...)
+ end
+
+ if type(HelperProfiles.isHelperActive) == "function" then
+ self.originalIsHelperActive = HelperProfiles.isHelperActive
+ HelperProfiles.isHelperActive = function(helperProfilesSelf, helper)
+ if HP_AutoDriveContinuityV5:isReserved(helper) then
+ return true
+ end
+ return HP_AutoDriveContinuityV5.originalIsHelperActive(helperProfilesSelf, helper)
+ end
+ end
+
+ self.installed = true
+ self._lastWaitReason = nil
+ log("Installed worker-assignment continuity hooks (getRandomHelper + releaseHelper + activity bridge)")
+ return true
+end
+
+function HP_AutoDriveContinuityV5:loadMap()
+ self.reservations = setmetatable({}, {__mode = "k"})
+ self.pendingByVehicle = setmetatable({}, {__mode = "k"})
+ self._lastWaitReason = nil
+ self._lastWaitLogMs = -100000
+end
+
+function HP_AutoDriveContinuityV5:update(dt)
+ if HP_Compatibility ~= nil and HP_Compatibility:isBlocked() then
+ return
+ end
+
+ if not self.installed then
+ if not self:install() then
+ return
+ end
+ end
+
+ self:_expireStoppedPending()
+ self:_syncWorkerAssignments()
+end
+
+function HP_AutoDriveContinuityV5:deleteMap()
+ self.reservations = setmetatable({}, {__mode = "k"})
+ self.pendingByVehicle = setmetatable({}, {__mode = "k"})
+end
+
+-- Keep payroll accounting optional and outside the continuity algorithm. The
+-- bridge is sourced here so older modDesc files on this feature branch do not
+-- need a new load-order dependency; failure to load it must never disable V5.
+if source ~= nil and g_currentModDirectory ~= nil then
+ local ok, err = pcall(source, g_currentModDirectory .. "scripts/HP_AutoDrivePayrollBridge.lua")
+ if not ok then
+ log("Optional HelperPayroll bridge failed to load: %s", tostring(err))
+ end
+end
+
+addModEventListener(HP_AutoDriveContinuityV5)
diff --git a/scripts/HP_AutoDrivePayrollBridge.lua b/scripts/HP_AutoDrivePayrollBridge.lua
new file mode 100644
index 0000000..796c992
--- /dev/null
+++ b/scripts/HP_AutoDrivePayrollBridge.lua
@@ -0,0 +1,236 @@
+-- HP_AutoDrivePayrollBridge.lua (FS25_HelperProfiles)
+-- Optional HelperPayroll bridge for AutoDrive continuity sessions.
+--
+-- HP_AutoDriveContinuityV5 owns the authoritative AutoDrive vehicle/helper
+-- reservation. This bridge mirrors only that logical reservation lifecycle into
+-- HelperPayroll's generic external-worker-session API. AutoDrive's internal
+-- release/reacquire cycles never end the payroll session because the V5
+-- reservation deliberately survives those transitions.
+
+HP_AutoDrivePayrollBridge = HP_AutoDrivePayrollBridge or {
+ activeByVehicle = setmetatable({}, {__mode = "k"}),
+ sessionSequence = 0,
+ _lastWaitReason = nil,
+ _lastWaitLogMs = -100000
+}
+
+local LOG = "[FS25_HelperProfiles/AutoDrivePayroll] "
+
+local function log(message, ...)
+ print(LOG .. string.format(tostring(message), ...))
+end
+
+local function nowMs()
+ return tonumber(g_time) or 0
+end
+
+local function vehicleName(vehicle)
+ if vehicle ~= nil and type(vehicle.getFullName) == "function" then
+ local ok, value = pcall(vehicle.getFullName, vehicle)
+ if ok and value ~= nil and tostring(value) ~= "" then return tostring(value) end
+ end
+ if vehicle ~= nil and type(vehicle.getName) == "function" then
+ local ok, value = pcall(vehicle.getName, vehicle)
+ if ok and value ~= nil and tostring(value) ~= "" then return tostring(value) end
+ end
+ return tostring(vehicle or "unknown-vehicle")
+end
+
+local function helperName(helper)
+ return tostring(helper ~= nil and helper.name or "?")
+end
+
+local function helperSlot(helper)
+ local index = math.floor(tonumber(helper ~= nil and helper.index or 0) or 0)
+ if index < 1 or index > 20 then return nil end
+ return string.char(string.byte("A") + index - 1)
+end
+
+local function ownerFarmId(vehicle)
+ if vehicle ~= nil and type(vehicle.getOwnerFarmId) == "function" then
+ local ok, value = pcall(vehicle.getOwnerFarmId, vehicle)
+ if ok and tonumber(value) ~= nil then return tonumber(value) end
+ end
+ if vehicle ~= nil and tonumber(vehicle.ownerFarmId) ~= nil then
+ return tonumber(vehicle.ownerFarmId)
+ end
+ return nil
+end
+
+function HP_AutoDrivePayrollBridge:_logWait(reason)
+ local now = nowMs()
+ reason = tostring(reason or "unknown")
+ if self._lastWaitReason ~= reason or now - (tonumber(self._lastWaitLogMs) or 0) >= 10000 then
+ self._lastWaitReason = reason
+ self._lastWaitLogMs = now
+ log("Waiting: %s", reason)
+ end
+end
+
+function HP_AutoDrivePayrollBridge:_getPayrollAPI()
+ local api = nil
+
+ if g_currentMission ~= nil then
+ api = g_currentMission.fs25HelperPayrollAPI or g_currentMission.helperPayrollAPI
+ end
+
+ if type(api) ~= "table" then
+ local ok, value = pcall(function()
+ return FS25_HelperPayroll_API or FS25_HelperPayrollAPI
+ end)
+ if ok then api = value end
+ end
+
+ if type(api) ~= "table" then return nil, "HelperPayroll API unavailable" end
+ if type(api.beginExternalWorkerSession) ~= "function" then return nil, "HelperPayroll external-session API unavailable" end
+ if type(api.endExternalWorkerSession) ~= "function" then return nil, "HelperPayroll external-session finish API unavailable" end
+ if type(api.capabilities) == "table" and api.capabilities.externalWorkerSessions == false then
+ return nil, "HelperPayroll external sessions disabled"
+ end
+
+ return api, nil
+end
+
+function HP_AutoDrivePayrollBridge:_begin(vehicle, reservation)
+ if vehicle == nil or reservation == nil or reservation.helper == nil then return false end
+
+ local api, reason = self:_getPayrollAPI()
+ if api == nil then
+ self:_logWait(reason)
+ return false
+ end
+
+ local helper = reservation.helper
+ local slot = helperSlot(helper)
+ if slot == nil then
+ self:_logWait("reserved helper has no A-T slot")
+ return false
+ end
+
+ self.sessionSequence = (tonumber(self.sessionSequence) or 0) + 1
+ local sessionId = string.format("helperprofiles-autodrive-%d", self.sessionSequence)
+ local request = {
+ sessionId = sessionId,
+ source = "FS25_HelperProfiles",
+ controller = "AutoDrive",
+ jobType = "AutoDrive",
+ label = "AutoDrive worker",
+ helperSlot = slot,
+ helperIndex = tonumber(helper.index),
+ helperName = helperName(helper),
+ helperSlotSource = "HelperProfiles-AutoDrive-reservation",
+ vehicleName = vehicleName(vehicle),
+ farmId = ownerFarmId(vehicle)
+ }
+
+ local ok, accepted, result = pcall(api.beginExternalWorkerSession, api, request)
+ if not ok then
+ self:_logWait("HelperPayroll beginExternalWorkerSession raised an error: " .. tostring(accepted))
+ return false
+ end
+ if accepted ~= true then
+ local status = type(result) == "table" and result.status or "rejected"
+ self:_logWait("HelperPayroll rejected external session: " .. tostring(status))
+ return false
+ end
+
+ self.activeByVehicle[vehicle] = {
+ helper = helper,
+ helperIndex = tonumber(helper.index) or 0,
+ sessionId = sessionId,
+ api = api,
+ startedAt = nowMs()
+ }
+ self._lastWaitReason = nil
+
+ log(
+ "Payroll session started: id=%s vehicle='%s' helper='%s' slot=%s",
+ tostring(sessionId),
+ vehicleName(vehicle),
+ helperName(helper),
+ tostring(slot)
+ )
+ return true
+end
+
+function HP_AutoDrivePayrollBridge:_finish(vehicle, active, reason)
+ if active == nil then return false end
+
+ local api = active.api
+ if type(api) ~= "table" or type(api.endExternalWorkerSession) ~= "function" then
+ api = select(1, self:_getPayrollAPI())
+ end
+
+ local finished = false
+ local status = "api-unavailable"
+ if type(api) == "table" and type(api.endExternalWorkerSession) == "function" then
+ local ok, accepted, result = pcall(api.endExternalWorkerSession, api, active.sessionId, reason)
+ if ok then
+ finished = accepted == true
+ status = type(result) == "table" and tostring(result.status or (finished and "finished" or "rejected")) or tostring(accepted)
+ else
+ status = "error:" .. tostring(accepted)
+ end
+ end
+
+ log(
+ "Payroll session ended: id=%s vehicle='%s' helper='%s' reason=%s accepted=%s status=%s",
+ tostring(active.sessionId),
+ vehicleName(vehicle),
+ helperName(active.helper),
+ tostring(reason or "reservation-ended"),
+ tostring(finished),
+ tostring(status)
+ )
+
+ self.activeByVehicle[vehicle] = nil
+ return finished
+end
+
+function HP_AutoDrivePayrollBridge:_sync()
+ if HP_AutoDriveContinuityV5 == nil or type(HP_AutoDriveContinuityV5.reservations) ~= "table" then
+ self:_logWait("AutoDrive V5 reservation table unavailable")
+ return
+ end
+
+ local reservations = HP_AutoDriveContinuityV5.reservations
+
+ -- End sessions whose logical V5 reservation has genuinely disappeared or
+ -- changed owner. Internal AutoDrive release/reacquire transitions retain the
+ -- reservation, so they do not pass through this branch.
+ for vehicle, active in pairs(self.activeByVehicle or {}) do
+ local reservation = reservations[vehicle]
+ if reservation == nil then
+ self:_finish(vehicle, active, "autodrive-reservation-ended")
+ elseif reservation.helper ~= active.helper then
+ self:_finish(vehicle, active, "autodrive-reservation-owner-changed")
+ end
+ end
+
+ -- Start payroll for any live V5 reservation that does not yet have a mirrored
+ -- external session. If HelperPayroll loads later, this naturally retries on a
+ -- subsequent update without disturbing AutoDrive continuity.
+ for vehicle, reservation in pairs(reservations) do
+ if reservation ~= nil and reservation.helper ~= nil and self.activeByVehicle[vehicle] == nil then
+ self:_begin(vehicle, reservation)
+ end
+ end
+end
+
+function HP_AutoDrivePayrollBridge:loadMap()
+ self.activeByVehicle = setmetatable({}, {__mode = "k"})
+ self.sessionSequence = 0
+ self._lastWaitReason = nil
+ self._lastWaitLogMs = -100000
+end
+
+function HP_AutoDrivePayrollBridge:update(dt)
+ if HP_Compatibility ~= nil and HP_Compatibility:isBlocked() then return end
+ self:_sync()
+end
+
+function HP_AutoDrivePayrollBridge:deleteMap()
+ self.activeByVehicle = setmetatable({}, {__mode = "k"})
+end
+
+addModEventListener(HP_AutoDrivePayrollBridge)
diff --git a/scripts/HP_Compatibility.lua b/scripts/HP_Compatibility.lua
index ce69f43..6e2c62a 100644
--- a/scripts/HP_Compatibility.lua
+++ b/scripts/HP_Compatibility.lua
@@ -274,4 +274,228 @@ function HP_Compatibility:deleteMap()
self.startupCheckRemainingMs = 0
end
-addModEventListener(HP_Compatibility)
\ No newline at end of file
+addModEventListener(HP_Compatibility)
+
+----------------------------------------------------------------------
+-- AutoDrive helper continuity compatibility
+----------------------------------------------------------------------
+-- AutoDrive can synchronously stop/restart a running mode. Its stop path releases
+-- and clears the GIANTS helper, then startAutoDrive() asks getRandomHelper() for a
+-- helper again. Preserve the vehicle's existing helper identity across that
+-- restart window without changing normal HelperProfiles selection behaviour.
+
+HP_AutoDriveContinuity = HP_AutoDriveContinuity or {
+ _helperHookInstalled = false,
+ _activityHookInstalled = false,
+ _baseGetRandomHelper = nil,
+ _baseIsHelperActive = nil,
+ _reservations = setmetatable({}, {__mode = "k"})
+}
+
+local AD_LOG = "[FS25_HelperProfiles/AutoDrive] "
+
+local function adLog(message, ...)
+ print(AD_LOG .. string.format(tostring(message), ...))
+end
+
+local function getVehicleLabel(vehicle)
+ if vehicle ~= nil and vehicle.getName ~= nil then
+ local ok, value = pcall(vehicle.getName, vehicle)
+ if ok and value ~= nil and tostring(value) ~= "" then
+ return tostring(value)
+ end
+ end
+ return tostring(vehicle or "unknown-vehicle")
+end
+
+local function getAutoDriveState(vehicle)
+ if vehicle == nil or vehicle.ad == nil or vehicle.ad.stateModule == nil then
+ return false, nil
+ end
+
+ local stateModule = vehicle.ad.stateModule
+ local active = false
+ if stateModule.isActive ~= nil then
+ local ok, value = pcall(stateModule.isActive, stateModule)
+ active = ok and value == true
+ end
+
+ local helperIndex = 0
+ if stateModule.getCurrentHelperIndex ~= nil then
+ local ok, value = pcall(stateModule.getCurrentHelperIndex, stateModule)
+ if ok then helperIndex = tonumber(value) or 0 end
+ end
+
+ return active, helperIndex
+end
+
+local function isEngineAvailable(helper)
+ if helper == nil or helper.inUse == true or g_helperManager == nil then return false end
+ for _, candidate in ipairs(g_helperManager.availableHelpers or {}) do
+ if candidate == helper then return true end
+ end
+ return false
+end
+
+function HP_AutoDriveContinuity:isEnabled()
+ return rawget(_G, "AutoDrive") ~= nil
+end
+
+function HP_AutoDriveContinuity:isReserved(helper)
+ if helper == nil then return false end
+ for _, reservation in pairs(self._reservations or {}) do
+ if reservation ~= nil and reservation.helper == helper then
+ return true
+ end
+ end
+ return false
+end
+
+function HP_AutoDriveContinuity:_remember(vehicle, helper, helperIndex)
+ if vehicle == nil or helper == nil then return end
+
+ local existing = self._reservations[vehicle]
+ if existing ~= nil and existing.helper == helper then
+ existing.helperIndex = tonumber(helperIndex) or tonumber(helper.index) or existing.helperIndex or 0
+ return
+ end
+
+ self._reservations[vehicle] = {
+ helper = helper,
+ helperIndex = tonumber(helperIndex) or tonumber(helper.index) or 0
+ }
+
+ adLog(
+ "Driver reserved: vehicle='%s' helper='%s' index=%d",
+ getVehicleLabel(vehicle),
+ tostring(helper.name or "?"),
+ tonumber(helperIndex) or tonumber(helper.index) or 0
+ )
+end
+
+function HP_AutoDriveContinuity:_release(vehicle, reason)
+ local reservation = self._reservations[vehicle]
+ if reservation == nil then return end
+
+ adLog(
+ "Driver reservation cleared: vehicle='%s' helper='%s' reason=%s",
+ getVehicleLabel(vehicle),
+ tostring(reservation.helper and reservation.helper.name or "?"),
+ tostring(reason or "unknown")
+ )
+ self._reservations[vehicle] = nil
+end
+
+function HP_AutoDriveContinuity:_findReacquireCandidate()
+ if not self:isEnabled() or g_currentMission == nil then return nil, nil end
+
+ local matchedVehicle = nil
+ local matchedHelper = nil
+ local matches = 0
+
+ for _, vehicle in pairs(g_currentMission.vehicles or {}) do
+ local reservation = self._reservations[vehicle]
+ if reservation ~= nil and reservation.helper ~= nil then
+ local active, helperIndex = getAutoDriveState(vehicle)
+ local adHelper = vehicle.ad ~= nil and vehicle.ad.currentHelper or nil
+
+ -- AutoDrive:startAutoDrive sets active=true before calling
+ -- g_helperManager:getRandomHelper(). During a restart this creates a
+ -- distinctive active + no-current-helper window for the calling vehicle.
+ if active and (adHelper == nil or (tonumber(helperIndex) or 0) <= 0) and isEngineAvailable(reservation.helper) then
+ matches = matches + 1
+ matchedVehicle = vehicle
+ matchedHelper = reservation.helper
+ end
+ end
+ end
+
+ -- getRandomHelper has no vehicle argument, so only override when exactly one
+ -- AutoDrive vehicle has an unambiguous reserved-helper reacquisition window.
+ if matches == 1 then
+ return matchedHelper, matchedVehicle
+ end
+
+ if matches > 1 then
+ adLog("Continuity skipped: %d AutoDrive vehicles are simultaneously awaiting reserved helpers", matches)
+ end
+ return nil, nil
+end
+
+function HP_AutoDriveContinuity:_installActivityHook()
+ if self._activityHookInstalled then return true end
+ if HelperProfiles == nil or HelperProfiles.isHelperActive == nil then return false end
+
+ self._baseIsHelperActive = HelperProfiles.isHelperActive
+ HelperProfiles.isHelperActive = function(helperProfilesSelf, helper)
+ if HP_AutoDriveContinuity ~= nil and HP_AutoDriveContinuity:isReserved(helper) then
+ return true
+ end
+ return HP_AutoDriveContinuity._baseIsHelperActive(helperProfilesSelf, helper)
+ end
+
+ self._activityHookInstalled = true
+ adLog("Installed reserved-helper activity bridge")
+ return true
+end
+
+function HP_AutoDriveContinuity:_installHelperHook()
+ if self._helperHookInstalled then return true end
+ if not self:isEnabled() then return false end
+ if HelperProfiles == nil or HelperProfiles._hooksDone ~= true then return false end
+ if HelperManager == nil or HelperManager.getRandomHelper == nil then return false end
+
+ self._baseGetRandomHelper = HelperManager.getRandomHelper
+ HelperManager.getRandomHelper = function(manager, ...)
+ local helper, vehicle = HP_AutoDriveContinuity:_findReacquireCandidate()
+ if helper ~= nil then
+ adLog(
+ "Driver continuity reacquire: vehicle='%s' helper='%s' index=%d",
+ getVehicleLabel(vehicle),
+ tostring(helper.name or "?"),
+ tonumber(helper.index) or 0
+ )
+ return helper
+ end
+ return HP_AutoDriveContinuity._baseGetRandomHelper(manager, ...)
+ end
+
+ self._helperHookInstalled = true
+ adLog("Installed AutoDrive getRandomHelper continuity hook")
+ return true
+end
+
+function HP_AutoDriveContinuity:loadMap()
+ self._reservations = setmetatable({}, {__mode = "k"})
+end
+
+function HP_AutoDriveContinuity:update(dt)
+ if HP_Compatibility ~= nil and HP_Compatibility:isBlocked() then return end
+
+ self:_installActivityHook()
+ self:_installHelperHook()
+
+ if not self:isEnabled() or g_currentMission == nil then return end
+
+ for _, vehicle in pairs(g_currentMission.vehicles or {}) do
+ if vehicle ~= nil and vehicle.ad ~= nil and vehicle.ad.stateModule ~= nil then
+ local active, helperIndex = getAutoDriveState(vehicle)
+ local currentHelper = vehicle.ad.currentHelper
+
+ if active and currentHelper ~= nil then
+ self:_remember(vehicle, currentHelper, helperIndex)
+ elseif not active and self._reservations[vehicle] ~= nil then
+ -- AutoDrive's RestartADTask performs stopAutoDrive() and mode:start()
+ -- synchronously, so an internal restart does not reach this update in
+ -- the inactive state. A vehicle observed inactive here is a genuine stop.
+ self:_release(vehicle, "autodrive-stopped")
+ end
+ end
+ end
+end
+
+function HP_AutoDriveContinuity:deleteMap()
+ self._reservations = setmetatable({}, {__mode = "k"})
+end
+
+addModEventListener(HP_AutoDriveContinuity)