472 lines
18 KiB
Lua
472 lines
18 KiB
Lua
-- AD Runs Controller
|
|
-- Oversætter et run til AutoDrive-tilstand pr. traktor: validering, Start/Stop, status,
|
|
-- Auto-vareliste, og "hvilket run hører traktoren til". GUI'et kalder kun herind;
|
|
-- de eksisterende hooks (choosePickup, Wait-punkter) spørger getRunForVehicle/getRunIngredients.
|
|
|
|
ADRunsController = {}
|
|
|
|
ADRunsController.LOG_PREFIX = "ADSmartPickup: "
|
|
-- Længere væk fra vejnettet end dette kan AutoDrive ikke starte (samme grænse som AD's HUD).
|
|
ADRunsController.MAX_NETWORK_DISTANCE = 30
|
|
|
|
ADRunsController.store = ADRuns.newStore()
|
|
|
|
local function copyList(list)
|
|
local copy = {}
|
|
for index, value in ipairs(list or {}) do
|
|
copy[index] = value
|
|
end
|
|
return copy
|
|
end
|
|
|
|
-- Stabil nøgle: FS25's uniqueId ("vehicle…"), ellers savegame-id. Navne bruges aldrig.
|
|
function ADRunsController.getVehicleId(vehicle)
|
|
if vehicle.uniqueId ~= nil then
|
|
return tostring(vehicle.uniqueId)
|
|
end
|
|
if vehicle.getUniqueId ~= nil then
|
|
local id = vehicle:getUniqueId()
|
|
if id ~= nil then
|
|
return tostring(id)
|
|
end
|
|
end
|
|
if vehicle.currentSavegameId ~= nil then
|
|
return tostring(vehicle.currentSavegameId)
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function ADRunsController.getFarmVehicles(adEnv)
|
|
local farmId = g_currentMission:getFarmId()
|
|
local result = {}
|
|
for _, vehicle in pairs(adEnv.AutoDrive.getAllVehicles()) do
|
|
if vehicle.ad ~= nil and vehicle.getOwnerFarmId ~= nil and vehicle:getOwnerFarmId() == farmId then
|
|
table.insert(result, vehicle)
|
|
end
|
|
end
|
|
return result
|
|
end
|
|
|
|
-- Ét map pr. kald i stedet for at genscanne getFarmVehicles pr. vehicleId — startRun/stopRun/
|
|
-- getRunStatus/load slår flere id'er op pr. kald, og GUI'et kalder getRunStatus hvert sekund.
|
|
local function getFarmVehiclesById(adEnv)
|
|
local byId = {}
|
|
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
|
local id = ADRunsController.getVehicleId(vehicle)
|
|
if id ~= nil then
|
|
byId[id] = vehicle
|
|
end
|
|
end
|
|
return byId
|
|
end
|
|
|
|
function ADRunsController.findVehicleById(adEnv, vehicleId)
|
|
return getFarmVehiclesById(adEnv)[vehicleId]
|
|
end
|
|
|
|
function ADRunsController.getRunForVehicle(vehicle)
|
|
local id = ADRunsController.getVehicleId(vehicle)
|
|
return id ~= nil and ADRuns.findRunByVehicle(ADRunsController.store, id) or nil
|
|
end
|
|
|
|
-- ---- store-wrappers (erstatter altid hele store'en; modellen muterer aldrig) ----
|
|
|
|
function ADRunsController.createRun(name)
|
|
local store, run = ADRuns.createRun(ADRunsController.store, name)
|
|
ADRunsController.store = store
|
|
return run
|
|
end
|
|
|
|
function ADRunsController.deleteRun(runId)
|
|
ADRunsController.store = ADRuns.deleteRun(ADRunsController.store, runId)
|
|
end
|
|
|
|
-- Hver ændring logges, så et klik der ikke når modellen kan ses i log.txt.
|
|
function ADRunsController.updateRun(runId, changes)
|
|
ADRunsController.store = ADRuns.updateRun(ADRunsController.store, runId, changes)
|
|
local run = ADRuns.getRun(ADRunsController.store, runId)
|
|
local parts = {}
|
|
for key, value in pairs(changes) do
|
|
table.insert(parts, string.format("%s=%s", tostring(key), type(value) == "table" and table.concat(value, " ") or tostring(value)))
|
|
end
|
|
table.sort(parts)
|
|
Logging.info(ADRunsController.LOG_PREFIX .. "run '%s': %s", run ~= nil and tostring(run.name) or tostring(runId), table.concat(parts, ", "))
|
|
end
|
|
|
|
function ADRunsController.assignVehicle(runId, vehicle)
|
|
local id = ADRunsController.getVehicleId(vehicle)
|
|
if id == nil then
|
|
return nil
|
|
end
|
|
local store, previousRunId = ADRuns.assignVehicle(ADRunsController.store, runId, id)
|
|
ADRunsController.store = store
|
|
return previousRunId
|
|
end
|
|
|
|
function ADRunsController.unassignVehicle(vehicle)
|
|
local id = ADRunsController.getVehicleId(vehicle)
|
|
if id ~= nil then
|
|
ADRunsController.store = ADRuns.unassignVehicle(ADRunsController.store, id)
|
|
end
|
|
end
|
|
|
|
-- ---- varer ----
|
|
|
|
function ADRunsController.getCarriableFillTypes(adEnv, vehicle)
|
|
return adEnv.AutoDrive.getSupportedFillTypesOfAllUnitsAlphabetically(vehicle) or {}
|
|
end
|
|
|
|
-- Auto-listen: det vognen kan bære, som målet også tager. Er målet en fabrik, indsnævrer
|
|
-- findSupplyPickup selv til de ønskede produktioners inputs bagefter.
|
|
function ADRunsController.getTargetFillTypes(adEnv, run, vehicle)
|
|
local farmId = ADSmartPickup.getFarmId(vehicle)
|
|
-- En kørende traktor følger AD's eget mål (stateModule:getSecondMarker(), det samme
|
|
-- choosePickup/findSupplyPickup bruger) indtil næste Start — IKKE run'ets gemte
|
|
-- targetWayPointId. Redigerer brugeren run'ets Mål mens tildelte traktorer kører,
|
|
-- skal Auto-listen derfor følge det LIVE mål, ellers kan den blive tom og fejlagtigt
|
|
-- smide traktoren over i mappe-logikken midt i turen.
|
|
local wayPointId = run.targetWayPointId
|
|
local stateModule = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
|
|
if stateModule ~= nil and stateModule.isActive ~= nil and stateModule:isActive() and stateModule.getSecondMarker ~= nil then
|
|
local secondMarker = stateModule:getSecondMarker()
|
|
if secondMarker ~= nil and secondMarker.id ~= nil then
|
|
wayPointId = secondMarker.id
|
|
end
|
|
end
|
|
local target = wayPointId ~= nil and ADSmartPickup.getUnloadStationAtWayPointId(adEnv, wayPointId, farmId) or nil
|
|
if target == nil then
|
|
return {}
|
|
end
|
|
local result = {}
|
|
for _, fillType in ipairs(ADRunsController.getCarriableFillTypes(adEnv, vehicle)) do
|
|
if ADSmartPickup.getStationLevelAndCapacity(target, fillType, farmId) ~= nil then
|
|
table.insert(result, fillType)
|
|
end
|
|
end
|
|
table.sort(result)
|
|
return result
|
|
end
|
|
|
|
function ADRunsController.getRunIngredients(adEnv, run, vehicle)
|
|
if run.ingredientMode == "manual" then
|
|
local result = {}
|
|
for _, name in ipairs(run.ingredients) do
|
|
local index = g_fillTypeManager:getFillTypeIndexByName(name)
|
|
if index ~= nil then
|
|
table.insert(result, index)
|
|
end
|
|
end
|
|
return result
|
|
end
|
|
return ADRunsController.getTargetFillTypes(adEnv, run, vehicle)
|
|
end
|
|
|
|
-- Alle varer målet tager (uanset vogn) — afkrydsningslisten i GUI'et. {index, name, title}, sorteret på titel.
|
|
function ADRunsController.getTargetAcceptedFillTypes(adEnv, run)
|
|
if run.targetWayPointId == nil then
|
|
return {}
|
|
end
|
|
local farmId = g_currentMission:getFarmId()
|
|
local target = ADSmartPickup.getUnloadStationAtWayPointId(adEnv, run.targetWayPointId, farmId)
|
|
if target == nil then
|
|
return {}
|
|
end
|
|
local fillTypes = g_fillTypeManager.getFillTypes ~= nil and g_fillTypeManager:getFillTypes() or g_fillTypeManager.fillTypes or {}
|
|
local result = {}
|
|
for _, fillType in ipairs(fillTypes) do
|
|
if fillType.index ~= nil and ADSmartPickup.getStationLevelAndCapacity(target, fillType.index, farmId) ~= nil then
|
|
table.insert(result, {index = fillType.index, name = fillType.name, title = fillType.title or fillType.name})
|
|
end
|
|
end
|
|
table.sort(result, function(a, b) return tostring(a.title) < tostring(b.title) end)
|
|
return result
|
|
end
|
|
|
|
-- Fire traktorer kan hedde det samme; løbenummeret er pladsen i farmens AD-køretøjsliste.
|
|
function ADRunsController.getVehicleLabel(adEnv, vehicle)
|
|
for index, candidate in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
|
if candidate == vehicle then
|
|
return string.format("%s (#%d)", tostring(vehicle:getName()), index)
|
|
end
|
|
end
|
|
return tostring(vehicle:getName())
|
|
end
|
|
|
|
-- ---- validering ----
|
|
|
|
local function getLoadCapacity(adEnv, vehicle)
|
|
local trailers = adEnv.AutoDrive.getAllUnits(vehicle)
|
|
if trailers == nil then
|
|
return nil
|
|
end
|
|
local _, capacity = adEnv.AutoDrive.getAllFillLevels(trailers)
|
|
return capacity ~= nil and capacity > 0 and capacity or nil
|
|
end
|
|
|
|
local function intersects(listA, listB)
|
|
for _, a in ipairs(listA) do
|
|
for _, b in ipairs(listB) do
|
|
if a == b then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- reason: "noTarget" | "noTrailer" | "cannotCarry" | "farFromNetwork" (l10n-nøgler i GUI'et).
|
|
function ADRunsController.validateVehicle(adEnv, run, vehicle)
|
|
local carriable = ADRunsController.getCarriableFillTypes(adEnv, vehicle)
|
|
local result = {ok = false, carriable = carriable, capacity = getLoadCapacity(adEnv, vehicle)}
|
|
if run.targetWayPointId == nil or adEnv.ADGraphManager:getMapMarkerByWayPointId(run.targetWayPointId) == nil then
|
|
result.reason = "noTarget"
|
|
return result
|
|
end
|
|
if result.capacity == nil then
|
|
result.reason = "noTrailer"
|
|
return result
|
|
end
|
|
if not intersects(ADRunsController.getRunIngredients(adEnv, run, vehicle), carriable) then
|
|
result.reason = "cannotCarry"
|
|
return result
|
|
end
|
|
local distance = adEnv.ADGraphManager.getDistanceFromNetwork ~= nil and adEnv.ADGraphManager:getDistanceFromNetwork(vehicle) or 0
|
|
if distance == nil or distance > ADRunsController.MAX_NETWORK_DISTANCE then
|
|
result.reason = "farFromNetwork"
|
|
return result
|
|
end
|
|
result.ok = true
|
|
return result
|
|
end
|
|
|
|
-- ---- Start / Stop ----
|
|
|
|
-- AD gemmer indstillinger som INDEKS i values; useFolders er global, rotateTargets pr. køretøj.
|
|
-- Er indstillingen køretøjs-specifik, men køretøjet mangler sin egen settings-tabel, skrives
|
|
-- der IKKE til den globale tabel — det ville ødelægge AD's globale default for alle køretøjer.
|
|
local function setSetting(adEnv, vehicle, settingName, value)
|
|
local global = adEnv.AutoDrive.settings[settingName]
|
|
if global == nil then
|
|
return
|
|
end
|
|
local setting = global
|
|
if global.isVehicleSpecific then
|
|
if vehicle.ad.settings == nil or vehicle.ad.settings[settingName] == nil then
|
|
return
|
|
end
|
|
setting = vehicle.ad.settings[settingName]
|
|
end
|
|
for index, candidate in ipairs(setting.values) do
|
|
if candidate == value then
|
|
setting.current = index
|
|
setting.new = index
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Nærmeste markør med en læssestation: kun en gyldig start — moden vælger den reelle kilde pr. tur.
|
|
local function findNearestLoadMarker(adEnv, vehicle, farmId)
|
|
local x, _, z = getWorldTranslation(vehicle.rootNode)
|
|
local best, bestDistance = nil, math.huge
|
|
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
|
if ADSmartPickup.getLoadStationAtWayPointId(adEnv, marker.id, farmId) ~= nil then
|
|
local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id)
|
|
-- En stale markør uden waypoint skal springes over — ellers fejler Start med "error"
|
|
-- for ALLE tildelte traktorer, ikke kun den der ramte den ugyldige markør.
|
|
if wayPoint ~= nil then
|
|
local dx, dz = wayPoint.x - x, wayPoint.z - z
|
|
local distance = dx * dx + dz * dz
|
|
if distance < bestDistance then
|
|
best, bestDistance = marker, distance
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return best
|
|
end
|
|
|
|
local function applyRunToVehicle(adEnv, run, vehicle, ingredients)
|
|
local stateModule = vehicle.ad.stateModule
|
|
local farmId = ADSmartPickup.getFarmId(vehicle)
|
|
stateModule:setMode(adEnv.AutoDrive.MODE_PICKUPANDDELIVER)
|
|
stateModule:setSecondMarkerByWayPointId(run.targetWayPointId)
|
|
local loadMarker = findNearestLoadMarker(adEnv, vehicle, farmId)
|
|
if loadMarker ~= nil then
|
|
stateModule:setFirstMarkerByWayPointId(loadMarker.id)
|
|
end
|
|
stateModule.selectedFillTypes = copyList(ingredients)
|
|
stateModule:setFillType(ingredients[1])
|
|
stateModule.loopCounter = run.loops
|
|
stateModule:setLoopsDone(0)
|
|
setSetting(adEnv, vehicle, "useFolders", true)
|
|
setSetting(adEnv, vehicle, "rotateTargets", adEnv.AutoDrive.RT_ONLYPICKUP)
|
|
stateModule:raiseDirtyFlag()
|
|
end
|
|
|
|
local function isVehicleActive(vehicle)
|
|
local stateModule = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
|
|
return stateModule ~= nil and stateModule.isActive ~= nil and stateModule:isActive()
|
|
end
|
|
|
|
-- Anvend run-tilstand OG start traktoren i ét pcall'et kald: kaster vehicle:startAutoDrive()
|
|
-- (fx AD's egen validering), skal den ene traktor fejle uden at hele startRun-loopet
|
|
-- afbrydes — ellers når de øvrige tildelte traktorer aldrig at blive startet.
|
|
local function applyAndStart(adEnv, run, vehicle, ingredients)
|
|
applyRunToVehicle(adEnv, run, vehicle, ingredients)
|
|
if not isVehicleActive(vehicle) then
|
|
vehicle:startAutoDrive()
|
|
end
|
|
end
|
|
|
|
-- Pr. tildelt traktor: validér, skriv AD-tilstand, start. ✖-traktorer springes over.
|
|
function ADRunsController.startRun(adEnv, runId)
|
|
local run = ADRuns.getRun(ADRunsController.store, runId)
|
|
local results = {}
|
|
if run == nil then
|
|
return results
|
|
end
|
|
local vehiclesById = getFarmVehiclesById(adEnv)
|
|
for _, vehicleId in ipairs(run.vehicleIds) do
|
|
local vehicle = vehiclesById[vehicleId]
|
|
if vehicle ~= nil then
|
|
local validation = ADRunsController.validateVehicle(adEnv, run, vehicle)
|
|
local result = {vehicle = vehicle, ok = validation.ok, reason = validation.reason}
|
|
if validation.ok then
|
|
local ingredients = ADRunsController.getRunIngredients(adEnv, run, vehicle)
|
|
local ok, err = pcall(applyAndStart, adEnv, run, vehicle, ingredients)
|
|
if ok then
|
|
Logging.info(ADRunsController.LOG_PREFIX .. "run '%s': '%s' startet mod markør %s", tostring(run.name), tostring(vehicle:getName()), tostring(run.targetWayPointId))
|
|
else
|
|
result.ok, result.reason = false, "error"
|
|
Logging.warning(ADRunsController.LOG_PREFIX .. "run '%s': kunne ikke starte '%s': %s", tostring(run.name), tostring(vehicle:getName()), tostring(err))
|
|
end
|
|
end
|
|
table.insert(results, result)
|
|
end
|
|
end
|
|
return results
|
|
end
|
|
|
|
function ADRunsController.stopRun(adEnv, runId)
|
|
local run = ADRuns.getRun(ADRunsController.store, runId)
|
|
local stopped = 0
|
|
if run == nil then
|
|
return stopped
|
|
end
|
|
local vehiclesById = getFarmVehiclesById(adEnv)
|
|
for _, vehicleId in ipairs(run.vehicleIds) do
|
|
local vehicle = vehiclesById[vehicleId]
|
|
if vehicle ~= nil and isVehicleActive(vehicle) then
|
|
vehicle:stopAutoDrive()
|
|
stopped = stopped + 1
|
|
end
|
|
end
|
|
return stopped
|
|
end
|
|
|
|
function ADRunsController.getRunStatus(adEnv, run)
|
|
local running = 0
|
|
local vehiclesById = getFarmVehiclesById(adEnv)
|
|
for _, vehicleId in ipairs(run.vehicleIds) do
|
|
local vehicle = vehiclesById[vehicleId]
|
|
if vehicle ~= nil and isVehicleActive(vehicle) then
|
|
running = running + 1
|
|
end
|
|
end
|
|
return {running = running, total = #run.vehicleIds}
|
|
end
|
|
|
|
-- ---- opslag til dropdowns ----
|
|
|
|
-- Mål er steder der SKAL holdes fyldt: stalde (1) og fabrikker (2). Siloer tager alt og
|
|
-- er kilder, ikke mål. Returnerer markører sorteret efter type, dernæst navn.
|
|
local function getTargetKind(station)
|
|
local placeable = station.owningPlaceable
|
|
if placeable == nil then
|
|
return nil
|
|
end
|
|
if placeable.getHusbandryFillLevel ~= nil then
|
|
return 1
|
|
end
|
|
if placeable.spec_productionPoint ~= nil then
|
|
return 2
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function ADRunsController.getTargetMarkers(adEnv, farmId)
|
|
local entries = {}
|
|
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
|
local station = ADSmartPickup.getUnloadStationAtWayPointId(adEnv, marker.id, farmId)
|
|
local kind = station ~= nil and getTargetKind(station) or nil
|
|
if kind ~= nil then
|
|
table.insert(entries, {marker = marker, kind = kind})
|
|
end
|
|
end
|
|
table.sort(entries, function(a, b)
|
|
if a.kind ~= b.kind then
|
|
return a.kind < b.kind
|
|
end
|
|
return tostring(a.marker.name) < tostring(b.marker.name)
|
|
end)
|
|
local result = {}
|
|
for index, entry in ipairs(entries) do
|
|
result[index] = entry.marker
|
|
end
|
|
return result
|
|
end
|
|
|
|
function ADRunsController.getWaitPools(adEnv)
|
|
local counts = {}
|
|
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
|
if ADSupplyPlanner.isWaitMarker(marker) then
|
|
counts[marker.group] = (counts[marker.group] or 0) + 1
|
|
end
|
|
end
|
|
local result = {}
|
|
for group, count in pairs(counts) do
|
|
table.insert(result, {group = group, count = count})
|
|
end
|
|
table.sort(result, function(a, b) return a.group < b.group end)
|
|
return result
|
|
end
|
|
|
|
-- ---- gem / indlæs ----
|
|
|
|
function ADRunsController.save()
|
|
local ok, saved = pcall(ADRunsStorage.save, ADRunsController.store)
|
|
if not ok then
|
|
Logging.warning(ADRunsController.LOG_PREFIX .. "kunne ikke gemme runs: %s", tostring(saved))
|
|
return false
|
|
end
|
|
return saved
|
|
end
|
|
|
|
-- Køretøjs-referencer der ikke findes (solgt, andet savegame) logges; run'et beholdes.
|
|
function ADRunsController.load(adEnv)
|
|
local ok, store = pcall(ADRunsStorage.load)
|
|
if not ok then
|
|
Logging.warning(ADRunsController.LOG_PREFIX .. "kunne ikke indlæse runs: %s", tostring(store))
|
|
return
|
|
end
|
|
ADRunsController.store = store
|
|
local vehiclesById = getFarmVehiclesById(adEnv)
|
|
for _, run in ipairs(store.runs) do
|
|
for _, vehicleId in ipairs(run.vehicleIds) do
|
|
if vehiclesById[vehicleId] == nil then
|
|
Logging.info(ADRunsController.LOG_PREFIX .. "run '%s': køretøj %s findes ikke (endnu) i dette savegame", tostring(run.name), tostring(vehicleId))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function ADRunsController.installSaveHook()
|
|
if ItemSystem == nil or ItemSystem.save == nil or Utils == nil then
|
|
return
|
|
end
|
|
ItemSystem.save = Utils.prependedFunction(ItemSystem.save, function()
|
|
ADRunsController.save()
|
|
end)
|
|
end
|