diff --git a/FS25_ADSmartPickup/adSmartPickup.lua b/FS25_ADSmartPickup/adSmartPickup.lua index 9ce546c..3376380 100644 --- a/FS25_ADSmartPickup/adSmartPickup.lua +++ b/FS25_ADSmartPickup/adSmartPickup.lua @@ -357,31 +357,70 @@ local function isFillTypeAccepted(station, husbandry, fillType) return station.getIsFillTypeSupported == nil or station:getIsFillTypeSupported(fillType) end +-- Niveau + kapacitet for én vare i aflæsningsmålet; nil hvis målet ikke tager varen. +local function getTargetLevelAndCapacity(station, fillType, farmId) + local husbandry = getHusbandry(station) + if not isFillTypeAccepted(station, husbandry, fillType) then + return nil + end + if husbandry ~= nil then + local level = husbandry:getHusbandryFillLevel(fillType) or 0 + return level, husbandry:getHusbandryCapacity(fillType) or level + end + local level, freeSpace = 0, 0 + for _, storage in pairs(getTargetStorages(station)) do + level = level + (storage:getFillLevel(fillType) or 0) + freeSpace = freeSpace + (storage:getFreeCapacity(fillType) or 0) + end + if level + freeSpace <= 0 and station.getFreeCapacity ~= nil then + -- ukendt lagertype: brug stationens egen opgørelse af fri plads + level, freeSpace = 0, station:getFreeCapacity(fillType, farmId) or 0 + end + return level, level + freeSpace +end + -- Niveau + kapacitet pr. ingrediens i aflæsningsmålets lager; demand giver forbrug/min. local function getTargetNeeds(station, ingredients, farmId, demand) local needs = {} - local storages = getTargetStorages(station) - local husbandry = getHusbandry(station) for _, fillType in ipairs(ingredients) do - if husbandry ~= nil and isFillTypeAccepted(station, husbandry, fillType) then - local level = husbandry:getHusbandryFillLevel(fillType) or 0 - table.insert(needs, {fillType = fillType, level = level, capacity = husbandry:getHusbandryCapacity(fillType) or level, litersPerMinute = demand[fillType]}) - elseif isFillTypeAccepted(station, husbandry, fillType) then - local level, freeSpace = 0, 0 - for _, storage in pairs(storages) do - level = level + (storage:getFillLevel(fillType) or 0) - freeSpace = freeSpace + (storage:getFreeCapacity(fillType) or 0) - end - if level + freeSpace <= 0 and station.getFreeCapacity ~= nil then - -- ukendt lagertype: brug stationens egen opgørelse af fri plads - level, freeSpace = 0, station:getFreeCapacity(fillType, farmId) or 0 - end - table.insert(needs, {fillType = fillType, level = level, capacity = level + freeSpace, litersPerMinute = demand[fillType]}) + local level, capacity = getTargetLevelAndCapacity(station, fillType, farmId) + if level ~= nil then + table.insert(needs, {fillType = fillType, level = level, capacity = capacity, litersPerMinute = demand[fillType]}) end end return needs end +-- Vognens samlede kapacitet (liter); nil hvis AutoDrive ikke kan opgøre den. +local function getLoadCapacity(adEnv, driver) + if adEnv.AutoDrive.getAllUnits == nil or adEnv.AutoDrive.getAllFillLevels == nil then + return nil + end + local trailers = adEnv.AutoDrive.getAllUnits(driver) + local _, capacity = adEnv.AutoDrive.getAllFillLevels(trailers) + return capacity ~= nil and capacity > 0 and capacity or nil +end + +-- Fri plads til varen i køretøjets aflæsningsmål (bruges af Wait-punkt-logikken); +-- nil hvis målet ikke kendes eller ikke tager varen. +function ADSmartPickup.getTargetFreeCapacity(adEnv, driver, fillType) + local secondMarker = driver.ad.stateModule:getSecondMarker() + local farmId = getFarmId(driver) + if secondMarker == nil or farmId == nil then + return nil + end + local targetWayPoint = adEnv.ADGraphManager:getWayPointById(secondMarker.id) + local target = targetWayPoint ~= nil and getUnloadStationAtMarker(adEnv, targetWayPoint, farmId) or nil + if target == nil then + return nil + end + local level, capacity = getTargetLevelAndCapacity(target, fillType, farmId) + if level == nil then + return nil + end + return capacity - level +end + -- Behovsstyret: den ingrediens aflæsningsmålet mangler mest, som også findes i en silo. function ADSmartPickup.findSupplyPickup(adEnv, driver, ingredients) local secondMarker = driver.ad.stateModule:getSecondMarker() @@ -407,7 +446,8 @@ function ADSmartPickup.findSupplyPickup(adEnv, driver, ingredients) local allMarkers = adEnv.ADGraphManager:getMapMarkers() local markerStations = collectMarkerStations(adEnv, allMarkers, farmId, target.owningPlaceable) - for _, need in ipairs(ADSupplyPlanner.rankNeeds(getTargetNeeds(target, ingredients, farmId, demand))) do + local loadCapacity = getLoadCapacity(adEnv, driver) + for _, need in ipairs(ADSupplyPlanner.rankNeeds(getTargetNeeds(target, ingredients, farmId, demand), loadCapacity)) do local choice = findBestSource(markerStations, {need.fillType}, farmId) if choice ~= nil then return { @@ -463,6 +503,9 @@ local function describeChoice(choice) if choice.need.minutesToEmpty ~= nil then urgency = string.format(", fabrikken bruger %.0f l/min = %.0f min til tom", choice.need.litersPerMinute, choice.need.minutesToEmpty) end + if choice.need.fits == false then + urgency = urgency .. ", et fuldt læs passer IKKE (rest ventes ved Wait-punkt)" + end return string.format("%s: kilde %.0f l, %s, målet har %.0f/%.0f l%s", tostring(fillTypeName), choice.level, source, choice.need.level, choice.need.capacity, urgency) end @@ -571,8 +614,11 @@ local function installHook(adEnv) end installLoadWaitHook(adEnv) + if ADUnloadWait ~= nil then + ADUnloadWait.install(adEnv) + end - Logging.info(ADSmartPickup.LOG_PREFIX .. "aktiv (mindst fyldte silo med varen; flere valgte varer = behovsstyret forsyning)") + Logging.info(ADSmartPickup.LOG_PREFIX .. "aktiv (mindst fyldte silo med varen; flere valgte varer = behovsstyret forsyning; Wait-punkter for rest-læs)") end -- Når AutoDrive stopper, får køretøjet hele ingredienslisten tilbage i varevalget @@ -608,6 +654,9 @@ function ADSmartPickup:update(dt) return end + if ADUnloadWait ~= nil then + pcall(ADUnloadWait.releaseInactive) + end local ok, err = pcall(restoreStoppedVehicles) if not ok then Logging.warning(ADSmartPickup.LOG_PREFIX .. "kunne ikke gendanne varevalg: %s", tostring(err)) diff --git a/FS25_ADSmartPickup/adSupplyPlanner.lua b/FS25_ADSmartPickup/adSupplyPlanner.lua index 53a026d..980aef1 100644 --- a/FS25_ADSmartPickup/adSupplyPlanner.lua +++ b/FS25_ADSmartPickup/adSupplyPlanner.lua @@ -114,9 +114,46 @@ function ADSupplyPlanner.scoreNeed(level, capacity, freeSpace, minutesToEmpty) return freeSpace / capacity end +-- nil når læs-størrelsen er ukendt, ellers om et fuldt læs kan komme af i målet. +function ADSupplyPlanner.fullLoadFits(freeSpace, loadCapacity) + if loadCapacity == nil or loadCapacity <= 0 then + return nil + end + return freeSpace >= loadCapacity +end + +-- Wait-punkter: markører med ordet "Wait" i navnet (uanset store/små) eller i mappen "Wait". +-- Her venter en traktor med rest-læs, til der igen er plads i målet. +function ADSupplyPlanner.isWaitMarker(marker) + if marker.group == "Wait" then + return true + end + local name = string.lower(tostring(marker.name or "")) + return string.find(name, "%f[%a]wait%f[%A]") ~= nil +end + +-- candidates: {{marker, x, z}}; claims: {[marker.id] = køretøj}. Nærmeste punkt der er +-- ledigt eller allerede reserveret af dette køretøj. nil hvis alle er optaget. +function ADSupplyPlanner.pickNearestFreeWaitPoint(candidates, claims, x, z, vehicle) + local best, bestDistance = nil, nil + for _, candidate in ipairs(candidates) do + local owner = claims[candidate.marker.id] + if owner == nil or owner == vehicle then + local dx, dz = candidate.x - x, candidate.z - z + local distance = dx * dx + dz * dz + if bestDistance == nil or distance < bestDistance then + best, bestDistance = candidate, distance + end + end + end + return best +end + -- needs: {{fillType, level, capacity[, litersPerMinute]}, ...}. Returnerer NY liste, -- mest trængende først; ingredienser uden plads i målet er sorteret fra. -function ADSupplyPlanner.rankNeeds(needs) +-- loadCapacity (valgfri): vognens kapacitet — varer hvor et FULDT læs kan komme af +-- går forud (så vognen ikke ender med rest-læs); passer intet, gælder ren trang. +function ADSupplyPlanner.rankNeeds(needs, loadCapacity) local ranked = {} for _, need in ipairs(needs) do local freeSpace = need.capacity - need.level @@ -129,11 +166,15 @@ function ADSupplyPlanner.rankNeeds(needs) freeSpace = freeSpace, litersPerMinute = need.litersPerMinute, minutesToEmpty = minutesToEmpty, + fits = ADSupplyPlanner.fullLoadFits(freeSpace, loadCapacity), score = ADSupplyPlanner.scoreNeed(need.level, need.capacity, freeSpace, minutesToEmpty), }) end end table.sort(ranked, function(a, b) + if a.fits ~= b.fits then + return a.fits == true + end if a.score ~= b.score then return a.score > b.score end diff --git a/FS25_ADSmartPickup/adUnloadWait.lua b/FS25_ADSmartPickup/adUnloadWait.lua new file mode 100644 index 0000000..eb72664 --- /dev/null +++ b/FS25_ADSmartPickup/adUnloadWait.lua @@ -0,0 +1,184 @@ +-- AD Unload Wait +-- Holder en traktor med rest-læs ved en fuld stald AutoDrive i dag stille ved +-- aflæsningspunktet "til den er tømt på en eller anden måde" — og spærrer for +-- de næste i køen. Her omdirigeres den i stedet til nærmeste ledige Wait-punkt +-- (markør med ordet "Wait" i navnet eller i mappen "Wait"), og når målet igen +-- har plads til HELE resten, sendes den tilbage og læsser af. +-- +-- Kun den aktuelle aflæsningsopgaves destination ændres; AutoDrives mål, +-- rotation og loop-tæller røres ikke. + +ADUnloadWait = {} + +-- Under dette regnes vognen som tom (AutoDrive selv bruger 0,1 l). +ADUnloadWait.MIN_REST_LITERS = 100 +-- Kan målet tage mindre end dette, regnes det som fuldt (aflæsning starter ikke). +ADUnloadWait.MIN_FREE_TO_UNLOAD = 100 +ADUnloadWait.LOG_PREFIX = "ADSmartPickup: " + +-- køretøj -> {task, homeDestinationID, waitMarker, fillType, rest} +local waits = setmetatable({}, {__mode = "k"}) +-- markør-id -> køretøj der har reserveret Wait-punktet +local claims = setmetatable({}, {__mode = "v"}) +local warnedNoWaitPoint = setmetatable({}, {__mode = "k"}) +local isInstalled = false + +-- Den vare og mængde der er tilbage på vognen (største fyldte aflæsselige enhed). +local function getRestLoad(adEnv, vehicle) + local bestFillType, bestLevel = nil, 0 + for _, unit in ipairs(adEnv.AutoDrive.getAllDischargeableUnits(vehicle) or {}) do + local object = unit.object + if object ~= nil and object.getFillUnitFillLevel ~= nil then + local level = object:getFillUnitFillLevel(unit.fillUnitIndex) or 0 + if level > bestLevel then + bestLevel = level + bestFillType = object.getFillUnitFillType ~= nil and object:getFillUnitFillType(unit.fillUnitIndex) or nil + end + end + end + return bestFillType, bestLevel +end + +local function getWaitCandidates(adEnv) + local candidates = {} + for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do + if ADSupplyPlanner.isWaitMarker(marker) then + local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id) + if wayPoint ~= nil then + table.insert(candidates, {marker = marker, x = wayPoint.x, z = wayPoint.z}) + end + end + end + return candidates +end + +local function getVehiclePosition(vehicle) + if vehicle.rootNode == nil then + return nil + end + local x, _, z = getWorldTranslation(vehicle.rootNode) + return x, z +end + +local function redirect(task, destinationID) + task.destinationID = destinationID + task.state = ADUnloadWait.STATE_DRIVING + task.vehicle.ad.drivePathModule:setPathTo(destinationID) +end + +local function release(vehicle) + local wait = waits[vehicle] + if wait ~= nil and claims[wait.waitMarker.id] == vehicle then + claims[wait.waitMarker.id] = nil + end + waits[vehicle] = nil +end + +-- Står køretøjet ved målet med rest-læs, som målet ikke kan tage? Så find et Wait-punkt. +local function startWaiting(adEnv, task) + local vehicle = task.vehicle + if not vehicle.ad.drivePathModule:isTargetReached() or vehicle.ad.trailerModule:isActiveAtTrigger() then + return + end + local fillType, rest = getRestLoad(adEnv, vehicle) + if fillType == nil or rest < ADUnloadWait.MIN_REST_LITERS then + return + end + local free = ADSmartPickup.getTargetFreeCapacity(adEnv, vehicle, fillType) + if free == nil or free >= ADUnloadWait.MIN_FREE_TO_UNLOAD then + return + end + + local x, z = getVehiclePosition(vehicle) + local candidate = x ~= nil and ADSupplyPlanner.pickNearestFreeWaitPoint(getWaitCandidates(adEnv), claims, x, z, vehicle) or nil + if candidate == nil then + if not warnedNoWaitPoint[vehicle] then + warnedNoWaitPoint[vehicle] = true + Logging.warning(ADUnloadWait.LOG_PREFIX .. "'%s' har %.0f l rest, og målet er fuldt — intet ledigt Wait-punkt (markør med 'Wait' i navnet), så AutoDrive venter ved målet", tostring(vehicle:getName()), rest) + end + return + end + warnedNoWaitPoint[vehicle] = nil + claims[candidate.marker.id] = vehicle + waits[vehicle] = {task = task, homeDestinationID = task.destinationID, waitMarker = candidate.marker, fillType = fillType, rest = rest} + redirect(task, candidate.marker.id) + local fillTypeName = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or tostring(fillType) + Logging.info(ADUnloadWait.LOG_PREFIX .. "'%s' venter ved %s: %.0f l %s rest, målet har kun %.0f l fri", tostring(vehicle:getName()), tostring(candidate.marker.name), rest, tostring(fillTypeName), free) +end + +-- Ventende køretøj: er der nu plads til hele resten, køres tilbage til målet. +local function updateWaiting(adEnv, task, wait) + local vehicle = task.vehicle + if wait.task ~= task then + release(vehicle) + return + end + local fillType, rest = getRestLoad(adEnv, vehicle) + if fillType == nil or rest < ADUnloadWait.MIN_REST_LITERS then + release(vehicle) + return + end + local free = ADSmartPickup.getTargetFreeCapacity(adEnv, vehicle, fillType) + if free == nil or free < rest then + return + end + local home = wait.homeDestinationID + release(vehicle) + redirect(task, home) + Logging.info(ADUnloadWait.LOG_PREFIX .. "'%s' kører tilbage fra %s og læsser %.0f l af (målet har %.0f l fri)", tostring(vehicle:getName()), tostring(wait.waitMarker.name), rest, free) +end + +function ADUnloadWait.beforeUpdate(adEnv, task) + local wait = waits[task.vehicle] + if wait ~= nil then + updateWaiting(adEnv, task, wait) + else + startWaiting(adEnv, task) + end +end + +-- Kaldes fra ADSmartPickup:update — standsede køretøjer slipper deres Wait-punkt. +function ADUnloadWait.releaseInactive() + for vehicle in pairs(waits) do + local stateModule = vehicle.ad ~= nil and vehicle.ad.stateModule or nil + if stateModule == nil or stateModule.isActive == nil or not stateModule:isActive() then + release(vehicle) + end + end +end + +function ADUnloadWait.install(adEnv) + local task, autoDrive = adEnv.UnloadAtDestinationTask, adEnv.AutoDrive + if isInstalled or task == nil or task.update == nil or autoDrive == nil or autoDrive.getSetting == nil then + return + end + isInstalled = true + ADUnloadWait.STATE_DRIVING = task.STATE_DRIVING or 2 + + local originalUpdate, originalFinished, originalGetSetting = task.update, task.finished, autoDrive.getSetting + + task.update = function(self, dt) + local ok, err = pcall(ADUnloadWait.beforeUpdate, adEnv, self) + if not ok then + Logging.warning(ADUnloadWait.LOG_PREFIX .. "fejl i Wait-punkt-logik, AutoDrive fortsætter selv: %s", tostring(err)) + release(self.vehicle) + end + return originalUpdate(self, dt) + end + + if originalFinished ~= nil then + task.finished = function(self) + release(self.vehicle) + return originalFinished(self) + end + end + + -- Ved "rotér aflæsning" kører AutoDrive straks videre fra et mål den ikke kan læsse + -- af ved — også fra Wait-punktet. Ventende køretøjer ser derfor rotationen som slået fra. + autoDrive.getSetting = function(settingName, vehicle) + if settingName == "rotateTargets" and vehicle ~= nil and waits[vehicle] ~= nil then + return autoDrive.RT_NONE + end + return originalGetSetting(settingName, vehicle) + end +end diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml index cb1a711..8cffa8a 100644 --- a/FS25_ADSmartPickup/modDesc.xml +++ b/FS25_ADSmartPickup/modDesc.xml @@ -1,17 +1,18 @@ masterdraco - 1.6.1.0 + 1.7.0.0 <en>AD Smart Pickup</en> - + icon.dds + diff --git a/README.md b/README.md index e02acf0..f53765d 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,24 @@ behovsstyring. Varer der både er input og output (kæde) må hentes. Kostald m.fl. svarer selv for niveau/kapacitet (`getHusbandryFillLevel/Capacity`), så halm+TMR til en stald rangeres korrekt, selv om staldens aflæsningsstation kun "kender" foder. +### Wait-punkter: rest-læs ved fuld stald (v1.7) + +AutoDrive holder ellers en traktor med rest-læs stille ved aflæsningspunktet, til den er tømt — og +spærrer for de næste i køen. Med Wait-punkter: + +1. Sæt markører med ordet **`Wait`** i navnet (fx `Wait 1`, `Cow Wait 2`) eller i en mappe kaldet + `Wait` — så mange du har plads til; de er en fælles pulje for alle traktorer. +2. Kan målet ikke tage mere af vognens vare, kører traktoren til det **nærmeste ledige** Wait-punkt + (modden holder styr på reservationer) og venter dér. +3. Når målet igen har plads til **hele** resten, kører den tilbage og læsser af. Derefter normal tur. + +Kun den aktuelle aflæsningsopgaves destination ændres — mål, rotation og loop-tæller røres ikke. +Ventende traktorer ser rotationen som slået fra, så AutoDrive ikke kører videre fra Wait-punktet. +Ingen ledige Wait-punkter → AutoDrive venter ved målet som i dag (én advarsel i loggen). + +**Læs der passer, foretrækkes (v1.7):** ved valg af næste tur går varer, hvor et fuldt læs kan komme af +i målet, forud; passer intet fuldt læs, tages den mest trængende alligevel (og Wait-punktet bruges). + **Opsætning med flere stalde:** én traktor (eller flere) pr. stald med *Rotér mål: kun pålæsning*. Brug IKKE "pålæsning og aflæsning": AutoDrive vælger næste pålæsning FØR den roterer aflæsningsmålet, så behovet ville blive regnet for den forkerte stald. Pålæsningsmarkøren må ikke være fast på en diff --git a/tests/test_adSmartPickup.lua b/tests/test_adSmartPickup.lua index f84e3cb..0c93555 100644 --- a/tests/test_adSmartPickup.lua +++ b/tests/test_adSmartPickup.lua @@ -84,7 +84,9 @@ FS25_AutoDrive = { AutoDrive = { RT_NONE = RT_NONE, getSetting = function(settingName) return settingName == "rotateTargets" and RT_ONLYPICKUP or "andet" end, - getAllFillLevels = function(trailers) return trailers.level end, + getAllFillLevels = function(trailers) return trailers.level, trailers.capacity or 0, false, 0 end, + getAllUnits = function(vehicle) return vehicle.trailers or {level = 0} end, + getAllDischargeableUnits = function(vehicle) return vehicle.units or {} end, }, -- opgaven noterer hvad AutoDrive "ser" af indstillinger mens den opdaterer LoadAtDestinationTask = {update = function(self) @@ -93,6 +95,12 @@ FS25_AutoDrive = { if self.shouldFail then error("task boom") end end}, ADMultipleTargetsManager = {pickups = {}, getNextPickup = function() return "ORIGINAL" end}, + -- aflæsningsopgaven: original update tæller bare kald; finished markerer + UnloadAtDestinationTask = { + STATE_PATHPLANNING = 1, STATE_DRIVING = 2, + update = function(self) self.updates = (self.updates or 0) + 1 end, + finished = function(self) self.isFinished = true end, + }, ADGraphManager = { getMapMarkersInGroup = function(_, group) local result = {} @@ -142,6 +150,7 @@ local function makeSupplyDriver(firstMarker, secondMarker, fillTypes) end dofile("FS25_ADSmartPickup/adSupplyPlanner.lua") +dofile("FS25_ADSmartPickup/adUnloadWait.lua") dofile("FS25_ADSmartPickup/adSmartPickup.lua") listener:update(16) -- installerer hook'en local manager = FS25_AutoDrive.ADMultipleTargetsManager @@ -417,6 +426,115 @@ check("P4 stald: TMR (90 % tom) hentes før halm (10 % tom)", manager:getNextPic barnLevels[TMR] = 95000 check("P4 stald: halm hentes når TMR er fuld", manager:getNextPickup(cowDriver, false), 21) +-- ==== Wait-punkter: rest-læs + fuld stald -> vent ved nærmeste ledige Wait, tilbage når resten passer ==== +local UnloadTask = FS25_AutoDrive.UnloadAtDestinationTask +local waitBarnLevels, waitBarnCapacity = {}, {[STRAW] = 50000, [TMR] = 100000} +local function buildWaitWorld() + buildCowWorld() + waitBarnLevels = {[STRAW] = 50000, [TMR] = 10000} -- halm FULD + local barn2 = { + getName = function() return "Kostald" end, + getHusbandryFillLevel = function(_, fillType) return waitBarnLevels[fillType] or 0 end, + getHusbandryCapacity = function(_, fillType) return waitBarnCapacity[fillType] or 0 end, + getHusbandryFreeCapacity = function(_, fillType) return (waitBarnCapacity[fillType] or 0) - (waitBarnLevels[fillType] or 0) end, + getHusbandryIsFillTypeSupported = function(_, fillType) return waitBarnCapacity[fillType] ~= nil end, + } + unloadStations[1].targetStorages = nil + unloadStations[1].owningPlaceable = barn2 + addMarker(40, "Wait 1", "Cow Food", 5100, 0) + addMarker(41, "Wait 2", "Cow Food", 5300, 0) +end +-- traktor der holder ved stalden (markør 30) med rest-læs halm på vognen +local function makeUnloadingVehicle(name, node, x, restStraw) + local vehicle = makeSupplyDriver(markerByIndex(21), markerByIndex(30), {TMR, STRAW}) + vehicle.getName = function() return name end + vehicle.rootNode = node + nodePositions[node] = {x = x, z = 10} + vehicle.units = {{object = { + getFillUnitFillLevel = function() return restStraw end, + getFillUnitFillType = function() return STRAW end, + }, fillUnitIndex = 1}} + vehicle.trailers = {level = restStraw, capacity = 40000} + vehicle.targetReached = true + vehicle.ad.drivePathModule = { + isTargetReached = function() return vehicle.targetReached end, + setPathTo = function(self, id) self.pathTo = id end, + } + vehicle.ad.trailerModule = {isActiveAtTrigger = function() return false end} + local task = {vehicle = vehicle, destinationID = 30, state = UnloadTask.STATE_DRIVING, trailers = vehicle.trailers} + return vehicle, task +end + +-- W1: fuld stald + rest-læs -> omdirigeres til nærmeste ledige Wait; AD's original kaldes stadig +buildWaitWorld() +local v1, t1 = makeUnloadingVehicle("Fastrac A", 501, 5000, 35000) +local waitLogMark = #logLines +UnloadTask.update(t1, 16) +check("W1 omdirigeret til nærmeste Wait", t1.destinationID, 40) +check("W1 rute lagt til Wait", v1.ad.drivePathModule.pathTo, 40) +check("W1 original update kaldt", t1.updates, 1) +check("W1 log fortæller om ventning", countLogsSince(waitLogMark, "Wait 1"), 1) + +-- W2: anden traktor i samme situation -> Wait 1 er optaget -> Wait 2 +local v2, t2 = makeUnloadingVehicle("Fastrac B", 502, 5010, 20000) +UnloadTask.update(t2, 16) +check("W2 optaget Wait springes over", t2.destinationID, 41) + +-- W3: mens A venter, skjules rotationen (ellers kører AD videre fra Wait-punktet ved "rotér aflæsning") +check("W3 rotation skjult for ventende", FS25_AutoDrive.AutoDrive.getSetting("rotateTargets", v1), RT_NONE) +check("W3 andre indstillinger urørt", FS25_AutoDrive.AutoDrive.getSetting("useFolders", v1), "andet") + +-- W4: stalden får plads til HELE resten -> A sendes hjem, Wait 1 frigives, rotation tilbage +waitBarnLevels[STRAW] = 10000 -- 40000 fri >= 35000 +v1.targetReached = true -- står ved Wait 1 +UnloadTask.update(t1, 16) +check("W4 sendt tilbage til stalden", t1.destinationID, 30) +check("W4 rute lagt hjem", v1.ad.drivePathModule.pathTo, 30) +check("W4 rotation gendannet", FS25_AutoDrive.AutoDrive.getSetting("rotateTargets", v1), RT_ONLYPICKUP) +local v3, t3 = makeUnloadingVehicle("Fastrac C", 503, 5020, 30000) +waitBarnLevels[STRAW] = 50000 +UnloadTask.update(t3, 16) +check("W4 Wait 1 er ledig igen", t3.destinationID, 40) + +-- W5: plads til noget, men ikke hele resten -> B bliver ved Wait (kun hele læs kommer af) +waitBarnLevels[STRAW] = 45000 -- 5000 fri < 20000 +UnloadTask.update(t2, 16) +check("W5 delvis plads -> venter stadig", t2.destinationID, 41) + +-- W6: stalden har plads ved ankomst -> ingen omdirigering (AD læsser af som normalt) +buildWaitWorld() +waitBarnLevels[STRAW] = 10000 +local v6, t6 = makeUnloadingVehicle("Fastrac D", 506, 5000, 35000) +UnloadTask.update(t6, 16) +check("W6 plads ved ankomst -> uændret mål", t6.destinationID, 30) + +-- W7: ingen Wait-punkter -> uændret (AD venter som i dag) + én advarsel +buildCowWorld() +unloadStations[1].targetStorages = nil +unloadStations[1].owningPlaceable = { getHusbandryFillLevel = function() return 50000 end, getHusbandryCapacity = function() return 50000 end, + getHusbandryFreeCapacity = function() return 0 end, getHusbandryIsFillTypeSupported = function() return true end } +local v7, t7 = makeUnloadingVehicle("Fastrac E", 507, 5000, 35000) +local warnMark = #logLines +UnloadTask.update(t7, 16); UnloadTask.update(t7, 16) +check("W7 uden Wait-punkter -> uændret mål", t7.destinationID, 30) +check("W7 én advarsel", countLogsSince(warnMark, "WARN"), 1) + +-- W8: B og C venter stadig og holder begge punkter; stoppes de i AD, frigives punkterne +buildWaitWorld() +local v8, t8 = makeUnloadingVehicle("Fastrac F", 508, 5000, 35000) +UnloadTask.update(t8, 16) +check("W8 alle Wait-punkter optaget -> uændret", t8.destinationID, 30) +v2.ad.stateModule.active, v3.ad.stateModule.active = false, false +listener:update(16) +UnloadTask.update(t8, 16) +check("W8 venter ved Wait 1 efter frigivelse", t8.destinationID, 40) +UnloadTask.finished(t8) +check("W8 original finished kaldt", t8.isFinished, true) +local v9, t9 = makeUnloadingVehicle("Fastrac G", 509, 5000, 35000) +UnloadTask.update(t9, 16) +check("W8 Wait 1 frigivet efter finished", t9.destinationID, 40) +check("W8 rotation gendannet efter finished", FS25_AutoDrive.AutoDrive.getSetting("rotateTargets", v8), RT_ONLYPICKUP) + -- L1: moden valgte siloen -> AutoDrive skal stå og læsse (rotation skjules under opgaven) resetWorld() local loadLevels = {[SUGARBEET] = 5000} diff --git a/tests/test_adSupplyPlanner.lua b/tests/test_adSupplyPlanner.lua index e33e1ea..2ee2185 100644 --- a/tests/test_adSupplyPlanner.lua +++ b/tests/test_adSupplyPlanner.lua @@ -122,5 +122,41 @@ check("vare fabrikken ikke kender -> ikke beskyttet", ADSupplyPlanner.isProducti check("ingen produktioner -> ikke beskyttet", ADSupplyPlanner.isProductionInput({}, STRAW), false) check("produktion uden inputs/outputs tåles", ADSupplyPlanner.isProductionInput({{id = "x"}}, STRAW), false) +-- rankNeeds med læs-størrelse: varer hvor et FULDT læs passer går forud, ellers mest trængende +local sized = ADSupplyPlanner.rankNeeds({ + {fillType = STRAW, level = 40000, capacity = 50000, litersPerMinute = 500}, -- 80 min, kun 10000 fri + {fillType = TMR, level = 20000, capacity = 100000, litersPerMinute = 50}, -- 400 min, 80000 fri +}, 40000) +check("fuldt læs der passer vinder over mere trængende der ikke passer", sized[1].fillType, TMR) +check("den der ikke passer bevares som reserve", sized[2].fillType, STRAW) +check("fits markeres", sized[1].fits, true) +check("ikke-fits markeres", sized[2].fits, false) +local noneFit = ADSupplyPlanner.rankNeeds({ + {fillType = STRAW, level = 40000, capacity = 50000, litersPerMinute = 500}, + {fillType = TMR, level = 90000, capacity = 100000, litersPerMinute = 50}, +}, 40000) +check("passer intet -> mest trængende først", noneFit[1].fillType, STRAW) +check("uden læs-størrelse -> som før", ADSupplyPlanner.rankNeeds({{fillType = STRAW, level = 40000, capacity = 50000}})[1].fits, nil) + +-- Wait-punkter: navn med ordet "Wait" (uanset store/små) eller mappen "Wait" +check("'Cow 1 Food Wait' er wait-punkt", ADSupplyPlanner.isWaitMarker({name = "Cow 1 Food Wait", group = "Cow Food"}), true) +check("'wait 3' er wait-punkt", ADSupplyPlanner.isWaitMarker({name = "wait 3", group = "Andet"}), true) +check("mappen Wait tæller", ADSupplyPlanner.isWaitMarker({name = "P2", group = "Wait"}), true) +check("'Waiter' er ikke wait-punkt", ADSupplyPlanner.isWaitMarker({name = "Waiter", group = "X"}), false) +check("'Cow 1 Food' er ikke wait-punkt", ADSupplyPlanner.isWaitMarker({name = "Cow 1 Food", group = "Cow Food"}), false) + +-- nærmeste ledige wait-punkt; candidates = {{marker, x, z}}, claims = {[markerId] = vehicle} +local V1, V2 = {}, {} +local waitPoints = { + {marker = {id = 1, name = "Wait A"}, x = 0, z = 0}, + {marker = {id = 2, name = "Wait B"}, x = 100, z = 0}, + {marker = {id = 3, name = "Wait C"}, x = 300, z = 0}, +} +check("nærmeste ledige vælges", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {}, 10, 0, V1).marker.id, 1) +check("reserveret springes over", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {[1] = V2}, 10, 0, V1).marker.id, 2) +check("egen reservation tæller som ledig", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {[1] = V1}, 10, 0, V1).marker.id, 1) +check("alle optaget -> nil", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {[1] = V2, [2] = V2, [3] = V2}, 10, 0, V1), nil) +check("ingen punkter -> nil", ADSupplyPlanner.pickNearestFreeWaitPoint({}, {}, 0, 0, V1), nil) + print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) os.exit(failures == 0 and 0 or 1)